“This is the 23rd day of my participation in the Gwen Challenge in November. Check out the details: The Last Gwen Challenge in 2021”
Hello, I’m looking at the mountains.
In distributed systems, we will need an ID generator component that implements ids that help us generate sequential or business-meaning ids.
There are many classic ID generation methods, such as database increment column (increment primary key or sequence), Snowflake algorithm, Meituanleaf algorithm, etc., so there will be some corporate and business level ID generator components. This paper is through BeanPostProcessor dynamic injection ID generator actual combat.
In Spring, injection can be implemented in many ways, such as starter of SpringBoot, initialization of ID generator Bean in custom Configuration, @autowired or @Resource injection in business code. Out of the box. This approach is simple and straightforward, but it also has the disadvantage of being too simple, lacking user custom entry.
Consider a real scenario where the ID is unique in the same business document, but can be repeated in different documents. Moreover, these algorithms lock the shared resources when generating ids in order to keep the results of multiple threads unique. If different services have different concurrency scenarios, low-concurrency services may be blocked by high-concurrency services to obtain ids, resulting in performance loss. Therefore, we should consider isolating the ID generator based on the business so that springBoot’s starter is not flexible enough.
implementation
Based on the above requirements, we can implement our logic in several steps:
- Custom property annotations that determine whether a property object needs to be injected
- Defines an ID generator interface, an implementation class, and a factory class that creates different ID generator implementation objects based on the definition
- Define BeanPostProcessor to find properties defined using custom annotations to implement injection
Custom annotations
Start by customizing an annotation. You can define a value attribute as an identifier to isolate the business:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface IdGeneratorClient {
/** * ID Generator name **@return* /
String value(a) default "DEFAULT";
}
Copy the code
Defining the ID generator
Define the interface to the ID generator:
public interface IdGenerator {
String groupName(a);
long nextId(a);
}
Copy the code
Implement ID generator interface, use AtomicLong to implement increment, and consider ID generator is grouping, through ConcurrentHashMap implementation ID generator hold:
class DefaultIdGenerator implements IdGenerator {
private static final Map<String, AtomicLong> ID_CACHE = new ConcurrentHashMap<>(new HashMap<>());
private final String groupName;
DefaultIdGenerator(final String groupName) {
this.groupName = groupName;
synchronized (ID_CACHE) {
ID_CACHE.computeIfAbsent(groupName, key -> new AtomicLong(1)); }}@Override
public String groupName(a) {
return this.groupName;
}
@Override
public long nextId(a) {
return ID_CACHE.get(this.groupName).getAndIncrement(); }}Copy the code
As designed above, we need a factory class to create the ID generator. The simplest implementation is used in the example, but when we actually use it, we can also use the more flexible SPI implementation (for SPI implementation, I’ll dig a hole here, and I’ll fill in the hole later) :
public enum IdGeneratorFactory {
INSTANCE;
private static final Map<String, IdGenerator> ID_GENERATOR_MAP = new ConcurrentHashMap<>(new HashMap<>());
public synchronized IdGenerator create(final String groupName) {
return ID_GENERATOR_MAP.computeIfAbsent(groupName, key -> newDefaultIdGenerator(groupName)); }}Copy the code
Define the BeanPostProcessor
This is the core of the extension. Our implementation logic is as follows:
- Scan all attributes of the bean and find definitions
IdGeneratorClient
Attributes of annotations - annotated
value
Value as a grouping identifier for the ID generator - use
IdGeneratorFactory
This factory class generates instances of the ID generator, which returns newly created or already defined instances - Write the ID generator instance to the bean through reflection
public class IdGeneratorBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(final Object bean, final String beanName) throws BeansException {
return bean;
}
@Override
public Object postProcessAfterInitialization(final Object bean, final String beanName) throws BeansException {
parseFields(bean);
return bean;
}
private void parseFields(final Object bean) {
if (bean == null) {
return; } Class<? > clazz = bean.getClass(); parseFields(bean, clazz);while(clazz.getSuperclass() ! =null&&! clazz.getSuperclass().equals(Object.class)) { clazz = clazz.getSuperclass(); parseFields(bean, clazz); }}private void parseFields(finalObject bean, Class<? > clazz) {
if (bean == null || clazz == null) {
return;
}
for (final Field field : clazz.getDeclaredFields()) {
try {
final IdGeneratorClient annotation = AnnotationUtils.getAnnotation(field, IdGeneratorClient.class);
if (annotation == null) {
continue;
}
final String groupName = annotation.value();
finalClass<? > fieldType = field.getType();if (fieldType.equals(IdGenerator.class)) {
final IdGenerator idGenerator = IdGeneratorFactory.INSTANCE.create(groupName);
invokeSetField(bean, field, idGenerator);
continue;
}
throw new RuntimeException("Unknown field type cannot be initialized, bean:" + bean + "The field," + field);
} catch (Throwable t) {
throw new RuntimeException("Failed to initialize field, bean=" + bean + ", the field ="+ field, t); }}}private void invokeSetField(final Object bean, final Field field, final Object param) { ReflectionUtils.makeAccessible(field); ReflectionUtils.setField(field, bean, param); }}Copy the code
Realize the BeanPostProcessor interface needs to be done postProcessBeforeInitialization and postProcessAfterInitialization the definition of the two methods. Here is the Bean instantiation process in Spring:
A diagram of the Bean instantiation process in Spring
As you can see from the figure, when Spring calls these two methods of BeanPostProcessor, the bean is already instantiated, and all properties that can be injected have been injected, making it a complete bean. And the return value of both methods can be either the original bean instance or the wrapped instance, depending on our definition.
Test our code
Write a test case to verify that our implementation works:
@SpringBootTest
class SpringBeanPostProcessorApplicationTests {
@IdGeneratorClient
private IdGenerator defaultIdGenerator;
@IdGeneratorClient("group1")
private IdGenerator group1IdGenerator;
@Test
void contextLoads(a) {
Assert.notNull(defaultIdGenerator, "Injection failed");
System.out.println(defaultIdGenerator.groupName() + "= >" + defaultIdGenerator.nextId());
Assert.notNull(group1IdGenerator, "Injection failed");
for (int i = 0; i < 5; i++) {
System.out.println(defaultIdGenerator.groupName() + "= >" + defaultIdGenerator.nextId());
System.out.println(group1IdGenerator.groupName() + "= >"+ group1IdGenerator.nextId()); }}}Copy the code
The running results are as follows:
DEFAULT => 1 DEFAULT => 2 group1 => 1 DEFAULT => 3 group1 => 2 DEFAULT => 4 group1 => 3 DEFAULT => 5 group1 => 4 DEFAULT => 6 group1 => 5Copy the code
As you can see, the default ID generator is generated separately from the one defined with the name group1, as expected.
At the end of the article think
We have implemented automatic injection of custom business objects through BeanPostProcessor. The above implementation is relatively simple and has many extendible areas, such as factory method implementation, which can create ID generator objects more flexibly by SPI. At the same time, considering the distributed scenario, we can also implement remote ID generation logic in the ID generator implementation class by injecting RPC instances.
There’s no limit to how you play. It’s up to our imagination. You can pay attention to the public account “mountain hut”, reply to “Spring” to obtain the source code.
Recommended reading
- SpringBoot: elegant response to achieve results in one move
- SpringBoot: How to handle exceptions gracefully
- SpringBoot: Dynamically injects the ID generator through the BeanPostProcessor
- SpringBoot: Customizes Filter to gracefully obtain request parameters and response results
- SpringBoot: Elegant use of enumeration parameters
- SpringBoot: Elegant use of enumeration parameters (Principles)
- SpringBoot: Gracefully use enumeration parameters in the RequestBody
- SpringBoot: Gracefully using enumeration parameters in the RequestBody
- Do unit tests with JUnit5+MockMvc+Mockito
- SpringBoot: Loads and reads resource files
Hello, I’m looking at the mountains. Swim in the code, play to enjoy life. If this article is helpful to you, please like, bookmark, follow. Welcome to follow the public account “Mountain Hut”, discover a different world.