Lesson 5
Bean Scopes and Lifecycle
Introduction

Welcome to this lesson on Bean Scopes and Lifecycle! So far, we've covered what Spring and Spring Boot are, delved into the project structure, and understood the importance of files like application.properties. We've also explored concepts such as Inversion of Control (IoC) and Dependency Injection (DI). We learned how to create simple beans without dependencies and, in the previous lesson, learned to create beans with dependencies. This lesson will explore the concepts of bean scopes and bean lifecycle, which are crucial for managing the state and lifecycle of beans in a Spring application.

Understanding Bean Scopes

In Spring, all beans are by default singletons, meaning only one instance of each bean exists in the application context. This single instance is reused wherever the bean is injected, making it efficient for stateless beans that can be used multiple times across the application without side effects. By ensuring that there is only one instance, Spring makes it possible to wire beans by type, as there won't be any ambiguity.

However, not all beans should be singletons. In scenarios where a class maintains some state and hence isn't safe for reuse, a different scope might be needed. Here are the main bean scopes in Spring:

  • Singleton: One instance of the bean is created for the entire application.
  • Prototype: A new instance is created each time the bean is requested.
  • Session: In a web application, one instance is created for each session.
  • Request: In a web application, one instance is created for each HTTP request.

You can declare a different scope using the @Scope annotation alongside the @Component or @Bean annotation.

Declaring a Prototype Bean with @Component

To define a prototype-scoped bean using the @Component annotation, you need to utilize the @Scope annotation. Here's how you can do it:

Kotlin
1package com.example 2 3import org.springframework.context.annotation.Scope 4import org.springframework.stereotype.Component 5 6@Component 7@Scope("prototype") 8class PrototypeComponentBean { 9 init { 10 println("PrototypeComponentBean instance created.") 11 } 12}

In this example, each time a PrototypeComponentBean is requested from the application context, a new instance is created. This approach is particularly useful for stateful beans that should not share the state with other instances.

Declaring a Prototype Bean with @Bean

Another way to create a prototype-scoped bean is by using the @Bean annotation within a configuration class. This method allows for more customization during bean creation. Here's an example:

Kotlin
1package com.example 2 3import org.springframework.context.annotation.Bean 4import org.springframework.context.annotation.Configuration 5import org.springframework.context.annotation.Scope 6 7@Configuration 8class BeanConfig { 9 10 @Bean 11 @Scope("prototype") 12 fun prototypeBean(): PrototypeBean { 13 return PrototypeBean() 14 } 15}

In this scenario, the prototypeBean method is annotated with @Scope("prototype"), ensuring that a new instance of PrototypeBean is created each time this method is invoked. This approach is beneficial when you have more complex creation logic or configuration needs for your beans.

By understanding both methods, you can choose the one that best fits your specific requirements, whether it's the simplicity of @Component or the flexibility of @Bean.

Bean Lifecycle at a Glance

To simplify understanding, let's compare the lifecycle of a Spring bean to the stages of a person's life:

  • Bean Creation (Birth): Think of this as when a person is born. In Spring, a bean is created when the application starts up. Spring looks at the configuration (like @Component or @Bean) and creates an instance of the bean. This is when the bean is "born."
  • Dependency Injection (Growing Up): As a person grows up, they learn and acquire things they need to live, like food, clothes, and education. After the bean is created, Spring injects all the necessary dependencies into it. These are the other objects (beans) that the bean needs to function properly. This process is like the bean "growing up" and getting what it needs to work.
  • Initialization (Starting Life): Once a person is grown up, they start their adult life, doing their job or daily activities. After dependency injection, the bean goes through an initialization phase where it gets ready to do its work. If there’s any setup needed (like opening a file or establishing a database connection), this is when it happens. In Spring, you can do this setup using methods like @PostConstruct.
  • Bean Usage (Living Life): Now the person is living their life, doing their job, helping others, etc. The bean is now ready to be used by the application. It performs its tasks, interacts with other beans, and does whatever it’s designed to do as part of the application.
  • Destruction (End of Life): Just like a person’s life comes to an end, a bean’s life also ends when it’s no longer needed. When the application shuts down, Spring will clean up the beans. If the bean needs to do any final tasks (like closing a file or releasing resources), it happens in this phase. You can use methods like @PreDestroy for this cleanup. This is the bean's "end of life."
Bean Lifecycle in Code

Let's look at how to implement the bean lifecycle stages in code:

Kotlin
1package com.example 2 3import org.springframework.beans.factory.DisposableBean 4import org.springframework.beans.factory.InitializingBean 5import org.springframework.stereotype.Component 6import jakarta.annotation.PostConstruct 7import jakarta.annotation.PreDestroy 8 9@Component 10class MyBean : InitializingBean, DisposableBean { 11 12 init { 13 println("Bean is being created (Constructor).") 14 } 15 16 @PostConstruct 17 fun postConstruct() { 18 println("Bean is being initialized (@PostConstruct).") 19 } 20 21 override fun afterPropertiesSet() { 22 println("Bean is fully initialized (afterPropertiesSet from InitializingBean).") 23 } 24 25 fun performTask() { 26 println("Bean is being used (performTask method).") 27 } 28 29 @PreDestroy 30 fun preDestroy() { 31 println("Bean is about to be destroyed (@PreDestroy).") 32 } 33 34 override fun destroy() { 35 println("Bean is destroyed (destroy from DisposableBean).") 36 } 37}

In this code:

  • Birth: The bean is created using the constructor.
  • Growing Up: Dependencies are injected.
  • Starting Life: @PostConstruct and afterPropertiesSet initialize the bean.
  • Living Life: The performTask method simulates the bean's activity.
  • End of Life: @PreDestroy and destroy handle bean destruction.

In simple applications, you don't need to use all these lifecycle methods. However, understanding them gives you comprehensive control over bean behavior.

Summary

In this lesson, we explored bean scopes and lifecycle in Spring, understanding the default singleton scope, when to use different scopes, and how to manage the lifecycle stages of a bean. Next, you'll get hands-on practice to reinforce these concepts. Happy coding!

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.