Spring IoC Container & Features - 2
Qualifier, Primary, Lazy Initialization, Bean Scope, Bean Lifecycle
As you remember, we discussed considering IoC Container in the first article. Throughout this article, we will cover some annotations which are valuable for the development lifecycle. Let’s refresh our coffees and get started ☕ 📰😊.
@Qualifier Annotation
As a payment company, we have three different payment providers, such as Bitcoin, Masterpass, and Visa provider, which are the same type and have the same method due to their extended abstract class. As we are the developers, we must implement a practical solution to be used for a payment provider in an efficient way, like a plug-and-play; therefore, developers should utilise the benefits of Spring Ioc.
@Qualifer annotation is that basically provides which bean (class, instance) should be injected when the parent interface called and multiple beans (class) implemented in the same parent interface.
According to our example, we have three different providers which are implemented the PaymentProvider interface so that we can use @Qualifer annotation for which bean (concrete class) will be injected by Spring IoC.
PaymentProvider
public interface PaymentProvider {
void pay();
}
BitcoinPaymentProvider
@Service("@")
public class BitcoinPaymentProvider implements PaymentProvider {
@Override
public void pay() {
System.out.println("Paid with Bitcoin");
}
}
MasterpassPaymentProvider
@Service("masterpass")
public class MasterpassPaymentProvider implements PaymentProvider{
@Override
public void pay() {
System.out.println("Paid with Masterpass");
}
}
VisaPaymentProvider
@Service("visa")
public class VisaPaymentProvider implements PaymentProvider {
@Override
public void pay() {
System.out.println("Paid with Visa");
}
}
Usage in PaymentController
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/payment")
public class PaymentController {
private final PaymentProvider paymentProvider;
public PaymentController(@Qualifier("bitcoin") PaymentProvider paymentProvider) {
this.paymentProvider = paymentProvider;
}
@RequestMapping("/pay")
public void pay(){
paymentProvider.pay();
}
}
@Primay Annotation
As the name implies, @Primary annotation is declared on the class that is being given prioritisation to be created instance (bean) by Spring IOC. In the example above, a provider can be annotated as a Primary being created instance by Ioc Container when its parent abstract class is called. This prevents possible errors when @Qualifer annotation is forgotten by developers because the IoC container creates an instance which has already been annotated with @Pimary annotation.
Usage in MasterpassPaymentProvider
@Primary
@Service("masterpass")
public class MasterpassPaymentProvider implements PaymentProvider{
@Override
public void pay() {
System.out.println("Paid with Masterpass");
}
}
Usage in PaymentController
@RestController
@RequestMapping("/payment")
public class PaymentController {
private final PaymentProvider paymentProvider;
// As you can see, no need Qualifer when a class already declared as a Primary
public PaymentController(PaymentProvider paymentProvider) {
this.paymentProvider = paymentProvider;
}
@RequestMapping("/pay")
public void pay(){
paymentProvider.pay();
}
}
@Lazy Initialization
In Spring Ioc, lazy initialization is a concept where beans are created only when they are requested rather than when they are created eagerly during the application startup. @Lazy annotation reduces startup time in large applications which have many beans. Furthermore, it avoids circular dependencies and optimizes performance. We will cover what is a circular dependency.
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@Lazy
@Service
public class MusicService {
public MusicService() {
System.out.println("Music Service is created");
}
public void play() {
System.out.println("Music is playing");
}
}
Circular Dependency
In IoC containers, including in the context of Spring, IoC containers refer to situations where two or more beans depend on each other directly or indirectly, forming a circular reference. These circular references can lead to runtime errors or unexpected behaviour.
I will talk about it with an example. Think about it: you have a UserService class, which is responsible for user operation, and you have another UserGroup service, which is responsible for managing user groups. You have an IAM service. When you want to create a user, you would like to add this user to a group, so obviously, you have to call the UserGroupService class in the UserService class; however, when you want to manage users in the group, you must do it in the UserGroupService. Therefore, you must also call UserService in the UserGroupService class; that’s why you will face a Circular Dependency error. Basically, these two beans are calling each other. Look at the picture below.
UserGroupService
import org.springframework.stereotype.Service;
@Service
public class UseGroupService {
private final UserService userService;
public UseGroupService(UserService userService) {
this.userService = userService;
}
public void save() {
userService.getUserByIds();
// logic
System.out.println("Saved");
}
public Object getById() {
return null;
}
}
UserService
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
private final UseGroupService useGroupService;
public UserService(UseGroupService useGroupService) {
this.useGroupService = useGroupService;
}
public void save() {
useGroupService.getById();
// logic
System.out.println("Saved");
}
public List getUserByIds() {
return null;
}
}
Bean Scope
Bean scope provides how long a bean remains in memory and how the IoC container handles its instance creation. This feature is significant for understanding the concept of IOC Containers. Basically, bean scope is the lifecycle and visibility of an instance.
Singelton Scope
This is the default scope in Spring. When a bean is declared as a singleton, Ioc Container creates and manages only one instance of the bean. The same instance is returned every time the bean is requested. Singleton scope is not thread-safe so that it can lead to some concurrency problems such as lock, race conditions, etc. However, it enhances the performance of the application.
Protoype Scope
If a bean is declared as a prototype, the IoC container creates a new instance of this bean each time when it is requested. The prototype scope is suitable for stateful beans, but we will cover which scope is suitable in which cases at the end of this writing.
@Service
@Scope("prototype")
public class UserService {
}
Request Scope
If a bean is declared as a Request scope, the IoC container creates a new instance once per HTTP request. This scope is typically used in web applications where each HTTP request requires a separate instance of certain beans.
@Service
@RequestScope
public class UserService {
}
Session Scope
if a bean is declared as a Session scope, the IoC container creates a new instance once per HTTP request. This scope is useful for maintaining user-specific data throughout the duration of a user session. It is especially useful for IAM services, which are keeping sessions.
@Service
@SessionScope
public class UserService {
}
Application Scope
if a bean is declared as an Application scope, the IoC container creates an instance once per servlet context. This scope is suitable for beans that need to be shared across the entire application.
@Service
@ApplicationScope
public class UserService {
}
WebSocket Scope
This scope is similar to the session scope; however, it is specific to WebSocket sessions. If a bean is declared as a WebSocket scope, the IoC container creates an instance once per WebSocket session.
@Service
@Scope(scopeName = "websocket")
public class UserService {
}
Bean Lifecycle
The bean lifecycle is managed by the spring container. The lifecycle basically means how objects are created, used and destroyed. Spring container consists of two different custom methods, which provide
customization for developers, such as PostContruct and PreDestrory.
@PostConstruct
Post Construct provides the implementation of some business logic, having a new instance created. For instance, to perform initialization of some tasks such as loading configuration, setting up connections, etc.
@PreDestroy
Pre Destroy provides the implementation of some business logic before the instance is destroyed by IoC Container. For instance, to perform cleanup tasks such as closing connections, releasing resources, etc.
package com.beratyesbek.ioccontainer.lifecycle;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class UserService {
public void save() {
log.info("User is saved");
}
@PostConstruct
public void postConstruct() {
// Perform initialization tasks such as loading configuration, setting up connections, etc.
log.info("Post Construct");
}
@PreDestroy
public void preDestroy() {
// Perform cleanup tasks such as closing connections, releasing resources, etc.
log.info("Pre Destroy");
}
}
Output
c.b.ioccontainer.lifecycle.UserService : Post Construct
c.b.ioccontainer.lifecycle.UserService : User is saved
[ionShutdownHook] c.b.ioccontainer.lifecycle.UserService : Pre Destroy