Spring: Let’s build our own IoC Container
Spring: Let’s build our own IoC Container
This blog is heavily inspired by a blog post from Dat Bui on Viblo (Here is the link of the blog post - It’s Vietnamese: link ) In the original post, Dat shows us a very simple implementation of IoC Container, he has only implemented dependency injection using field injection only, for constructor-based, setter-based, he left it for the reader to challenge themselves against leftovers. I’m kinda interested in this one and want to continue further. You can find the original implementation here
Before diving into the main part of the blog, I want to provide some background information. The original blog post by Dat Bui was written in Vietnamese. I will cover all the essential points that we need to understand beforehand. Each of these topics could warrant its own post, and there are many articles available that discuss these concepts in detail.
Please note that the definitions provided here are based on my perspective and understanding. They might not be entirely accurate, but I believe they are correct as of now. If there are any inaccuracies, I will correct them later. Alternatively, you can let me know by leaving a comment at the end of this blog.
- The Dependency Inversion Principle is one of the SOLID principles. It states that:
- High-level modules should not depend on low-level modules. Both should depend on abstractions (e.g., interfaces).
- Abstractions should not depend on details. Details should depend on abstractions.
Purpose:
- DIP aims to reduce the coupling between high-level and low-level components by introducing abstractions (interfaces or abstract classes) that both components depend on. This makes the system more modular and easier to change or extend.
Example:
// High-level module
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
public List<Order> getOrders() {
orderRepository.getOrders();
}
}
// Abstraction
public interface OrderRepository {
List<Order> getOrders();
}
// Low-level module
public class PostgresOrderRepository implements OrderRepository {
@Override
public List<Order> getOrders() {
// Logic to get order
return List.of();
}
}
public class Main {
public static void main(String[] args) {
OrderRepository postgresOrderRepository = new PostgresOrderRepository();
OrderService orderService = new OrderService(postgresOrderRepository);
List<Order> orders = orderService.getOrders();
}
}// High-level module
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
public List<Order> getOrders() {
orderRepository.getOrders();
}
}
// Abstraction
public interface OrderRepository {
List<Order> getOrders();
}
// Low-level module
public class PostgresOrderRepository implements OrderRepository {
@Override
public List<Order> getOrders() {
// Logic to get order
return List.of();
}
}
public class Main {
public static void main(String[] args) {
OrderRepository postgresOrderRepository = new PostgresOrderRepository();
OrderService orderService = new OrderService(postgresOrderRepository);
List<Order> orders = orderService.getOrders();
}
}In this example:
- OrderService (high-level module) depends on the OrderRepository interface (abstraction).
- PostgresOrderRepository (low-level module) implements the OrderRepository interface.
- This decoupling allows for easy substitution of different payment processors without changing the OrderService code.
Why does it matter?
- It’s better for testing, since you can pass a mock version of OrderRepository into OrderService
- You’re free to change the concrete implementation of the interface without affecting the high-level module (imagine if you hardcoded the PostgresOrderRepository inside the OrderService, if in the future you want to change to MySqlOrderRepository, you have to update all the occurrences of PostgresOrderRepository in OrderSerivce, even though the OrderSerivce does not change any logic at all!)
Definition:
- Inversion of Control is a design principle where the control of object creation, configuration, and lifecycle management is inverted from the application code to a container or framework. In other words, the framework or container manages the dependencies and flow of control in the application.
- IoC provides a mechanism to achieve DIP by shifting the responsibility of managing dependencies to a container or framework.
Purpose:
- IoC aims to decouple the application code from the creation and management of its dependencies, making the code more modular, testable, and maintainable.
Implementation:
- IoC can be implemented through several patterns, such as:
- Dependency Injection (DI): Dependencies are injected into objects, typically via constructors, setters, or fields.
- Service Locator: A central registry provides dependencies to objects upon request.
Example with Spring IoC:
// High-level module
public class OrderService {
private final OrderRepository orderRepository;
// Constructor-based dependency injection
@Autowired
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
public List<Order> getOrders() {
orderRepository.getOrders();
}
}
// Abstraction
public interface OrderRepository {
List<Order> getOrders();
}
// Low-level module
@Component
public class PostgresOrderRepository implements OrderRepository {
@Override
public List<Order> getOrders() {
// detail logic to get orders
return List.of();
}
}
// Main class
public class Main {
public static void main(String[] args) {
// IoC Container is a container provided by the framework, you think of it as a black box for now
IoCContainer ioc = new IoCContainer();
OrderService orderService = ioc.getBean(OrderService.class);
List<Order> orders = orderService.getOrders();
}
}// High-level module
public class OrderService {
private final OrderRepository orderRepository;
// Constructor-based dependency injection
@Autowired
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
public List<Order> getOrders() {
orderRepository.getOrders();
}
}
// Abstraction
public interface OrderRepository {
List<Order> getOrders();
}
// Low-level module
@Component
public class PostgresOrderRepository implements OrderRepository {
@Override
public List<Order> getOrders() {
// detail logic to get orders
return List.of();
}
}
// Main class
public class Main {
public static void main(String[] args) {
// IoC Container is a container provided by the framework, you think of it as a black box for now
IoCContainer ioc = new IoCContainer();
OrderService orderService = ioc.getBean(OrderService.class);
List<Order> orders = orderService.getOrders();
}
}So in here, we have an IoC Container provided the framework which is responsible for initializing all the components and auto injects the needed dependencies of those components. All we need is just get the component from the IoC Container and use the component, we do not need to care when and how the components are created.
Spring is the most popular framework when it comes to modern web development with Java. The Spring IoC Container is one of the core component of Spring Framework which implements Inversion Of Control. Just like what I mentioned in previous section, the Spring IoC Container is responsible for:
- Initializing objects (as known as Bean), no need to use the new keyword
- Auto inject dependencies for the object
In order to give a hint about what’s the dependency of object, the annotation @Autowired is usually used. We add it to the field to indicate that field is a dependency (field-based), or use along with the constructor (constructor-based) , to indicate that the arguments passed to that constructor are dependencies, or we can use with setter method (setter-based) to achieve the same behavior as constructor-based.
Now that we have covered the background information, let’s delve into the blog by Dat Bui, where he attempted to implement a simple version of the Spring IoC Container. He successfully implemented the initialization of beans and field-based injection, but left constructor-based and setter-based injection as exercises for the reader. Additionally, he did not address circular dependency detection.
In this post, I will cover two additional topics:
- Implementing constructor-based and setter-based injection
- Bean creation order and circular dependency detection
Please note that I haven’t reviewed the implementation of the Spring IoC Container.
When considering how the IoC Container should resolve and inject dependencies, as well as detect circular dependencies, it seems related to graph theory. Imagine each bean as a vertex in a directed graph, with dependency relationships represented as edges. We can apply graph algorithms to address these challenges.
Here are the main challenges:
- Building the Graph: We need to construct a graph where each bean is a vertex and each dependency is an edge.
- Initialization Order: We must determine an order for initializing beans such that all dependencies are resolved before a bean is created. This is related to the famous graph theory problem called Topological Sorting.
- Detecting Circular Dependencies: Detecting circular dependencies and identifying all circular paths is crucial. While topological sorting can help detect the presence of circular dependencies, it does not identify the circular paths. To address this, we can use backtracking. After applying topological sorting, if circular paths exist, some beans will remain uncreated. We can then traverse these beans using backtracking to find all circular dependency paths.
I hope this serves as an example of how algorithms and data structures are applied in real-world software development. There might be simpler or more efficient ways to build this (perhaps the team that built Spring used a different approach), but all roads lead to Rome. I am doing this for fun and to challenge myself a bit.
Before getting our hands dirty, we have to clarify some points first:
-
How the bean is defined? In this version, we create an annotation called @Component. All class annotated with this annotation is considered as a Bean and will be managed by our IoC Container
-
How the beans are injected?
We also create another annotation called @Autowired, all the field, constructor and setter which is annotated by this annotation will be taken into consideration for dependency injection.
I want to make this as simple as possible.
In the real world, the way Spring IoC Container resolves the dependency injection is way more complicated. For field-based injection and setter-based injection, Spring IoC Container relies on the @Autowired to know which field/method will be used. But for constructor-based injection, it’s not that easy. With the constructor-based injection, we don’t need to use @Autowired, in case a class has multiple constructors, Spring will greedy choose the constructor with largest number of parameter with some extra logic to determine which constructor should be used.
In this version, we only rely on @Autowired annotation.
Okay, it’s enough for chit-chat, let’s go
Please note that the snippet codes might be messy, I’ll provide the link to the repository with the full code, so you can go and check it along reading from now on.
First, let’s define needed annotations. We have 3 annotations
The first one is @Autowired
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({
ElementType.CONSTRUCTOR,
ElementType.METHOD,
ElementType.PARAMETER,
ElementType.FIELD,
ElementType.ANNOTATION_TYPE
})
@Retention(RetentionPolicy.RUNTIME)
public @interface Autowired {
}import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({
ElementType.CONSTRUCTOR,
ElementType.METHOD,
ElementType.PARAMETER,
ElementType.FIELD,
ElementType.ANNOTATION_TYPE
})
@Retention(RetentionPolicy.RUNTIME)
public @interface Autowired {
}The second one is @Component
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Component {
}import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Component {
}And the final one is @PostConstruct
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Target(METHOD)
@Retention(RUNTIME)
public @interface PostConstruct {
}import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Target(METHOD)
@Retention(RUNTIME)
public @interface PostConstruct {
}First, we need to get the list of beans the IoC Container needs to manage. We loop through all class declared in the scan package, and check if any class annotated with @Component annotation → It’s the bean we need to manage
Here is the snippet code
final Reflections reflections = new Reflections(scanPackage);
final Set<Class<?>> classes = reflections.getTypesAnnotatedWith(Component.class);
this.beanNames = classes.stream().map(Class::getName).toList();final Reflections reflections = new Reflections(scanPackage);
final Set<Class<?>> classes = reflections.getTypesAnnotatedWith(Component.class);
this.beanNames = classes.stream().map(Class::getName).toList();Once we get the list of beans, we need to get the list of their dependencies as well.
We have 3 ways to inject dependency into bean: constructor-based, setter-based and field-based.
I’ll create an interface for all of them
public interface DependencyGetter {
List<String> get(final Class<?> clazz, List<String> beanNames);
}public interface DependencyGetter {
List<String> get(final Class<?> clazz, List<String> beanNames);
}And create 3 implementations of this interface
FieldBasedDependencyGetter
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
public class FieldBasedDependencyGetter implements DependencyGetter {
@Override
public List<String> get(Class<?> clazz, List<String> beanNames) {
final Field[] fields = clazz.getDeclaredFields();
return Arrays.stream(fields)
.filter(
field -> Arrays.stream(field.getDeclaredAnnotations())
.anyMatch(a -> a.annotationType() == Autowired.class)
)
.map(field -> field.getType().getName())
.filter(beanNames::contains)
.toList();
}
}import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
public class FieldBasedDependencyGetter implements DependencyGetter {
@Override
public List<String> get(Class<?> clazz, List<String> beanNames) {
final Field[] fields = clazz.getDeclaredFields();
return Arrays.stream(fields)
.filter(
field -> Arrays.stream(field.getDeclaredAnnotations())
.anyMatch(a -> a.annotationType() == Autowired.class)
)
.map(field -> field.getType().getName())
.filter(beanNames::contains)
.toList();
}
}SetterBasedDependencyGetter
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
public class SetterBasedDependencyGetter implements DependencyGetter {
@Override
public List<String> get(Class<?> clazz, List<String> beanNames) {
final Method[] methods = clazz.getDeclaredMethods();
return Arrays.stream(methods)
.filter(
method -> method.getName().startsWith("set")
&& method.getParameterTypes().length == 1
&& Arrays.stream(method.getDeclaredAnnotations())
.anyMatch(a -> a.annotationType() == Autowired.class)
)
.map(
method -> {
var parameters = method.getParameters();
return Arrays.stream(parameters)
.map(Parameter::getName)
.filter(beanNames::contains)
.toList();
}
)
.flatMap(Collection::stream)
.toList();
}
}import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
public class SetterBasedDependencyGetter implements DependencyGetter {
@Override
public List<String> get(Class<?> clazz, List<String> beanNames) {
final Method[] methods = clazz.getDeclaredMethods();
return Arrays.stream(methods)
.filter(
method -> method.getName().startsWith("set")
&& method.getParameterTypes().length == 1
&& Arrays.stream(method.getDeclaredAnnotations())
.anyMatch(a -> a.annotationType() == Autowired.class)
)
.map(
method -> {
var parameters = method.getParameters();
return Arrays.stream(parameters)
.map(Parameter::getName)
.filter(beanNames::contains)
.toList();
}
)
.flatMap(Collection::stream)
.toList();
}
}ConstructorBasedDependencyGetter
import java.lang.reflect.Constructor;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class ConstructorBasedDependencyGetter implements DependencyGetter {
@Override
public List<String> get(Class<?> clazz, List<String> beanNames) {
for (Constructor<?> constructor : clazz.getConstructors()) {
if (constructor.isAnnotationPresent(Autowired.class)) {
return Arrays.stream(constructor.getParameters())
.map(parameter -> parameter.getType().getName())
.filter(beanNames::contains)
.toList();
}
}
return Collections.emptyList();
}
}import java.lang.reflect.Constructor;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class ConstructorBasedDependencyGetter implements DependencyGetter {
@Override
public List<String> get(Class<?> clazz, List<String> beanNames) {
for (Constructor<?> constructor : clazz.getConstructors()) {
if (constructor.isAnnotationPresent(Autowired.class)) {
return Arrays.stream(constructor.getParameters())
.map(parameter -> parameter.getType().getName())
.filter(beanNames::contains)
.toList();
}
}
return Collections.emptyList();
}
}To get the list of dependencies, we get the dependency list from all above dependency based
private final List<DependencyGetter> dependencyGetters = List.of(
new ConstructorBasedDependencyGetter(),
new FieldBasedDependencyGetter(),
new SetterBasedDependencyGetter()
);
private List<String> getDependencies(final Class<?> clazz) {
final List<String> dependencies = new ArrayList<>();
dependencyGetters.forEach(dependencyGetter -> dependencies.addAll(dependencyGetter.get(clazz, beanNames)));
return dependencies;
}private final List<DependencyGetter> dependencyGetters = List.of(
new ConstructorBasedDependencyGetter(),
new FieldBasedDependencyGetter(),
new SetterBasedDependencyGetter()
);
private List<String> getDependencies(final Class<?> clazz) {
final List<String> dependencies = new ArrayList<>();
dependencyGetters.forEach(dependencyGetter -> dependencies.addAll(dependencyGetter.get(clazz, beanNames)));
return dependencies;
}Now we got the list of beans to be managed and their dependencies. Now it’s time we decide the order of the creation for each bean.
If beanA depends on beanB, we must ensure that the beanB is created before the creation of beanA. As discussed at the beginning of this section, we can resolve the ordering using Topology Sort, I already attached an article about this previously, so you can go to that link and take a look. I will simplify the idea here.
We have a collection of beans that we will consider as “vertices” in a graph. Each bean has a list of dependencies, which are also beans. We can represent these dependencies as directed edges in the graph: if beanA depends on beanB, there is a directed edge from beanA to beanB.
To determine the correct order of bean creation, we can follow these steps:
Step 1: Identify Initial Beans: Find all beans (vertices) with no outgoing edges. These beans do not depend on any other beans and are ready to be created first.
Step 2: Traverse Dependencies:
- For each bean identified in the previous step, traverse all beans that depend on them.
- For each beanB that depends on beanA, remove the directed edge from beanB to beanA.
Step 3: Check Remaining Dependencies:
- After removing the edge, check if beanB has any other outgoing edges (i.e., dependencies on other beans).
- If beanB still has outgoing edges, it still has dependencies that are not yet created, so we leave it for now.
- If beanB has no remaining outgoing edges, it means all of its dependencies have been created, and it is now ready to be created.
Step 4: Repeat the Process:
- Repeat the above steps for the newly identified beans with no outgoing edges.
- Continue this process until no more beans can be identified with no outgoing edges.
. We can traverse all beans which is not created, then from that bean, we continue to traverse through its outgoing paths, it’s trivial that there might be some points, we will see the bean we’ve already seen
If, after processing all possible beans, there are still beans left that have not been created, it indicates the presence of circular dependencies. These beans are part of at least one circular dependency path.
The next thing is that we have to identify all the circular path, we can achieve this by using backtracking as following
Step 1: Traverse Uncreated Beans:
- Start from each uncreated bean and traverse its outgoing paths.
- Track the beans encountered during the traversal.
Step 2: Detect Circular Paths:
- If during traversal you encounter a bean that has already been seen in the current path, a circular dependency is detected.
- Record this circular path.
Here is the code for resolving bean creation order and circular dependency paths detector
import java.util.*;
public class BeansOrderResolver {
private Map<String, List<String>> dependents = new HashMap<>();
public BeansOrderResolver(Map<String, List<String>> dependents) {
this.dependents = dependents;
}
public Pair<List<String>, List<List<String>>> resolve() {
Map<String, List<String>> children = new HashMap<>();
Set<String> services = new HashSet<>();
for (Map.Entry<String, List<String>> entry : dependents.entrySet()) {
String key = entry.getKey();
List<String> value = entry.getValue();
services.add(key);
for (String dependent : value) {
services.add(dependent);
children.computeIfAbsent(dependent, k -> new ArrayList<>()).add(key);
}
}
Queue<String> queue = new LinkedList<>();
Map<String, Boolean> initialized = new HashMap<>();
List<String> order = new ArrayList<>();
for (String service : services) {
if (!dependents.containsKey(service) || dependents.get(service).isEmpty()) {
queue.add(service);
initialized.put(service, true);
}
}
while (!queue.isEmpty()) {
String currentService = queue.poll();
initialized.put(currentService, true);
order.add(currentService);
if (children.containsKey(currentService)) {
for (String child : children.get(currentService)) {
dependents.get(child).remove(currentService);
if (dependents.get(child).isEmpty() && !initialized.getOrDefault(child, false)) {
queue.add(child);
initialized.put(child, true);
}
}
}
}
List<List<String>> circulars = new ArrayList<>();
for (String service : services) {
if (!initialized.getOrDefault(service, false)) {
List<List<String>> circularsFromService = findCirculars(service);
circulars.addAll(circularsFromService);
}
}
return new Pair<>(order, circulars);
}
private List<List<String>> findCirculars(String source) {
List<List<String>> ans = new ArrayList<>();
Queue<List<String>> queue = new LinkedList<>();
List<String> pathSoFar = new ArrayList<>();
pathSoFar.add(source);
queue.add(pathSoFar);
while (!queue.isEmpty()) {
List<String> u = queue.poll();
String last = u.get(u.size() - 1);
if (dependents.containsKey(last)) {
for (String dependent : dependents.get(last)) {
List<String> v = new ArrayList<>(u);
v.add(dependent);
if (u.contains(dependent)) {
ans.add(v);
} else {
queue.add(v);
}
}
}
}
return ans;
}
}
record Pair<K, V>(K first, V second) {
public K getFirst() {
return first;
}
public V getSecond() {
return second;
}
}import java.util.*;
public class BeansOrderResolver {
private Map<String, List<String>> dependents = new HashMap<>();
public BeansOrderResolver(Map<String, List<String>> dependents) {
this.dependents = dependents;
}
public Pair<List<String>, List<List<String>>> resolve() {
Map<String, List<String>> children = new HashMap<>();
Set<String> services = new HashSet<>();
for (Map.Entry<String, List<String>> entry : dependents.entrySet()) {
String key = entry.getKey();
List<String> value = entry.getValue();
services.add(key);
for (String dependent : value) {
services.add(dependent);
children.computeIfAbsent(dependent, k -> new ArrayList<>()).add(key);
}
}
Queue<String> queue = new LinkedList<>();
Map<String, Boolean> initialized = new HashMap<>();
List<String> order = new ArrayList<>();
for (String service : services) {
if (!dependents.containsKey(service) || dependents.get(service).isEmpty()) {
queue.add(service);
initialized.put(service, true);
}
}
while (!queue.isEmpty()) {
String currentService = queue.poll();
initialized.put(currentService, true);
order.add(currentService);
if (children.containsKey(currentService)) {
for (String child : children.get(currentService)) {
dependents.get(child).remove(currentService);
if (dependents.get(child).isEmpty() && !initialized.getOrDefault(child, false)) {
queue.add(child);
initialized.put(child, true);
}
}
}
}
List<List<String>> circulars = new ArrayList<>();
for (String service : services) {
if (!initialized.getOrDefault(service, false)) {
List<List<String>> circularsFromService = findCirculars(service);
circulars.addAll(circularsFromService);
}
}
return new Pair<>(order, circulars);
}
private List<List<String>> findCirculars(String source) {
List<List<String>> ans = new ArrayList<>();
Queue<List<String>> queue = new LinkedList<>();
List<String> pathSoFar = new ArrayList<>();
pathSoFar.add(source);
queue.add(pathSoFar);
while (!queue.isEmpty()) {
List<String> u = queue.poll();
String last = u.get(u.size() - 1);
if (dependents.containsKey(last)) {
for (String dependent : dependents.get(last)) {
List<String> v = new ArrayList<>(u);
v.add(dependent);
if (u.contains(dependent)) {
ans.add(v);
} else {
queue.add(v);
}
}
}
}
return ans;
}
}
record Pair<K, V>(K first, V second) {
public K getFirst() {
return first;
}
public V getSecond() {
return second;
}
}After resolving the bean creation order, we go to the step to create actual bean. If we encounter any circular dependency → we throw an runtime exception with all detected circular paths
To create beans, we go through the beans one by one in their creation order. To init the bean, we need to call the constructor of bean’s class. Since we have the constructor based dependency injection, so we need to identify which constructor we use, in case the bean has constructor-based dependency injection, we will use it, otherwise, we use the default empty arguments constructor of the class.
We check if there is any constructor annotated with @Autowired annotation, if there is, we use that constructor, otherwise, we initialize the bean using the class default constructor
private Object initBeanInstance(Class<?> clazz) throws InstantiationException, IllegalAccessException,
InvocationTargetException, NoSuchMethodException, ClassNotFoundException {
Optional<Constructor<?>> nullableAutowiredConstructor = getConstructorForInjection(clazz);
final Object instance;
if (nullableAutowiredConstructor.isEmpty()) {
instance = Class.forName(clazz.getName()).getDeclaredConstructor().newInstance();
} else {
Constructor<?> constructor = nullableAutowiredConstructor.get();
var parameterTypes = constructor.getParameterTypes();
Object[] dependencies = new Object[parameterTypes.length];
for (int i = 0; i < parameterTypes.length; i++) {
dependencies[i] = getBean(parameterTypes[i]);
}
instance = constructor.newInstance(dependencies);
}
return instance;
} private Object initBeanInstance(Class<?> clazz) throws InstantiationException, IllegalAccessException,
InvocationTargetException, NoSuchMethodException, ClassNotFoundException {
Optional<Constructor<?>> nullableAutowiredConstructor = getConstructorForInjection(clazz);
final Object instance;
if (nullableAutowiredConstructor.isEmpty()) {
instance = Class.forName(clazz.getName()).getDeclaredConstructor().newInstance();
} else {
Constructor<?> constructor = nullableAutowiredConstructor.get();
var parameterTypes = constructor.getParameterTypes();
Object[] dependencies = new Object[parameterTypes.length];
for (int i = 0; i < parameterTypes.length; i++) {
dependencies[i] = getBean(parameterTypes[i]);
}
instance = constructor.newInstance(dependencies);
}
return instance;
}After the bean creation, it’s time to inject the dependencies
Since we already injected the dependencies via constructor when we initialize the bean, we now have to inject dependencies via field-based and setter-based.
Field-based dependency injection
private void injectFieldBasedDependency(final Object instance) {
var fields = instance.getClass().getDeclaredFields();
Arrays.stream(fields)
.filter(
field -> Arrays.stream(field.getDeclaredAnnotations())
.anyMatch(a -> a.annotationType() == Autowired.class)
)
.forEach(
field -> {
final Object bean = getBean(field.getType());
field.setAccessible(true);
try {
field.set(instance, bean);
} catch (IllegalAccessException e) {
throw new RuntimeException("Cannot inject dependency " + field.getClass().getName()
+ " to " + instance.getClass().getName());
}
}
);
} private void injectFieldBasedDependency(final Object instance) {
var fields = instance.getClass().getDeclaredFields();
Arrays.stream(fields)
.filter(
field -> Arrays.stream(field.getDeclaredAnnotations())
.anyMatch(a -> a.annotationType() == Autowired.class)
)
.forEach(
field -> {
final Object bean = getBean(field.getType());
field.setAccessible(true);
try {
field.set(instance, bean);
} catch (IllegalAccessException e) {
throw new RuntimeException("Cannot inject dependency " + field.getClass().getName()
+ " to " + instance.getClass().getName());
}
}
);
}Setter-based dependency injection
private void injectSetterBasedDependencies(final Object instance) throws InvocationTargetException,
IllegalAccessException {
final Method[] methods = instance.getClass().getDeclaredMethods();
for (final Method method : methods) {
if (isAutowiredSetterMethod(method)) {
Class<?> parameterType = method.getParameterTypes()[0];
Object dependency = getBean(parameterType);
if (dependency != null) {
method.invoke(instance, dependency);
}
}
}
}
private boolean isAutowiredSetterMethod(Method method) {
return method.getName().startsWith("set")
&& method.getParameterTypes().length == 1
&& Arrays.stream(method.getDeclaredAnnotations())
.anyMatch(a -> a.annotationType() == Autowired.class);
} private void injectSetterBasedDependencies(final Object instance) throws InvocationTargetException,
IllegalAccessException {
final Method[] methods = instance.getClass().getDeclaredMethods();
for (final Method method : methods) {
if (isAutowiredSetterMethod(method)) {
Class<?> parameterType = method.getParameterTypes()[0];
Object dependency = getBean(parameterType);
if (dependency != null) {
method.invoke(instance, dependency);
}
}
}
}
private boolean isAutowiredSetterMethod(Method method) {
return method.getName().startsWith("set")
&& method.getParameterTypes().length == 1
&& Arrays.stream(method.getDeclaredAnnotations())
.anyMatch(a -> a.annotationType() == Autowired.class);
}Actually, we can stop at previous step, since it’s enough, but we can make it a little extra fun with the @PostConstruct, we annotate this annotation at the method we want to run after the bean creation
private void invokePostInitiate(Object instance) {
var postMethods = Arrays.stream(instance.getClass().getDeclaredMethods())
.filter(
method -> Arrays.stream(method.getDeclaredAnnotations())
.anyMatch(a -> a.annotationType() == PostConstruct.class)
)
.toList();
if (postMethods.isEmpty()) {
return;
}
if (postMethods.size() > 1) {
throw new RuntimeException("Cannot have more than 1 post initiate method");
}
try {
var method = postMethods.get(0);
method.setAccessible(true);
method.invoke(instance);
} catch (Exception e) {
throw new RuntimeException(e);
}
} private void invokePostInitiate(Object instance) {
var postMethods = Arrays.stream(instance.getClass().getDeclaredMethods())
.filter(
method -> Arrays.stream(method.getDeclaredAnnotations())
.anyMatch(a -> a.annotationType() == PostConstruct.class)
)
.toList();
if (postMethods.isEmpty()) {
return;
}
if (postMethods.size() > 1) {
throw new RuntimeException("Cannot have more than 1 post initiate method");
}
try {
var method = postMethods.get(0);
method.setAccessible(true);
method.invoke(instance);
} catch (Exception e) {
throw new RuntimeException(e);
}
}Create ContextLoader class which is responsible for creating and managing beans
import lombok.extern.log4j.Log4j2;
import org.reflections.Reflections;
import org.ricky.annotation.Autowired;
import org.ricky.annotation.Component;
import org.ricky.annotation.PostConstruct;
import org.ricky.loader.dependencygetter.ConstructorBasedDependencyGetter;
import org.ricky.loader.dependencygetter.DependencyGetter;
import org.ricky.loader.dependencygetter.FieldBasedDependencyGetter;
import org.ricky.loader.dependencygetter.SetterBasedDependencyGetter;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
@Log4j2
public class ContextLoader {
private static final ContextLoader INSTANCE = new ContextLoader();
private final Map<Class<?>, Object> beans = new HashMap<>();
private final List<DependencyGetter> dependencyGetters = List.of(
new ConstructorBasedDependencyGetter(),
new FieldBasedDependencyGetter(),
new SetterBasedDependencyGetter()
);
private List<String> beanNames = new ArrayList<>();
private ContextLoader() {
}
public static ContextLoader getInstance() {
return INSTANCE;
}
@SuppressWarnings("unchecked")
public <T> T getBean(Class<T> interfaceType) {
if (beans.containsKey(interfaceType)) {
return (T) beans.get(interfaceType);
}
throw new RuntimeException("No bean registered for type: " + interfaceType);
}
public synchronized void load(String scanPackage) {
final Reflections reflections = new Reflections(scanPackage);
final Set<Class<?>> classes = reflections.getTypesAnnotatedWith(Component.class);
this.beanNames = classes.stream().map(Class::getName).toList();
final Map<String, List<String>> objWithDependencies = new HashMap<>();
classes.forEach(clazz -> {
List<String> beanDependencies = getDependencies(clazz);
objWithDependencies.put(clazz.getName(), beanDependencies);
});
final BeansOrderResolver beansOrderResolver = new BeansOrderResolver(objWithDependencies);
final Pair<List<String>, List<List<String>>> resolverResponse = beansOrderResolver.resolve();
final List<String> serviceInitializationOrder = resolverResponse.getFirst();
final List<List<String>> circulars = resolverResponse.getSecond();
if (!circulars.isEmpty()) {
throw new RuntimeException(
"Circular dependency detected: " + circulars
);
}
serviceInitializationOrder.forEach(
beanName -> {
try {
final Optional<Class<?>> nullableClazz =
classes.stream().filter(clazz -> clazz.getName().equals(beanName)).findFirst();
if (nullableClazz.isEmpty()) {
return;
}
final Class<?> clazz = nullableClazz.get();
final Object instance = initBeanInstance(clazz);
injectFieldBasedDependency(instance);
injectSetterBasedDependencies(instance);
invokePostInitiate(instance);
beans.put(clazz, instance);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
);
}
private Object initBeanInstance(Class<?> clazz) throws InstantiationException, IllegalAccessException,
InvocationTargetException, NoSuchMethodException, ClassNotFoundException {
Optional<Constructor<?>> nullableAutowiredConstructor = getConstructorForInjection(clazz);
final Object instance;
if (nullableAutowiredConstructor.isEmpty()) {
instance = Class.forName(clazz.getName()).getDeclaredConstructor().newInstance();
} else {
Constructor<?> constructor = nullableAutowiredConstructor.get();
var parameterTypes = constructor.getParameterTypes();
Object[] dependencies = new Object[parameterTypes.length];
for (int i = 0; i < parameterTypes.length; i++) {
dependencies[i] = getBean(parameterTypes[i]);
}
instance = constructor.newInstance(dependencies);
}
return instance;
}
private Optional<Constructor<?>> getConstructorForInjection(Class<?> clazz) {
for (Constructor<?> constructor : clazz.getConstructors()) {
if (constructor.isAnnotationPresent(Autowired.class)) {
return Optional.of(constructor);
}
}
return Optional.empty();
}
private List<String> getDependencies(final Class<?> clazz) {
final List<String> dependencies = new ArrayList<>();
dependencyGetters.forEach(dependencyGetter -> dependencies.addAll(dependencyGetter.get(clazz, beanNames)));
return dependencies;
}
private void injectFieldBasedDependency(final Object instance) {
var fields = instance.getClass().getDeclaredFields();
Arrays.stream(fields)
.filter(
field -> Arrays.stream(field.getDeclaredAnnotations())
.anyMatch(a -> a.annotationType() == Autowired.class)
)
.forEach(
field -> {
final Object bean = getBean(field.getType());
field.setAccessible(true);
try {
field.set(instance, bean);
} catch (IllegalAccessException e) {
throw new RuntimeException("Cannot inject dependency " + field.getClass().getName()
+ " to " + instance.getClass().getName());
}
}
);
}
private void injectSetterBasedDependencies(final Object instance) throws InvocationTargetException,
IllegalAccessException {
final Method[] methods = instance.getClass().getDeclaredMethods();
for (final Method method : methods) {
if (isAutowiredSetterMethod(method)) {
Class<?> parameterType = method.getParameterTypes()[0];
Object dependency = getBean(parameterType);
if (dependency != null) {
method.invoke(instance, dependency);
}
}
}
}
private boolean isAutowiredSetterMethod(Method method) {
return method.getName().startsWith("set")
&& method.getParameterTypes().length == 1
&& Arrays.stream(method.getDeclaredAnnotations())
.anyMatch(a -> a.annotationType() == Autowired.class);
}
private void invokePostInitiate(Object instance) {
var postMethods = Arrays.stream(instance.getClass().getDeclaredMethods())
.filter(
method -> Arrays.stream(method.getDeclaredAnnotations())
.anyMatch(a -> a.annotationType() == PostConstruct.class)
)
.toList();
if (postMethods.isEmpty()) {
return;
}
if (postMethods.size() > 1) {
throw new RuntimeException("Cannot have more than 1 post initiate method");
}
try {
var method = postMethods.get(0);
method.setAccessible(true);
method.invoke(instance);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}import lombok.extern.log4j.Log4j2;
import org.reflections.Reflections;
import org.ricky.annotation.Autowired;
import org.ricky.annotation.Component;
import org.ricky.annotation.PostConstruct;
import org.ricky.loader.dependencygetter.ConstructorBasedDependencyGetter;
import org.ricky.loader.dependencygetter.DependencyGetter;
import org.ricky.loader.dependencygetter.FieldBasedDependencyGetter;
import org.ricky.loader.dependencygetter.SetterBasedDependencyGetter;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
@Log4j2
public class ContextLoader {
private static final ContextLoader INSTANCE = new ContextLoader();
private final Map<Class<?>, Object> beans = new HashMap<>();
private final List<DependencyGetter> dependencyGetters = List.of(
new ConstructorBasedDependencyGetter(),
new FieldBasedDependencyGetter(),
new SetterBasedDependencyGetter()
);
private List<String> beanNames = new ArrayList<>();
private ContextLoader() {
}
public static ContextLoader getInstance() {
return INSTANCE;
}
@SuppressWarnings("unchecked")
public <T> T getBean(Class<T> interfaceType) {
if (beans.containsKey(interfaceType)) {
return (T) beans.get(interfaceType);
}
throw new RuntimeException("No bean registered for type: " + interfaceType);
}
public synchronized void load(String scanPackage) {
final Reflections reflections = new Reflections(scanPackage);
final Set<Class<?>> classes = reflections.getTypesAnnotatedWith(Component.class);
this.beanNames = classes.stream().map(Class::getName).toList();
final Map<String, List<String>> objWithDependencies = new HashMap<>();
classes.forEach(clazz -> {
List<String> beanDependencies = getDependencies(clazz);
objWithDependencies.put(clazz.getName(), beanDependencies);
});
final BeansOrderResolver beansOrderResolver = new BeansOrderResolver(objWithDependencies);
final Pair<List<String>, List<List<String>>> resolverResponse = beansOrderResolver.resolve();
final List<String> serviceInitializationOrder = resolverResponse.getFirst();
final List<List<String>> circulars = resolverResponse.getSecond();
if (!circulars.isEmpty()) {
throw new RuntimeException(
"Circular dependency detected: " + circulars
);
}
serviceInitializationOrder.forEach(
beanName -> {
try {
final Optional<Class<?>> nullableClazz =
classes.stream().filter(clazz -> clazz.getName().equals(beanName)).findFirst();
if (nullableClazz.isEmpty()) {
return;
}
final Class<?> clazz = nullableClazz.get();
final Object instance = initBeanInstance(clazz);
injectFieldBasedDependency(instance);
injectSetterBasedDependencies(instance);
invokePostInitiate(instance);
beans.put(clazz, instance);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
);
}
private Object initBeanInstance(Class<?> clazz) throws InstantiationException, IllegalAccessException,
InvocationTargetException, NoSuchMethodException, ClassNotFoundException {
Optional<Constructor<?>> nullableAutowiredConstructor = getConstructorForInjection(clazz);
final Object instance;
if (nullableAutowiredConstructor.isEmpty()) {
instance = Class.forName(clazz.getName()).getDeclaredConstructor().newInstance();
} else {
Constructor<?> constructor = nullableAutowiredConstructor.get();
var parameterTypes = constructor.getParameterTypes();
Object[] dependencies = new Object[parameterTypes.length];
for (int i = 0; i < parameterTypes.length; i++) {
dependencies[i] = getBean(parameterTypes[i]);
}
instance = constructor.newInstance(dependencies);
}
return instance;
}
private Optional<Constructor<?>> getConstructorForInjection(Class<?> clazz) {
for (Constructor<?> constructor : clazz.getConstructors()) {
if (constructor.isAnnotationPresent(Autowired.class)) {
return Optional.of(constructor);
}
}
return Optional.empty();
}
private List<String> getDependencies(final Class<?> clazz) {
final List<String> dependencies = new ArrayList<>();
dependencyGetters.forEach(dependencyGetter -> dependencies.addAll(dependencyGetter.get(clazz, beanNames)));
return dependencies;
}
private void injectFieldBasedDependency(final Object instance) {
var fields = instance.getClass().getDeclaredFields();
Arrays.stream(fields)
.filter(
field -> Arrays.stream(field.getDeclaredAnnotations())
.anyMatch(a -> a.annotationType() == Autowired.class)
)
.forEach(
field -> {
final Object bean = getBean(field.getType());
field.setAccessible(true);
try {
field.set(instance, bean);
} catch (IllegalAccessException e) {
throw new RuntimeException("Cannot inject dependency " + field.getClass().getName()
+ " to " + instance.getClass().getName());
}
}
);
}
private void injectSetterBasedDependencies(final Object instance) throws InvocationTargetException,
IllegalAccessException {
final Method[] methods = instance.getClass().getDeclaredMethods();
for (final Method method : methods) {
if (isAutowiredSetterMethod(method)) {
Class<?> parameterType = method.getParameterTypes()[0];
Object dependency = getBean(parameterType);
if (dependency != null) {
method.invoke(instance, dependency);
}
}
}
}
private boolean isAutowiredSetterMethod(Method method) {
return method.getName().startsWith("set")
&& method.getParameterTypes().length == 1
&& Arrays.stream(method.getDeclaredAnnotations())
.anyMatch(a -> a.annotationType() == Autowired.class);
}
private void invokePostInitiate(Object instance) {
var postMethods = Arrays.stream(instance.getClass().getDeclaredMethods())
.filter(
method -> Arrays.stream(method.getDeclaredAnnotations())
.anyMatch(a -> a.annotationType() == PostConstruct.class)
)
.toList();
if (postMethods.isEmpty()) {
return;
}
if (postMethods.size() > 1) {
throw new RuntimeException("Cannot have more than 1 post initiate method");
}
try {
var method = postMethods.get(0);
method.setAccessible(true);
method.invoke(instance);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}We can try creating some bean.
import org.ricky.annotation.Component;
import java.util.List;
@Component
public class OrderRepository {
public List<String> getOrderIds() {
return List.of("1", "2", "3");
}
}import org.ricky.annotation.Component;
import java.util.List;
@Component
public class OrderRepository {
public List<String> getOrderIds() {
return List.of("1", "2", "3");
}
}Constructor-based injection to inject OrderRepository
Field-based injection to inject RestaurantService
Setter-based injection to inject PaymentService
import lombok.extern.slf4j.Slf4j;
import org.ricky.annotation.Autowired;
import org.ricky.annotation.Component;
import org.ricky.annotation.PostConstruct;
import java.util.List;
@Slf4j
@Component
public class OrderService {
private OrderRepository orderRepository;
private PaymentService paymentService;
@Autowired
private RestaurantService restaurantService;
@Autowired
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
@Autowired
public void setPaymentService(PaymentService paymentService) {
this.paymentService = paymentService;
}
@PostConstruct
void postInitiate() {
System.out.println("Do something after creating orderService instance");
}
public void makeOrder() {
paymentService.doSomething();
restaurantService.doSomething();
final List<String> orderIds = orderRepository.getOrderIds();
System.out.println("orderIds = " + orderIds);
}
}import lombok.extern.slf4j.Slf4j;
import org.ricky.annotation.Autowired;
import org.ricky.annotation.Component;
import org.ricky.annotation.PostConstruct;
import java.util.List;
@Slf4j
@Component
public class OrderService {
private OrderRepository orderRepository;
private PaymentService paymentService;
@Autowired
private RestaurantService restaurantService;
@Autowired
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
@Autowired
public void setPaymentService(PaymentService paymentService) {
this.paymentService = paymentService;
}
@PostConstruct
void postInitiate() {
System.out.println("Do something after creating orderService instance");
}
public void makeOrder() {
paymentService.doSomething();
restaurantService.doSomething();
final List<String> orderIds = orderRepository.getOrderIds();
System.out.println("orderIds = " + orderIds);
}
}import lombok.extern.slf4j.Slf4j;
import org.ricky.annotation.Component;
@Slf4j
@Component
public class PaymentService {
public void doSomething() {
System.out.println("Payment service does something");
}
}import lombok.extern.slf4j.Slf4j;
import org.ricky.annotation.Component;
@Slf4j
@Component
public class PaymentService {
public void doSomething() {
System.out.println("Payment service does something");
}
}
import lombok.extern.slf4j.Slf4j;
import org.ricky.annotation.Component;
import java.time.Instant;
@Slf4j
@Component
public class RestaurantService {
public void doSomething() {
System.out.println("Restaurant service does something");
}
public void logToday() {
System.out.println("Today = " + Instant.now());
}
}
import lombok.extern.slf4j.Slf4j;
import org.ricky.annotation.Component;
import java.time.Instant;
@Slf4j
@Component
public class RestaurantService {
public void doSomething() {
System.out.println("Restaurant service does something");
}
public void logToday() {
System.out.println("Today = " + Instant.now());
}
}Now create an entry file to test things out
import lombok.extern.log4j.Log4j2;
import org.ricky.annotation.Component;
import org.ricky.loader.ContextLoader;
import org.ricky.service.OrderService;
import org.ricky.service.RestaurantService;
@Log4j2
@Component
public class Application {
public static void main(String[] args) {
ContextLoader.getInstance().load("org.ricky");
final OrderService orderService = ContextLoader.getInstance().getBean(OrderService.class);
orderService.makeOrder();
final RestaurantService restaurantService = ContextLoader.getInstance().getBean(RestaurantService.class);
restaurantService.logToday();
}
}import lombok.extern.log4j.Log4j2;
import org.ricky.annotation.Component;
import org.ricky.loader.ContextLoader;
import org.ricky.service.OrderService;
import org.ricky.service.RestaurantService;
@Log4j2
@Component
public class Application {
public static void main(String[] args) {
ContextLoader.getInstance().load("org.ricky");
final OrderService orderService = ContextLoader.getInstance().getBean(OrderService.class);
orderService.makeOrder();
final RestaurantService restaurantService = ContextLoader.getInstance().getBean(RestaurantService.class);
restaurantService.logToday();
}
}The result
We can test the circular dependency detection as well
We can update the OrderRepository to depend on OrderSerivce, since OrderService also depends on OrderRepository → circular dependency
import org.ricky.annotation.Autowired;
import org.ricky.annotation.Component;
import java.util.List;
@Component
public class OrderRepository {
@Autowired
private OrderService orderService;
public List<String> getOrderIds() {
return List.of("1", "2", "3");
}
}import org.ricky.annotation.Autowired;
import org.ricky.annotation.Component;
import java.util.List;
@Component
public class OrderRepository {
@Autowired
private OrderService orderService;
public List<String> getOrderIds() {
return List.of("1", "2", "3");
}
}Now re-run and check the result
You can find the full code at: link
There are a couple of things we did not cover in this version. For example:
- Lazy bean initialization
- Multi-constructor without @Autowired
- Other annotation such as @Repository, @Service also indicate the bean
….
I do not have time to cover all of them, so if you interest in any of them, I challenge you to extend my version, add further functionalities to its so we can make it be as close as the actual implementation of Spring IoC Container