Dev Learning Lab

The labs on this site need JavaScript to run. The same content is written out below.

Learn › Spring › IoC container and DI

Bean lifecycle callbacks

Why is an injected field null when you use it in the constructor?

The container handles a bean in a fixed order. It creates the object with the constructor, injects the dependencies, calls the initialization callbacks, lets the bean be used, and calls the destruction callbacks when the container is closed.

Checked against real execution · All 20 combinations were run on Spring Boot 4.1.1 and the order of the calls was checked against the results

Concepts in this lab

Bean lifecycle
This is the process from the creation of a bean to its end. It proceeds in this order: object creation, dependency injection, the initialization callback, use, and the destruction callback when the container shuts down. Put initialization work that must run after all dependencies are injected in @PostConstruct, and put cleanup work before shutdown in @PreDestroy.
@PostConstruct and @PreDestroy
These are annotations that mark the methods to be called when a bean is initialized and when it is destroyed. The container calls the method annotated with @PostConstruct after it creates the object and finishes dependency injection, and it calls the method annotated with @PreDestroy when the container is closed. So setup work that needs an object injected through a field or a setter belongs in a @PostConstruct method, not in the constructor (an object injected through the constructor can also be used inside the constructor). They are not annotations of Spring but annotations from the jakarta.annotation package. For a prototype-scoped bean, @PreDestroy is not called.
InitializingBean and DisposableBean
These are two interfaces of Spring that define the methods to be called when a bean is initialized and when it is destroyed. If a class implements InitializingBean, afterPropertiesSet() is called after dependency injection finishes, and if it implements DisposableBean, destroy() is called when the container is closed. Because the class must implement interfaces of Spring, the code is coupled to Spring. The same work can also be done with @PostConstruct and @PreDestroy, or with initMethod and destroyMethod of @Bean. When all three are used together, they are called in this order: the annotations, the interfaces, and then the methods specified in @Bean.
Constructor injection, setter injection, and field injection
These are three ways to supply a dependency. Constructor injection receives the dependencies when the object is created, so the fields can be final and an object is never created with a dependency missing; Spring recommends it. Setter injection is used for optional dependencies. Field injection puts @Autowired on a field; the code is short, but it is hard to create the object without Spring in order to test it.
@Configuration and @Bean
@Configuration marks a configuration class that contains bean definitions, and the object returned by each @Bean method inside it is registered as a bean. Use them when you need to make a bean from a class in an external library that you cannot modify, or when you need to control the creation process yourself. Classes that you write yourself are usually registered with @Component or a related annotation.

Try these

Running with the default settings

  1. Container startup: The container registers two beans, priceRepository and priceCache. The scope of priceCache is singleton. Singleton beans are created during startup.
  2. Constructor: The container calls the constructor of PriceCache to create the object. Field injection happens after the object is created, so repository as seen inside the constructor is still null.
  3. this.repository.findAll() inside the constructor: repository is null, so a NullPointerException is thrown. The constructor could not finish, so the object was not created, and the container aborts startup.

Result of this run

Code

@Component
class PriceCache {
    @Autowired
    private PriceRepository repository;
    private Map<String, Integer> prices;

    PriceCache() {
        System.out.println("constructor: repository = " + this.repository);
        prices = this.repository.findAll();   // reads the price list in advance
    }

    int price(String name) {
        return prices.get(name);
    }
}

@Component
class PriceRepository {
    Map<String, Integer> findAll() {
        return new HashMap<>(Map.of("apple", 1000));
    }
}

// Where it is used
PriceCache priceCache = context.getBean(PriceCache.class);
int price = priceCache.price("apple");
context.close();                              // closes the container

Assumptions of this model

All 20 combinations were run on Spring Boot 4.1.1 (Spring Framework 7.0.9, jakarta.annotation-api 3.0.0) on this PC and checked against the results. For each combination, source code with the same structure as on the screen was compiled, the container was started, the bean was retrieved and price("apple") was called once, and the container was closed. Along the way, the order in which the constructor and each callback were called, and whether the repository field was null at that moment, were recorded and compared one by one with the order in the model. The exception chain of the startup failure was BeanCreationException → BeanInstantiationException → NullPointerException, and the screen shows the innermost cause. Simplifications on the screen: the "dependency injection" stage cannot be recorded separately, so the fact that the field was null in the constructor and set in the initialization callback was taken as confirmation that injection happened in between. Setter injection, and the destruction order among several beans when the container is closed, are not covered. It was also confirmed by execution that, even when destroyMethod is not written in @Bean, a public close() method is called when the container is closed (and is not called for a prototype bean). Two things were not confirmed by execution and follow the official Spring documentation: that @PostConstruct and @PreDestroy are the recommended approach, and that when the process is forcibly terminated, the container cannot close and the destruction callbacks are not called.

Previous lab: Bean scopes · Next lab: Proxies and self-invocation

Concepts in this lab

What this lab shows

Settings

Code


      
    

Diagram

Run comparison

Try these