04 Spring Bean Lifecycle Common Errors #
Hello, I’m Fu Jian. In this lesson, we will discuss some issues related to the initialization and destruction processes of Spring Beans.
Although getting started with the Spring container is simple, and you can quickly use it by learning a few limited annotations, we still encounter some common errors in engineering practice. Especially when you don’t have a deep understanding of the Spring lifecycle, the potential conventions during initialization and destruction of classes may not be clear.
This can lead to situations where some errors can be quickly resolved with Spring’s exception prompts, but the underlying principles are not understood. On the other hand, some errors are not easy to discover in the development environment, resulting in more serious consequences in the production environment.
Next, we will analyze these common scenarios and their underlying principles in detail.
Case 1: NullPointerException thrown in the constructor #
Let’s start with an example. In the process of building a dormitory management system, we have a LightMgrService
class to manage the LightService
class, which controls the on and off state of the dormitory lights. We want to automatically call the check
method of LightService
when initializing LightMgrService
. The code is as follows:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class LightMgrService {
@Autowired
private LightService lightService;
public LightMgrService() {
lightService.check();
}
}
In the default constructor of LightMgrService
, we used the @Autowired
annotation to inject the LightService
member variable and called the check
method:
@Service
public class LightService {
public void start() {
System.out.println("turn on all lights");
}
public void shutdown() {
System.out.println("turn off all lights");
}
public void check() {
System.out.println("check all lights");
}
}
The above code defines the original class of the LightService
object.
From the implementation of the entire example, our expectation is that during the initialization process of LightMgrService
, LightService
can be automatically autowired because it is marked as @Autowired
. Then, the shutdown
method of LightService
can be automatically called during the execution of the LightMgrService
constructor. Finally, the message “check all lights” should be printed.
However, contrary to our expectations, we only get a NullPointerException
. The incorrect output is shown below:
Why does this happen?
Analysis #
Obviously, this is a common mistake for beginners, but the root cause of the problem lies in our lack of understanding of the Spring class initialization process. Here is a sequence diagram that describes some key points during the Spring startup process:
This diagram may seem complex at first glance, so let’s divide it into three parts:
- The first part involves registering some necessary system classes, such as bean post-processors, into the Spring container. This includes the
CommonAnnotationBeanPostProcessor
class that we are interested in this lesson. - The second part involves instantiating these post-processors and registering them with the Spring container.
- The third part involves instantiating all user-defined classes, calling the post-processors for auxiliary assembly, class initialization, etc.
The first and second parts are not the focus of our discussion today. I just want to let you know that the CommonAnnotationBeanPostProcessor
class is loaded and instantiated by Spring at a specific time.
Here are two additional points for you to expand your knowledge:
- Many necessary system classes, especially bean post-processors (such as
CommonAnnotationBeanPostProcessor
,AutowiredAnnotationBeanPostProcessor
, etc.), are loaded and managed by Spring and play a very important role in the Spring framework. - Through bean post-processors, Spring can flexibly call different post-processors in different scenarios. For example, I will now explain how to fix the issue in the following solution. The solution involves using the
PostConstruct
annotation, and its processing logic requires theCommonAnnotationBeanPostProcessor
(which is inherited fromInitDestroyAnnotationBeanPostProcessor
) post-processor.
Now let’s focus on the third part, which is the general process of initializing singleton classes in Spring. The basic process is getBean()->doGetBean()->getSingleton()
. If the bean does not exist, the createBean()->doCreateBean()
method is called to instantiate it.
The source code of the doCreateBean()
method is as follows:
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args) throws BeanCreationException {
// omit non-essential code
if (instanceWrapper == null) {
instanceWrapper = createBeanInstance(beanName, mbd, args);
}
final Object bean = instanceWrapper.getWrappedInstance();
// omit non-essential code
Object exposedObject = bean;
try {
populateBean(beanName, mbd, instanceWrapper);
exposedObject = initializeBean(beanName, exposedObject, mbd);
}
catch (Throwable ex) {
// omit non-essential code
}
}
The above code shows the three key steps of bean initialization in the sequence order. These steps are: createBeanInstance
(line 5), populateBean
(line 12), and initializeBean
(line 13). They correspond to the instantiation of the bean, injection of bean dependencies, and initialization of the bean (e.g., executing methods marked with @PostConstruct
).
The createBeanInstance
method that instantiates the bean calls DefaultListableBeanFactory.instantiateBean()
and SimpleInstantiationStrategy.instantiate()
in sequence, which ultimately leads to the call to BeanUtils.instantiateClass()
. The code is as follows:
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);
}
//omit non-essential code
}
Since the current language is not Kotlin, the ctor.newInstance()
method is ultimately called to instantiate the user-defined class LightMgrService
. The default constructor is automatically called during class instantiation, and Spring cannot control this process. At this time, the populateBean
method, responsible for autowiring, has not been executed yet, so the LightService
property of LightMgrService
is still null
. Therefore, it is reasonable to get a NullPointerException.
Issue Resolution #
Through source code analysis, we now know the root cause of the problem, which is that the autowiring behavior triggered by directly using @Autowired
on a member attribute occurs after the constructor is executed. Therefore, we can correct this issue by revising the code as follows:
@Component
public class LightMgrService {
private LightService lightService;
public LightMgrService(LightService lightService) {
this.lightService = lightService;
lightService.check();
}
}
In Lesson 02’s Case 2, we mentioned implicit injection of constructor parameters. When using the code above, the constructor parameter LightService
will be automatically injected with a bean of LightService
, so that there will be no null pointer exception when the constructor is executed. It can be said that using constructor parameters for implicit injection is a best practice in Spring because it successfully avoids the problem in Case 1.
Besides this correction method, are there any other ways?
In fact, after Spring injects the properties of a class, it will invoke the user-defined initialization method. After the populateBean
method, the initializeBean
method is called. Let’s take a look at its key code:
protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) {
// omitted non-key code
if (mbd == null || !mbd.isSynthetic()) {
wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
}
try {
invokeInitMethods(beanName, wrappedBean, mbd);
}
// omitted non-key code
}
Here you can see the execution of two key methods: applyBeanPostProcessorsBeforeInitialization
and invokeInitMethods
. They respectively handle the logic of @PostConstruct
annotations and the InitializingBean
interface. Let me explain them in detail.
1. applyBeanPostProcessorsBeforeInitialization
and @PostConstruct
The applyBeanPostProcessorsBeforeInitialization
method ultimately executes the buildLifecycleMetadata
method of the InitDestroyAnnotationBeanPostProcessor
(the parent class of CommonAnnotationBeanPostProcessor
):
private LifecycleMetadata buildLifecycleMetadata(final Class<?> clazz) {
// omitted non-key code
do {
// omitted non-key code
final List<LifecycleElement> currDestroyMethods = new ArrayList<>();
ReflectionUtils.doWithLocalMethods(targetClass, method -> {
// The value of this.initAnnotationType here is PostConstruct.class
if (this.initAnnotationType != null && method.isAnnotationPresent(this.initAnnotationType)) {
LifecycleElement element = new LifecycleElement(method);
currInitMethods.add(element);
// omitted non-key code
}
});
}
In this method, Spring will traverse and find methods annotated with PostConstruct.class
, return them to the upper level, and finally call those methods.
2. invokeInitMethods
and the InitializingBean
interface
The invokeInitMethods
method determines whether the current bean implements the InitializingBean
interface. Only in this case will Spring call the interface’s implementation method, afterPropertiesSet()
.
protected void invokeInitMethods(String beanName, final Object bean, @Nullable RootBeanDefinition mbd) throws Throwable {
boolean isInitializingBean = (bean instanceof InitializingBean);
if (isInitializingBean && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) {
// omitted non-key code
} else {
((InitializingBean) bean).afterPropertiesSet();
}
// omitted non-key code
}
By learning up to this point, the answer is apparent. We have two more ways to solve this problem.
- Add an
init
method and annotate it with@PostConstruct
:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class LightMgrService {
@Autowired
private LightService lightService;
@PostConstruct
public void init() {
lightService.check();
}
}
- Implement the
InitializingBean
interface and execute the initialization code in itsafterPropertiesSet()
method:
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class LightMgrService implements InitializingBean {
@Autowired
private LightService lightService;
@Override
public void afterPropertiesSet() throws Exception {
lightService.check();
}
}
Compared with the initial solution proposed, it is obvious that the two subsequent methods are not the most optimal for this case. However, each of these two methods has its own strengths in certain scenarios. Otherwise, why would Spring provide this feature, right?
Case 2: Unexpected Trigger of the shutdown Method #
In the previous example, I explained to you the most common issues with class initialization. Similarly, when a class is being destroyed, there are some hidden conventions that can lead to unnoticed errors.
Next, let’s look at another example using the same scenario. We can briefly review the implementation of LightService
, which includes a shutdown
method responsible for turning off all lights. The key code is as follows:
import org.springframework.stereotype.Service;
@Service
public class LightService {
// omitted other non-critical code
public void shutdown(){
System.out.println("shutting down all lights");
}
// omitted other non-critical code
}
In the previous example, the lights won’t be turned off when our dormitory management system restarts. However, as business requirements change, we may remove the @Service
annotation and use another way to create the bean: create a configuration class BeanConfiguration
(marked with @Configuration
) to create a bunch of beans, including creating a bean of type LightService
and registering it with the Spring container:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class BeanConfiguration {
@Bean
public LightService getTransmission(){
return new LightService();
}
}
Reuse the startup program from Case 1 with slight modifications to make Spring complete its startup and immediately close the current Spring context. In this way, it is equivalent to simulating the start and stop of the dormitory management system:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(Application.class, args);
context.close();
}
}
The above code doesn’t invoke any other methods. It simply initializes and loads all classes that conform to the conventions into the Spring container and then closes the current Spring container. As expected, this code will not produce any log output since we only changed the way the beans are created.
However, after running this code, we can see the message “shutting down all lights” printed on the console. Clearly, the shutdown
method was not executed as expected, resulting in an interesting bug: before using the new bean creation method, every time the dormitory management service is restarted, all the lights in the dormitory will not be turned off. But after the modification, only when the service is restarted, the lights are unexpectedly turned off. How do we understand this bug?
Analysis of the Case #
Through debugging, we found that only objects registered with the Spring container using the @Bean
annotation will have their shutdown
method automatically called when the Spring container is closed. However, when using @Component
(Service is also a type of Component) to automatically inject the current class into the Spring container, the shutdown
method will not be automatically executed.
We can try to find some clues in the code of the bean annotation class. We can see that the destroyMethod
attribute has a very long comment, which basically answers most of our doubts about this issue.
For bean objects registered with the @Bean
annotation, if the user does not set the destroyMethod
attribute, its value is AbstractBeanDefinition.INFER_METHOD
. At this time, Spring will check whether the original class of the current bean object contains a method named shutdown
or close
. If it does, this method will be recorded by Spring and automatically executed when the container is destroyed. Of course, if it doesn’t, then nothing will happen naturally.
Let’s continue to look at the Spring source code to further analyze this issue.
First, we can search for references to the INFER_METHOD
enum value, and it is easy to find the method DisposableBeanAdapter#inferDestroyMethodIfNecessary
that uses this enum value:
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);
}
We can see that the logic of the code is identical to the comment of the destroyMethod
attribute in the bean annotation class. If destroyMethodName
is equal to INFER_METHOD
and the current class does not implement the DisposableBean
interface, then it first looks for the close
method of the class. If it cannot find it, it continues to look for the shutdown
method after throwing an exception. If it finds it, it returns the name of the method (close
or shutdown
).
Next, we continue to trace the references and ultimately reach the calling chain from top to bottom: doCreateBean
-> registerDisposableBeanIfNecessary
-> registerDisposableBean(new DisposableBeanAdapter)
-> inferDestroyMethodIfNecessary
.
Then, we trace back to the top-level doCreateBean
method, and the code is as follows:
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
throws BeanCreationException {
// omitted non-critical code
if (instanceWrapper == null) {
instanceWrapper = createBeanInstance(beanName, mbd, args);
}
// omitted non-critical code
// Initialize the bean instance.
Object exposedObject = bean;
try {
populateBean(beanName, mbd, instanceWrapper);
exposedObject = initializeBean(beanName, exposedObject, mbd);
}
// omitted non-critical code
// Register bean as disposable.
try {
registerDisposableBeanIfNecessary(beanName, bean, mbd);
}
catch (BeanDefinitionValidationException ex) {
throw new BeanCreationException(
mbd.getResourceDescription(), beanName, "Invalid destruction signature", ex);
}
return exposedObject;
}
Here, we can summarize the doCreateBean
method. It can be said that doCreateBean
manages almost all key points in the entire lifecycle of the bean, directly responsible for the creation and dependency injection of the bean, as well as the customization of the class initialization method’s callback and the registration of disposable methods.
Next, let’s take a look at the registerDisposableBean
method:
public void registerDisposableBean(String beanName, DisposableBean bean) {
// Omitted non-critical code
synchronized (this.disposableBeans) {
this.disposableBeans.put(beanName, bean);
}
// Omitted non-critical code
}
In the registerDisposableBean
method, the DisposableBeanAdapter
class (which has the destroyMethodName
attribute to record which destroy method to use) is instantiated and added to the DefaultSingletonBeanRegistry#disposableBeans
attribute. The disposableBeans
temporarily stores these DisposableBeanAdapter
instances until the close
method of AnnotationConfigApplicationContext
is called.
When the close
method of AnnotationConfigApplicationContext
is called, which means when the Spring container is destroyed, it will ultimately call DefaultSingletonBeanRegistry#destroySingleton
. This method will iterate through the disposableBeans
attribute to get the DisposableBean
one by one, and then call the close
or shutdown
methods in them:
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);
}
Obviously, in the end, our example calls the LightService#shutdown
method to turn off all the lights.
Issue Fix #
Now that we know the root cause of the problem, solving it is very simple.
We can solve it by avoiding defining methods with verbs that have special meanings in Java classes. Of course, if you must define a method named close
or shutdown
, you can also solve this problem by setting the destroyMethod
attribute inside the Bean
annotation to an empty string.
The first modification method is relatively simple, so here I will only show the second modification method, with the following code:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class BeanConfiguration {
@Bean(destroyMethod="")
public LightService getTransmission(){
return new LightService();
}
}
In addition, I would like to add a little more explanation about this issue. If we can cultivate good coding habits and carefully read the comments of an annotation before using it, we can greatly reduce the probability of encountering this problem.
However, speaking of this, you may still be puzzled as to why the shutdown
method of the LightService
injected by @Service
cannot be executed. Here, I would like to supplement the explanation.
To execute it, a DisposableBeanAdapter
must be added, 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 {
// Omitted non-critical code
}
}
}
Referring to the above code, the key statement is:
!mbd.isPrototype() && requiresDestruction(bean, mbd)
Obviously, in the case of code changes before and after the example, we are both singletons, so the only difference lies in whether it meets the requiresDestruction
condition. Referring to its code, the key call is 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 we’re using @Service
to create the bean, then the destroyMethodName
we obtain in the above code is actually null. And if we use the @Bean
approach, the default value is AbstractBeanDefinition.INFER_METHOD
, referring to the definition of Bean
:
public @interface Bean {
// Omitted non-critical code
String destroyMethod() default AbstractBeanDefinition.INFER_METHOD;
}
Continue to compare the code, you will find that the LightService
marked by @Service
does not implement AutoCloseable
or DisposableBean
, and there is no DisposableBeanAdapter
added in the end. So the shutdown method we defined is not called in the end.
Key Review #
Through the above two cases, I believe you have gained a certain understanding of the Spring lifecycle, especially the initialization and destruction process of the Bean. Let’s review the key points again:
- The DefaultListableBeanFactory class is the soul of Spring Bean, and the core is the doCreateBean method, which controls the creation of Bean instances, dependency injection of Bean objects, callback of customized class initialization methods, and registration of Disposable methods, and other crucial steps.
- The post-processor is one of the most elegant designs in Spring, and many functional annotations processing is done using post-processors. Although this lesson did not introduce it in detail, in the first case, the “supplementary” initialization action of the Bean object was completed in the CommonAnnotationBeanPostProcessor (inherited from InitDestroyAnnotationBeanPostProcessor) post-processor.
Thought Question #
In the class LightService in Case 2, if we don’t use the Bean method to inject it into the Spring container in the Configuration annotation class, but insist on using @Service to automatically inject it into the container, while implementing the Closeable interface, the code is as follows:
import org.springframework.stereotype.Component;
import java.io.Closeable;
@Service
public class LightService implements Closeable {
public void close() {
System.out.println("turn off all lights);
}
//Omit non-essential code
}
Will the interface method close() also be automatically executed when the Spring container is destroyed?
I look forward to your answer in the comments section!