Current Tutorial
Creating a Dependency Injection Framework
That's the end of the series!

Creating a Dependency Injection Framework

The second example you are going to run consists in designing a simple dependency injection framework. This is a system that exists in Java EE, Jakarta EE, and other enterprise application frameworks. The concept is simple: instead of creating the objects your application needs yourself, you delegate this task to a factory. This factory can then reflectively explore your class, check if some fields need to be initialized, and if this is the case, do it for you.

 

What is Dependency Injection?

Dependency Injection is used in applications to make sure that all your business objects are correctly initialized, and that all their fields receive a correct value when doing so. Initializing a set of business objects can be complex, as your objects depend on other, collaborator objects, that need to be properly initialized in a specific order. Dependency injection frameworks can greatly help you in that.

This section shows you two features these frameworks give you. The first one is the concept of singleton, and the second one is dependency injection itself.

 

Creating a Bean Factory

Let us first implement a simple bean factory. That is, a factory that you can use with the following pattern.

public record Message(String message) {
    public Message {
        Objects.requireNonNull(message);
    }
}

void main() {
    BeanFactory beanFactory = BeanFactory.INSTANCE;
    
    Message message1 = beanFactory.getInstanceOf(Message.class, "Hello");
    System.out.println("Message 1 = " + message1);
    
    Message message2 = beanFactory.getInstanceOf(Message.class, "world");
    System.out.println("Message 2 = " + message2);
}

This class implements the singleton pattern, that you can easily implement with an enumeration. Then, it has a getInstanceOf() method, that takes a class, the one you want to build an instance of, and the arguments taken by the constructor of this class.

The implementation of the BeanFactory class is quite simple.

First, you need an array with the types of the arguments you receive. This can be done with the following stream pattern.

Second, you need to locate the corresponding constructor of the class beanClass. If it does not exist, an exception is thrown.

And lastly, you need to invoke this constructor with the arguments you received.

public enum BeanFactory {
    INSTANCE;
    public <T> T getInstanceOf(Class<T> beanClass, Object... arguments) {
        try {
            // Creating the array of the parameters types
            Class<?>[] argumentsClasses =
                Arrays.stream(arguments).map(Object::getClass).toArray(Class<?>[]::new);
            
            // Locating the corresponding constructor
            Constructor<T> beanConstructor = beanClass.getConstructor(argumentsClasses);
            
            // creating the bean
            T bean = beanConstructor.newInstance(arguments);
            return bean;
        } catch (NoSuchMethodException | InvocationTargetException |
                 InstantiationException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }
}

Running the previous main() method gives you the following.

Message 1 = Message[message=Hello]
Message 2 = Message[message=world]

 

Creating Singletons

A very common pattern used in enterprise application is to have your factory to create singletons. This makes sense if your bean are used to access services like a database or some REST server. You can implement such a pattern in the BeanFactory class.

Let us first define the annotation you need.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Singleton {
}

And let us create a service class, for instance DBService.

@Singleton
public class DBService {
}

The DBFactory needs to be refactored to detect this @Singleton annotation, and in that case, create only one instance of that class. This part is a little subtle to write, because it needs a registry, that needs to be thread-safe, and used in a thread-safe way.

Here is the class. You can see that the code you wrote in the previous iteration has been put in a private method instantiateBeanClass(), to avoid have to duplicate it in the getInstanceOf() method.

The first step consists in checking if the @Singleton annotation has been declared on the beanClass class. If this annotation is not found, then the code continues as in the first version of this class.

If the annotation is found, then you need to enforce the Singleton pattern. For that, you can use a registry, implemented by a ConcurrentMap, that is a thread-safe extension of the Map interface.

If the registry has already an instance of beanClass, then the code returns it, there is no need to build another one.

Then you need to create a new instance of beanClass. Note that this part can be executed by several threads concurrently. So at this point, you may build several instances of beanClass. You could avoid that by synchronizing all this code, but that would create some contention.

Note that the call to ConcurrentMap.putIfAbsent() is an atomic call in the case of ConcurrentMap, so you do not need to synchronize this part.

There is one caveat though: the value you passed to the ConcurrentMap.putIfAbsent() may not be the one that, in the end, was put in the map. That could be the case if several threads called this ConcurrentMap.putIfAbsent() concurrently, with different instances. In the end, there is a winner, and it could be another thread than yours. So to overcome this, you need to call ConcurrentMap.get(), to return the singleton.

public enum BeanFactory {
    INSTANCE;
    
    private final ConcurrentMap<Class<?>, Object> registry = new ConcurrentHashMap<>();
    
    public <T> T getInstanceOf(Class<T> beanClass, Object... arguments) {
        try {
            // Checking if the @Singleton annotation in on beanClass
            if (beanClass.isAnnotationPresent(Singleton.class)) {
                
                // Checking if the registry has an instance beanClass
                if (registry.containsKey(beanClass)) {
                    return (T) registry.get(beanClass);
                }
                
                // Creating a new instance of beanClass
                T bean = instantiateBeanClass(beanClass, arguments);
                
                // Adding this instance to the registry
                // putIfAbsent() is atomic in the case of a ConcurrentMap
                registry.putIfAbsent(beanClass, bean);
                
                // Returning the value that landed in the map
                // It could be another one than yours
                return (T) registry.get(beanClass);
            } else {
                T bean = instantiateBeanClass(beanClass, arguments);
                return bean;
            }
        } catch (NoSuchMethodException | InvocationTargetException |
                 InstantiationException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }
    
    private <T> T instantiateBeanClass(Class<T> beanClass, Object[] arguments)
            throws NoSuchMethodException, InstantiationException,
            IllegalAccessException, InvocationTargetException {
        Class<?>[] argumentsClasses =
                Arrays.stream(arguments).map(Object::getClass).toArray(Class<?>[]::new);
        Constructor<T> beanConstructor = beanClass.getConstructor(argumentsClasses);
        T bean = beanConstructor.newInstance(arguments);
        return bean;
    }
}

With this class, you can run this main() method.

void main() {
    BeanFactory beanFactory = BeanFactory.INSTANCE;
    
    Message message = beanFactory.getInstanceOf(Message.class, "Hello");
    System.out.println("Message = " + message);
    
    DBService dbService1 = beanFactory.getInstanceOf(DBService.class);
    DBService dbService2 = beanFactory.getInstanceOf(DBService.class);
    System.out.println("Instances of DBService are the same? " + (dbService1 == dbService2));
}

Running the previous example prints the following.

Message = Message[message=Hello]
Instances of DBService are the same? true

 

Injecting Dependencies

The last step is to enable the scanning of beanClass in search of annotated fields.

Let us define the following annotation.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Inject {
}

And let us define the following service, that depends on DBService.

@Singleton
public class MyApplication {
    @Inject
    private DBService dbService;
    
    public boolean isDBServiceSet() {
        return dbService != null;
    }
}

What you need now is for your factory to put a correct instance of DBService in the right field of MyApplication. So you need to add some code in the instantiateBeanClass() private method to scan the fields, looking for the ones that declare the @Inject annotation.

Let us refactor this private method to do the following.

The code to construct the bean from beanClass and arguments is the same. What you get at this point is a bean, but all its fields are null.

The next step is to get all the declared fields of this bean (no matter their visibility modifiers), and keep the ones that declare the @Inject annotation.

Then, for each of these annotated fields, you need to instantiate the correct type, and set the field to this value. Note that the instantiation uses BeanFactory, so this instantiation follows the @Singleton declaration you may have on this class. Note that setAccessible(true) is called no matter what, even if the field is public. In that case, it would not be needed.

private <T> T instantiateBeanClass(Class<T> beanClass, Object[] arguments)
        throws NoSuchMethodException, InstantiationException,
        IllegalAccessException, InvocationTargetException {
    
    Class<?>[] argumentsClasses =
            Arrays.stream(arguments).map(Object::getClass).toArray(Class<?>[]::new);
    Constructor<T> beanConstructor = beanClass.getConstructor(argumentsClasses);
    
    // This is the constructed bean
    T bean = beanConstructor.newInstance(arguments);
    
    // Getting the fields of this bean
    Field[] fields = beanClass.getDeclaredFields();
    
    // Filtering the fields that declare the @Inject annotation
    Field[] injectableFields =
            Arrays.stream(fields)
                    .filter(field -> field.isAnnotationPresent(Inject.class))
                    .toArray(Field[]::new);
    
    for (Field injectableField : injectableFields) {
        // Getting the class of this field, 
        // and creating an instance of this class
        // using BeanFactory
        Class<?> fieldClass = injectableField.getType();
        Object fieldValue = BeanFactory.INSTANCE.getInstanceOf(fieldClass);
        
        // Setting this field to this value
        injectableField.setAccessible(true);
        injectableField.set(bean, fieldValue);
    }
    return bean;
}

You can then run the following code to check if everything has been properly set.

void main() {
    BeanFactory beanFactory = BeanFactory.INSTANCE;
    
    MyApplication app = BeanFactory.INSTANCE.getInstanceOf(MyApplication.class);
    System.out.println("App has been created with a DBService: " + app.isDBServiceSet());
}

Running the previous code prints the following.

App has been created with a DBService: true

Last update: July 25, 2024


Current Tutorial
Creating a Dependency Injection Framework
That's the end of the series!