Some problems in the initialization and destruction of spring beans.
- Some bug s can be quickly solved under the Spring exception prompt, but they don't understand the underlying principle
- Some errors are not easy to be found in the development environment, resulting in serious consequences on the production line
1 implicit injection using constructor parameters
Common bug s in class initialization. When building the dormitory management system, LightMgrService is used to manage LightService and control the on and off of dormitory lights.
Now it is expected to automatically call LightService#check when LightMgrService is initialized to check whether the circuits of all dormitory lights are normal:
We call the member variable LightService#check injected through @Autoware in the default constructor of LightMgrService:
- Original class of LightService object
Expected phenomenon:
- During the initialization of LightMgrService, LightService can be automatically assembled because it is marked with * * @ Autowired * *
- During the execution of LightMgrService constructor, LightService#check() can be called automatically
- Print check all lights
However, contrary to our wishes, we will only get NPE:
1.1 source code analysis
The root cause is that you don't know enough about the Spring class initialization process. The following sequence diagram describes some key nodes during Spring startup:
- Register some necessary system classes, such as Bean postprocessor, into the Spring container, including commonannotation beanpostprocessor
- Instantiate these postprocessors and register them with the Spring container
- Instantiate all user-defined classes and call the post processor for auxiliary assembly, class initialization, etc.
When was the CommonAnnotationBeanPostProcessor postprocessor class loaded and instantiated by Spring?
- Many necessary system classes, such as Bean post processor (common annotation beanpostprocessor, Autowired annotation beanpostprocessor, etc.), are uniformly loaded and managed by Spring
- Through the Bean post processor, Spring can flexibly call different post processors in different scenarios, such as @ PostConstruct. Its processing logic needs to use the common annotation beanpostprocessor (inherited from initdestroyannotation beanpostprocessor)
General process of Spring initializing singleton class:
- getBean()
- doGetBean()
- getSingleton()
If it is found that the Bean does not exist, call
createBean()=>doCreateBean()
Instantiate.
doCreateBean()
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args) throws BeanCreationException { // ... if (instanceWrapper == null) { // 1. instanceWrapper = createBeanInstance(beanName, mbd, args); } final Object bean = instanceWrapper.getWrappedInstance(); // ... Object exposedObject = bean; try { // 2. populateBean(beanName, mbd, instanceWrapper); // 3. exposedObject = initializeBean(beanName, exposedObject, mbd); } catch (Throwable ex) { // ... }
Key steps of Bean initialization:
- Instantiate Bean
- Inject Bean dependency
- Initialize the Bean (for example, the method that executes the @ PostConstruct tag)
The createBeanInstance of the instantiated Bean is called in sequence:
- DefaultListableBeanFactory.instantiateBean()
- SimpleInstantiationStrategy.instantiate()
Finally, execute to BeanUtils.instantiateClass():
public static <T> T instantiateClass(Constructor<T> ctor, Object... args) throws BeanInstantiationException { Assert.notNull(ctor, "Constructor must not be null"); try { ReflectionUtils.makeAccessible(ctor); return (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isKotlinType(ctor.getDeclaringClass()) ? KotlinDelegate.instantiateClass(ctor, args) : ctor.newInstance(args)); } catch (InstantiationException ex) { throw new BeanInstantiationException(ctor, "Is it an abstract class?", ex); } // ... }
Finally, ctor.newInstance() is called to instantiate the user-defined class LightMgrService, and the default constructor is automatically called when the class is instantiated, which Spring cannot control.
At this time, the populateBean method responsible for automatic assembly has not been executed, and the attribute LightService of LightMgrService is still null, resulting in NPE.
correct
The problem is that the assembly behavior caused by using @ Autowired to directly mark the member attribute occurs after the constructor executes.
Therefore, the following solutions can be adopted:
Constructor Injection
When using the above code, the constructor parameter LightService will be automatically injected into the Bean of LightService, so as to avoid NPE when the constructor executes.
After the class attribute injection is completed, Spring will call back the initialization method defined by us. That is, after the populateBean method, the
AbstractAutowireCapableBeanFactory#initializeBean
- applyBeanPostProcessorsBeforeInitialization @ PostConstruct
- Invokeinitialmethods handles the InitializingBean interface
Logic of two different initialization schemes
applyBeanPostProcessorsBeforeInitialization and @ PostConstruct
The applyBeanPostProcessorsBeforeInitialization method is finally executed to
InitDestroyAnnotationBeanPostProcessor#buildLifecycleMetadata:
private LifecycleMetadata buildLifecycleMetadata(final Class<?> clazz) { // ... do { // ... final List<LifecycleElement> currDestroyMethods = new ArrayList<>(); ReflectionUtils.doWithLocalMethods(targetClass, method -> { // initAnnotationType is PostConstruct.class if (this.initAnnotationType != null && method.isAnnotationPresent(this.initAnnotationType)) { LifecycleElement element = new LifecycleElement(method); currInitMethods.add(element); // ... }
In this method, Spring will traverse and find the method annotated by PostConstruct.class, return to the upper layer, and finally call this method.
Invokeinitialmethods and InitializingBean interfaces
Give the bean a chance to respond. Now all its properties have been set, and have a chance to know the bean factory (this object) it owns. This means checking whether the bean implements InitializingBean or customizes the init method.
If so, call the necessary callback.
Invokeinitialmethods will judge whether the current Bean implements the InitializingBean interface. Only when the interface is implemented, Spring will call the Bean's interface implementation method afterpropertieset().
There are two other ways:
init method & & @ postconstruct
Implement the InitializingBean interface and call back afterpropertieset()
For this case, the latter two schemes are not optimal.
However, in some scenarios, the two schemes have their own advantages.
2 the shutdown method is triggered unexpectedly
When the class is destroyed, it is also easy to write a pile of bug s.
LightService#shutdown, responsible for turning off the lights:
In the previous case, if the dormitory system was restarted, the light would not be turned off. However, with business changes, you may remove @ Service and use another way to generate beans: create a Configuration class BeanConfiguration (marked @ Configuration) to create a pile of beans, including creating beans of LightService type and registering them in the Spring container:
After Spring is started, close the current Spring context immediately, which can simulate the start and stop of the dormitory management system:
The above code does not call any other methods. It just initializes and loads all classes that meet the contract into the Spring container, and then closes the current Spring container.
Expectation: there will be no log after running, and only the generation method of Bean will be changed.
After running, the console Prints:
Obviously, the shutdown method is not executed as expected, which leads to an interesting bug:
- Before using the new Bean generation method, every time the dormitory management service is restarted, all the lights in the dormitory will not be turned off
- However, after the modification, the lights are turned off unexpectedly as long as the service is restarted
Can you understand the bug?
Source code analysis
Discovery:
- Only objects registered to the Spring container using Bean annotations will automatically call shutdown when the Spring container is closed
- When the current class is automatically injected into the Spring container using @ Component, the shutdown method will not be executed automatically
You can try to find some clues in the code of the Bean annotation class. You can see the property destroyMethod.
If the user does not set the destroyMethod property of the Bean object registered with the Bean annotated method, its property value is abstractbeandefinition.reference_ METHOD.
At this time, Spring will check whether there is a method named shutdown or close in the original class of the current Bean object:
- Yes, this method will be recorded by Spring and executed automatically when the container is destroyed
- No, it's okay
Find INFER_METHOD enumeration value, it is easy to find a way to use the enumeration value
DisposableBeanAdapter#inferDestroyMethodIfNecessary
private String inferDestroyMethodIfNecessary(Object bean, RootBeanDefinition beanDefinition) { String destroyMethodName = beanDefinition.getDestroyMethodName(); if (AbstractBeanDefinition.INFER_METHOD.equals(destroyMethodName) ||(destroyMethodName == null && bean instanceof AutoCloseable)) { if (!(bean instanceof DisposableBean)) { try { // Try to find the close method return bean.getClass().getMethod(CLOSE_METHOD_NAME).getName(); } catch (NoSuchMethodException ex) { try { // Try to find the shutdown method return bean.getClass().getMethod(SHUTDOWN_METHOD_NAME).getName(); } catch (NoSuchMethodException ex2) { // no candidate destroy method found } } } return null; } return (StringUtils.hasLength(destroyMethodName) ? destroyMethodName : null); }
Code logic and
Comments on destroyMethod attribute in Bean annotation class:
Exactly.
- destroyMethodName==INFER_ Method & & the current class does not implement the DisposableBean interface
Find the close method of the class first:- can't find
Just after throwing the exception, continue to look for the shutdown method - find
Returns its method name (close or shutdown)
- can't find
Then, continue to find references level by level, and the final call chain is as follows from top to bottom:
- doCreateBean
- registerDisposableBeanIfNecessary
- registerDisposableBean(new DisposableBeanAdapter)
- inferDestroyMethodIfNecessary
Then, we trace back to the top-level doCreateBean:
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args) throws BeanCreationException { // Instantiate bean if (instanceWrapper == null) { instanceWrapper = createBeanInstance(beanName, mbd, args); } // ... // Initialize the bean instance Object exposedObject = bean; try { populateBean(beanName, mbd, instanceWrapper); exposedObject = initializeBean(beanName, exposedObject, mbd); } // ... // Register bean as disposable. try { registerDisposableBeanIfNecessary(beanName, bean, mbd); } catch (BeanDefinitionValidationException ex) { throw new BeanCreationException( mbd.getResourceDescription(), beanName, "Invalid destruction signature", ex); } return exposedObject; }
doCreateBean manages almost all key nodes in the whole life cycle of a Bean and is directly responsible for the birth, aging and death of Bean objects. Its main functions include:
- Creation of Bean instance
- Bean object dependent injection
- Callback of custom class initialization method
- Registration of the Disposable method
Next, continue to view registerDisposableBean:
public void registerDisposableBean(String beanName, DisposableBean bean) { synchronized (this.disposableBeans) { this.disposableBeans.put(beanName, bean); } }
The DisposableBeanAdapter class whose property destroyMethodName records which destruction method is used is instantiated
And added to the DefaultSingletonBeanRegistry#disposableBeans property. disposableBeans will temporarily stage these DisposableBeanAdapter instances until AnnotationConfigApplicationContext#close is called.
When AnnotationConfigApplicationContext#close is called, that is, when the Spring container is destroyed, it will eventually call DefaultSingletonBeanRegistry#destroySingleton:
- Traverse the disposableBeans property
- Get DisposableBean one by one
- Call its close or shutdown in turn
public void destroySingleton(String beanName) { // Remove a registered singleton of the given name, if any. removeSingleton(beanName); // Destroy the corresponding DisposableBean instance. DisposableBean disposableBean; synchronized (this.disposableBeans) { disposableBean = (DisposableBean) this.disposableBeans.remove(beanName); } destroyBean(beanName, disposableBean); }
The case calls the LightService#shutdown method to turn off all lights.
correct
Avoid defining some methods with special meaning verbs in Java classes.
If you must define a method named close or shutdown, you can set the destroyMethod property in the Bean annotation to null. As follows:
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class BeanConfiguration { @Bean(destroyMethod="") public LightService getTransmission() { return new LightService(); } }
Why can't the shutdown method of LightService injected by @ Service be executed? To execute, you must add a DisposableBeanAdapter, and its addition is conditional:
protected void registerDisposableBeanIfNecessary(String beanName, Object bean, RootBeanDefinition mbd) { AccessControlContext acc = (System.getSecurityManager() != null ? getAccessControlContext() : null); if (!mbd.isPrototype() && requiresDestruction(bean, mbd)) { if (mbd.isSingleton()) { // Register a DisposableBean implementation that performs all destruction // work for the given bean: DestructionAwareBeanPostProcessors, // DisposableBean interface, custom destroy method. registerDisposableBean(beanName, new DisposableBeanAdapter(bean, beanName, mbd, getBeanPostProcessors(), acc)); } else { //Omit non critical code } } }
The key statements are:
!mbd.isPrototype() && requiresDestruction(bean, mbd)
Before and after the case code is modified, we are single cases, so the difference only lies in whether the requireddestruction condition is met.
DisposableBeanAdapter#hasDestroyMethod: public static boolean hasDestroyMethod(Object bean, RootBeanDefinition beanDefinition) { if (bean instanceof DisposableBean || bean instanceof AutoCloseable) { return true; } String destroyMethodName = beanDefinition.getDestroyMethodName(); if (AbstractBeanDefinition.INFER_METHOD.equals(destroyMethodName)) { return (ClassUtils.hasMethod(bean.getClass(), CLOSE_METHOD_NAME) || ClassUtils.hasMethod(bean.getClass(), SHUTDOWN_METHOD_NAME)); } return StringUtils.hasLength(destroyMethodName); }
- If @ Service is used to generate beans, the destroyMethodName obtained by the above code is null
- Use @ Bean, and the default value is abstractbeandefinition.reference_ Method, refer to Bean definition:
public @interface Bean { //Omit other non critical code String destroyMethod() default AbstractBeanDefinition.INFER_METHOD; }
@The LightService marked with Service does not implement AutoCloseable and DisposableBean, and finally does not add a DisposableBeanAdapter. So in the end, the shutdown method we defined was not called.
summary
DefaultListableBeanFactory class is the soul of Spring Bean. Its core is its doCreateBean, which controls key nodes such as the creation of Bean instances, the injection of Bean object dependencies, the callback of custom class initialization methods and the registration of Disposable methods.