04 Spring Bean Lifecycle Common Errors

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:

  1. 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.
  2. 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 the CommonAnnotationBeanPostProcessor (which is inherited from InitDestroyAnnotationBeanPostProcessor) 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.

  1. 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();
    }
}
  1. Implement the InitializingBean interface and execute the initialization code in its afterPropertiesSet() 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:

  1. 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.
  2. 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!