One of the main responsibilities of the Spring Container is managing objects known as beans, which form the backbone of any Spring application. Managing these objects is a complex process consisting of multiple steps, and it uses a lot of internal concepts of the framework.

In this topic, we will dive into the lifecycle of beans and learn about some additional functionalities provided by the framework to customize the lifecycle. For simplicity, we will focus only on the most essential parts of the lifecycle for now because learning all of them at once might be quite overwhelming for a single topic.

High-level overview of bean lifecycle

As you know, when a Spring application is launched, the Spring Container gets started. The container is mainly responsible for managing the lifecycle of beans from their creation to destruction. The following picture gives us a high-level view of it.

As you can see, once the container is started, the lifecycle of a bean takes place. The container creates a new bean object, then injects its dependencies and performs some additional initialization that can be customized by the programmer. After that, the bean is ready for use by the application. The lifecycle of a bean ends with its destruction when the container is shut down.

The presented lifecycle is the same for both @Bean-annotated methods and @Component-annotated classes. However, there is a significant difference between singletons and beans annotated with Scope("prototype"). Spring doesn’t conduct destroy prototypes and doesn’t allow us to customize them.

Considering that any Spring application contains multiple beans and the beans have dependencies on each other, the container creates them in the right order according to their dependencies.

Note that separate parts of this lifecycle can be customized according to the specific needs of a particular application. Bean initialization and destruction are especially worth considering because programmers often customize them. Other parts of the lifecycle can be adjusted as well, however, it is required much less frequently in practice.

Customizing bean initialization and destruction

To get a bean into the ready state, it may be required to do some initialization after dependency injection. The Spring container does it automatically, but it also allows us to customize the initialization according to the needs of our application. For example, we can load some resources, read a file, connect to a database, and so on. At the same time, when a bean is no longer required in the application, some custom cleanup may be required before destroying the bean, such as closing some connections, cleaning files, and so on.

There are several ways to add these customizations to your code:

  • using special annotations (@PostConstruct, @PreDestroy, @Bean);
  • implementing some interfaces (InitializingBean, DisposableBean);
  • using an XML bean definition file, which is an outdated way mostly used for legacy applications (we will skip it here).

No matter which way you choose, you need to have separate methods that conduct some custom bean initialization and destruction. These are the rules for such methods:

  • the methods can have any names and access modifiers;
  • the methods must not have any arguments, otherwise, an exception will be thrown.

To demonstrate different customization ways, we will use a simple example with a library of technical books that comprises an in-memory list of strings. It must be enough for now to give you the main idea.

Using annotations for customization

The simplest way to customize the initialization and destruction processes is to add the @PostConstruct and @PreDestroy annotations to the methods of a container-managed class.

Here is an example with the init and destroy methods annotated with @PostConstruct and @PreDestroy. If we run an application containing this component, Spring will call the annotated methods only once.

@Component
class TechLibrary {
    private final List<String> bookTitles = 
            Collections.synchronizedList(new ArrayList<>());

    @PostConstruct
    public void init() {
        bookTitles.add("Clean Code");
        bookTitles.add("The Art of Computer Programming");
        bookTitles.add("Introduction to Algorithms");
        System.out.println("The library has been initialized: " + bookTitles);
    }

    @PreDestroy
    public void destroy() {
        bookTitles.clear();
        System.out.println("The library has been cleaned: " + bookTitles);
    }
}

Here is the output produced by the TechLibrary class:

The library has been initialized: [Clean Code, The Art of Computer Programming, Introduction to Algorithms]

2022-04-22 12:08:06.515  INFO Started HsSpringApplication in 0.382 seconds (JVM running for 5.698)

The library has been cleaned: []

Process finished with exit code 0

If you work with a @Bean-annotated method instead of a component, you can achieve the same result. What you need to do is specify the names of the init and destroy methods as the values of initMethod and destroyMethod properties of the @Bean annotation.

The following code is equivalent to the one above, but this new version uses @Bean instead of @Component.

@Configuration
class Config {

    @Bean(initMethod = "init", destroyMethod = "destroy")
    public TechLibrary library() {
        return new TechLibrary();
    }
}

class TechLibrary {
    private final List<String> bookTitles = 
            Collections.synchronizedList(new ArrayList<>());

    public void init() {
        bookTitles.add("Clean Code");
        bookTitles.add("The Art of Computer Programming");
        bookTitles.add("Introduction to Algorithms");
        System.out.println("The library has been initialized: " + bookTitles);
    }

    public void destroy() {
        bookTitles.clear();
        System.out.println("The library has been cleaned: " + bookTitles);
    }
}

If you run this code, the result will be exactly the same as before.

As an alternative, we can still add the @PostConstruct and @PreDestroy annotations to the init and destroy methods instead of specifying the attributes of the @Bean annotation, even if we use a @Bean-annotated method.

Using interfaces for customization

Another way to customize the initialization and destruction processes is to implement the InitializingBean and DisposableBean interfaces, and then override their afterPropertiesSet and destroy methods correspondingly.

Here is the modified version of the previous example:

@Component
class TechLibrary implements InitializingBean, DisposableBean {
    private final List<String> bookTitles = 
            Collections.synchronizedList(new ArrayList<>());

    @Override
    public void afterPropertiesSet() throws Exception {
        bookTitles.add("Clean Code");
        bookTitles.add("The Art of Computer Programming");
        bookTitles.add("Introduction to Algorithms");
        System.out.println("The library has been initialized: " + bookTitles);
    }

    @Override
    public void destroy() {
        bookTitles.clear();
        System.out.println("The library has been cleaned: " + bookTitles);
    }
}

After running this code, the result will be exactly the same as it was with the use of annotations.

Post-processors for beans

Now that we’ve considered a few ways of initializing beans to make them ready for use, let’s look at another way that is even more straightforward. You can initialize beans using the BeanPostProcessor interface that allows for custom modification of bean instances. Using the post-processor, it is possible to run any custom operation before or right after a bean is initialized and even return a modified bean.

To start using post-processors, your class should:

  1. Implement the BeanPostProcessor interface;
  2. Override the postProcessBeforeInitialization and/or postProcessAfterInitialization methods.

Here is an example of a post-processor that only prints the name of the processed bean.

@Component
class PostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessBeforeInitialization(
            Object bean, String beanName) throws BeansException {

        System.out.println("Before initialization: " + beanName);

        return BeanPostProcessor.super
                .postProcessBeforeInitialization(bean, beanName);
    }

    @Override
    public Object postProcessAfterInitialization(
            Object bean, String beanName) throws BeansException {

        System.out.println("After initialization: " + beanName);

        return BeanPostProcessor.super
                .postProcessAfterInitialization(bean, beanName);
    }
}

If you run this code, you will see a long list of beans in your application at different stages of their lifecycle.

It is important that BeanPostProcessor be executed for each bean defined in the Spring context, including the beans created by the framework. At the same time, it is possible to keep some of the beans from being modified by writing a particular condition.

Post-processors is a much more advanced concept and for now, it is enough to get only the basic idea of it. Unlike @PreDestroy, @PostConstruct and other approaches for custom initialization, post-processors are used for processing multiple beans in the same way. The processors aren’t usually tied to business logic and provide some infrastructure code that modifies or wraps the beans.

Conclusion

In this topic, you have learned about the main stages of bean lifecycle managed by the Spring Container. You have also seen several ways to customize the initialization and destruction parts of the lifecycle using the @PostConstruct and @PreDestroy annotations, as well as the InitializingBean and DisposableBean interfaces. And finally, we introduced you to the concept of a post-processor that allows for custom modification of new bean instances.

Leave a Reply

Your email address will not be published.