Understanding Bean Scopes and Life Cycle in Spring
When working with Spring, you’ll come across different ways to manage the lifecycle of beans. Simply put, a bean is an object that Spring manages for you, and its scope controls how long that object exists and how many instances of it get created while your application is running. In other words, the bean scope is how we manage the life cycle of a bean. Understanding bean scopes is important because they affect how your application’s resources are used. The two most common scopes in Spring are Singleton and Prototype.
Singleton Scope: The Default in Spring
By default, Spring beans are singleton scoped. This means that Spring creates one instance of the bean for the entire application context. Whenever you request this bean, you get the same instance each time.
@Service
public class CommentService {
public void sendComment(Comment c) {
// Logic for sending a comment
}
}
In the above example, CommentService
is a singleton. Spring creates a single instance of this class and reuses it whenever it is needed.
Multiple Instances of Singleton Beans?
In Spring, the term “singleton” may lead to some confusion. It means there is one instance of a bean per unique bean name, not just one instance per application. This means you can create multiple beans of the same type as long as they have different names, and each name will have its own single instance.
public class MyService {
private String name;
public MyService(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
@Configuration
public class AppConfig {
@Bean(name = "beanA")
public MyService myServiceA() {
return new MyService("Service A");
}
@Bean(name = "beanB")
public MyService myServiceB() {
return new MyService("Service B");
}
}
@SpringBootApplication
public class MyApp {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(MyApp.class, args);
MyService serviceA = context.getBean("beanA", MyService.class); // Fetch beanA
MyService serviceB = context.getBean("beanB", MyService.class); // Fetch beanB
System.out.println(serviceA.getName()); // Output: Service A
System.out.println(serviceB.getName()); // Output: Service B
}
}
In this example, the AppConfig
class defines two beans, beanA
and beanB
, both of which are instances of the MyService
class. Even though both beans are of the same type, they have different names. When we fetch these beans in the MyApp
class, we can see that each bean maintains its own instance. This demonstrates that Spring treats each bean name as a unique reference, allowing us to work with multiple singleton instances effectively.
Thread Safety Concerns with Singleton Beans
One important thing to keep in mind is that singleton beans are shared between threads. If two or more threads try to access and modify the same singleton instance at the same time, you can run into race conditions, which lead to unexpected behavior.
@Service
public class CounterService {
private int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}
@SpringBootApplication
public class MyApp {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(MyApp.class, args);
CounterService counterService = context.getBean(CounterService.class);
// Simulate multiple threads incrementing the counter
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
counterService.increment();
}
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final Count: " + counterService.getCount()); // Output may vary
}
}
In this example, CounterService
is a singleton bean managed by Spring, designed to increment a counter. A typical computer has multiple cores, and each core can run a process. Processes are made up of threads, which are small units of work. When a core is idle, it can pick up a thread from a process to run simultaneously with other threads in the same process, like the MyApp
main program. Ideally, Thread 1 would increment the count to 1000, and then Thread 2 would continue from there, bringing the total to 2000. However, both threads try to update the count at the same time without synchronization, which can lead to interference and missed increments, resulting in a final count likely less than 2000.
To create a thread in Java, you can implement the Runnable
interface or extend the Thread
class. We define tasks for each thread and start their execution with start()
, allowing Thread 1 and Thread 2 to run concurrently. If multiple cores are available, the threads may run on different ones; otherwise, they may share a core, using context switching. The join()
method pauses the main program until both threads finish, demonstrating why singleton beans in a multi-threaded environment require careful handling to avoid race conditions.
Eager vs. Lazy Instantiation
Singleton is the default scope of a bean in Spring, meaning that by default, Spring eagerly creates all singleton beans when the application context starts. This is called eager instantiation.
@Service
public class EagerService {
// Created when the application context is initialized
public EagerService() {
System.out.println("EagerService instance created");
}
}
However, sometimes you might not want to create the bean until it’s actually needed. In that case, you can use lazy instantiation.
@Service
@Lazy
public class LazyService {
// Created only when it's accessed for the first time
public LazyService() {
System.out.println("LazyService instance created");
}
public void performAction() {
System.out.println("Action performed");
}
}
With lazy instantiation, Spring waits until you first request the bean before creating it. This can save resources, but it adds a little overhead because Spring has to check whether the instance exists.
@SpringBootApplication
public class MyApp {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(MyApp.class, args);
// LazyService is created only when it's accessed
LazyService lazyService = context.getBean(LazyService.class); // LazyService is instantiated here
lazyService.performAction();
// EagerService is created immediately when the application context is initialized
EagerService eagerService = context.getBean(EagerService.class);
}
}
This code results in the following output order: "EagerService instance created,"
followed by "LazyService instance created,"
and then "Action performed."
.
Prototype Scope: A New Instance Each Time
Unlike singleton beans, prototype-scoped beans are created new every time they are requested. This is helpful when you need a fresh instance for each operation.
@Service
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class UserSession {
// New instance every time it's requested
}
In this case, every time you ask Spring for a UserSession
bean, it creates a new instance.
Mixing Singleton and Prototype Beans
If you inject a prototype-scoped bean into a singleton bean, you might run into unexpected behavior. Since the singleton is created only once, the prototype bean will also be created only once at that time, even though you might expect a new prototype instance every time.
Problematic Scenario:
@Service
public class OrderService {
@Autowired
private InvoiceGenerator invoiceGenerator;
public void processOrder(Order order) {
invoiceGenerator.generateInvoice(order); // Reuses the same instance of InvoiceGenerator
}
}
In this example, InvoiceGenerator
is expected to be prototype-scoped, but since it is injected into a singleton OrderService
, the same instance is reused each time processOrder()
is called, which might not be what you want.
Recommended Approach:
To ensure that a new InvoiceGenerator instance is created every time it’s needed, retrieve it from the Spring context directly.
@Service
public class OrderService {
@Autowired
private ApplicationContext context;
public void processOrder(Order order) {
InvoiceGenerator invoiceGenerator = context.getBean(InvoiceGenerator.class); // New instance each time
invoiceGenerator.generateInvoice(order);
}
}
In this revised approach, every time processOrder()
is invoked, Spring creates a fresh InvoiceGenerator
instance.
Bean Lifecycle Overview
The lifecycle of a Spring bean begins when it’s created by the Spring container. Depending on its scope, the life cycle varies:
- Singleton: The bean is created once and exists for the entire application context. It’s initialized at startup and destroyed when the application context is closed.
- Prototype: A new instance is created every time the bean is requested. It’s initialized each time it’s called but is not managed by the Spring container after that; the container doesn’t handle its destruction.
Conclusion
Understanding how bean scopes work in Spring is essential to manage the lifecycle of your objects effectively. Singleton beans are great for shared resources, but be careful when dealing with multiple threads. Prototype beans are useful when you need fresh instances, but watch out for potential issues when injecting them into singletons.