Spring IoC Container & How to Build an IoC Container in a Simple Way - 1
Greetings, everyone. Today, we will discuss IoC containers and how to build one quickly. Let’s refresh our coffees and get started ☕ 📰😊.
What is the IoC Container?
The IoC container is a core concept in the framework that is responsible for managing the lifecycle of instances (beans) and their dependencies. It is used with Dependency Injection (DI) to achieve this.
In a traditional methodology, the flow control is determined by the programmers that use some patterns such as Factory MethodPattern, Singleton Pattern, Abstract Factory pattern, etc, which are responsible for creating and managing objects and their dependencies. However, this comes at a cost, such as increasing complexity and making a massive mistake in the code that developers might have written. Therefore, an IoC container should be the optimal solution to manage dependencies during the application lifecycle through a framework such as Spring Framework.
The IoC Container Achieves this through configuration metadata, which can be provided in XML, Annotations or Java Code. The container reads this metadata or scans the annotations using reflection to be aware of how to create and wire the beans, which allows for loose coupling between components and easier unit testing, as dependencies can be easily mocked or stubbed.
Design Patterns which are working under the hood?
Factory Method Pattern: IoC Containers often use factory patterns to create instances of beans based on their configuration.
Singleton Pattern: IoC containers can manage beans as singletons, ensuring that only one instance of a bean is created and shared throughout the application context.
Prototype Pattern: IoC containers can create new instances of beans for each request to be made by the client, similar to the prototype pattern
Proxy Pattern: IoC containers may use proxy objects to manage the lifecycle of beans or to provide additional features before or after the actual object, such as lazy loading, transaction management, etc.
Decorator Pattern: IoC Containers can wrap beans with decorators to add additional functionality or behaviour to the beans.
Observer Pattern: IoC containers often use the observer pattern to notify beans of changes or events in the application context.
What is the Dependency Injection
Dependency injection is a design pattern used to implement inversion of control (IoC) in software applications, which is a way to achieve loose coupling between components and their dependencies, making code more modular, testable and maintainable.
Constructor Injection
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
public static void main(String[] args) {
UserService userService = new UserService(new UserRepositoryImpl())
}
Setter Injection
public class UserService {
private UserRepository userRepository;
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void saveUser(User user) {
userRepository.save(user);
}
}
public static void main(String[] args) {
UserService userService = new UserService();
userService.setUserRepository(new UserRepositoryImpl());
User user = new User("Alice");
userService.saveUser(user);
}
We already explained what the IoC (inversion of control) Container is and how it works in Spring Framework. Now, I am going to show a quite simple code to be aware of the IoC container.
This code is not a true concept; it is just an example to be aware of it.
@Retention(RetentionPolicy.RUNTIME)
public @interface Inject {
Prototype value() default Prototype.SINGLETON;
String name() default "DEFAULT";
}
public class InstanceFactory {
private static final HashMap<String, Object> singleInstances = new HashMap<>();
private void load() {
Method[] methods = ServiceRegister.class.getDeclaredMethods();
Arrays.stream(methods).filter(method -> method.getName().startsWith("register")).forEach(method -> {
try {
method.setAccessible(true);
Object instance = method.invoke(new ServiceRegister());
Inject inject = method.getReturnType().getAnnotation(Inject.class);
if (Objects.isNull(inject)) {
throw new RuntimeException("Inject annotation is not found, instance cannot be created by IoC");
}
if (inject.value() == Prototype.SINGLETON) {
singleInstances.put(inject.name(), instance);
}
} catch (Exception e) {
e.printStackTrace();
}
});
}
public static <T> T getInstance(Class<T> clazz) {
try {
Inject inject = clazz.getAnnotation(Inject.class);
if (Objects.isNull(inject)) {
throw new RuntimeException("Inject annotation is not found, instance cannot be created by IoC");
}
if (inject.value() == Prototype.SINGLETON) {
return (T) singleInstances.get(inject.name());
}
return getNewInstance(clazz);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private static <T> T getNewInstance(Class<T> clazz) throws InvocationTargetException, IllegalAccessException {
Method[] methods = ServiceRegister.class.getDeclaredMethods();
Method method = Arrays.stream(methods).filter(m -> m.getReturnType().equals(clazz)).findAny().orElseThrow(
() -> new RuntimeException("Method is not found in ServiceRegister")
);
method.setAccessible(true);
return (T) method.invoke(new ServiceRegister());
}
}
public final class ServiceRegister {
private UserService registerUserService() {
return new UserServiceImpl();
}
private AuthService registerAuthService() {
return new AuthServiceImpl();
}
}
public enum Prototype {
SINGLETON,
PROTOTYPE
}
public class Test {
public static void main(String[] args) {
try {
Method method = InstanceFactory.class.getDeclaredMethod("load");
method.setAccessible(true);
method.invoke(new InstanceFactory());
} catch (Exception e) {
e.printStackTrace();
}
UserService userService1 = InstanceFactory.getInstance(UserService.class);
userService1.save();
UserService userService2 = InstanceFactory.getInstance(UserService.class);
userService2.save();
AuthService authService1 = InstanceFactory.getInstance(AuthService.class);
authService1.login();
AuthService authService2 = InstanceFactory.getInstance(AuthService.class);
authService2.login();
System.out.println(userService1.hashCode());
System.out.println(userService2.hashCode());
System.out.println("-------------------------------------------------");
System.out.println(authService1.hashCode());
System.out.println(authService2.hashCode());
}
}
The code implements a basic IoC (Inversion of Control) container in Java using annotations and reflection. It defines an `Inject` annotation to mark classes managed by the container and uses a `ServiceRegister` class to create instances of services. The `InstanceFactory` class manages the lifecycle of these instances, allowing them to be retrieved based on their type and scope. The `Test` class demonstrates how to use the IoC container to retrieve and use instances of services. Overall, this code provides a basic implementation of an IoC container using Java annotations and reflection. However, it is a simplified version and may not cover all the features and edge cases of a production-ready IoC container.
Spring IoC Container
The org.springframework.beans
and org.springframework.context
packages are the basis for Spring Framework's IoC container. The BeanFactory
interface provides an advanced configuration mechanism capable of managing any type of object. ApplicationContext
is a sub-interface of BeanFactory.
It adds easier integration with Spring's AOP features; message resource handling (for use in internationalization), event publication; and application-layer specific contexts such as the WebApplicationContext
for use in web applications.
In short, the BeanFactory
provides the configuration framework and basic functionality, and the ApplicationContext
adds more enterprise-specific functionality. [1]
@Component and @Service Annotation
@Component and @Service annotations must be used on the class, which will be used by the IoC Container to achieve Dependency Injection by injecting instances of the class
@Service annotation is already declared with @Component annotation; therefore, the purpose of @Service annotation is specific to the domain, so @Service annotation should be preferred with some business logic and non-generic classes.
@Component should be preferred for generic structures such as Message Service; yeah, you might say, “But the name of it consists of Service. Why shouldn’t I declare it as service?”. In my opinion, in such cases, it completely depends on the approach, so it is up to you!
Message Service
@Component
@RequiredArgsConstructor // lombok, same with constructor injection: @Autowired
public class MessageService {
public void sendMessage(String message) {
System.out.println("Sending message: " + message);
}
}
Notification Service
@Service
@RequiredArgsConstructor // lombok, same with constructor injection: @Autowired
public class NotificationService {
private final MessageService messageService;
public void sendNotification(String notification) {
messageService.sendMessage("Notification: " + notification);
}
}
Injection Types
There are three types of injections in Spring Boot: constructor injection, setter injection and field injection. Most of the time, developers prefer constructor injection.
Constructor Injection
Constructor injection in Spring Boot is done by defining a constructor in your class and annotating it with @Autowired. When Spring initializes your bean, it will look for a constructor annotated with @Autowired and use it to inject the dependencies.
@Service
public class NotificationService {
private final MessageService messageService;
@Autowired
public NotificationService(MessageService messageService) {
this.messageService = messageService;
}
public void sendNotification(String notification) {
messageService.sendMessage("Notification: " + notification);
}
}
Setter Injection
Setter injection in Spring Boot is similar to constructor injection, but instead of using a constructor, you define setter methods for your dependencies and annotate them with.
@Service
public class UserService {
private UserRepository userRepository;
@Autowired
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
Field Injection
Field injection in Spring Boot involves directly injecting dependencies into your class's fields using the @Autowired annotation. While convenient, this approach is discouraged in favour of constructor or setter injection, as it makes testing more difficult and can lead to issues with circular dependencies.
@Service
public class NotificationService {
@Autowired
private final MessageService messageService;
public void sendNotification(String notification) {
messageService.sendMessage("Notification: " + notification);
}
}
REFERENCES
https://docs.spring.io/spring-framework/docs/3.2.x/spring-framework-reference/html/beans.html [1]