If you have used a modern Dependency Injection (DI) framework like JBoss Seam, Google Guice or CDI (Contexts and Dependency Injection), you might already be familiar with the following line of code:
@Inject MyBean myBean;
But, to really understand how a DI framework works and what makes one different from the other, we are going to build one from scratch. Let’s call it OurDI. Then, we will compare it with some of the most popular DI frameworks out there.
So, let’s start by writing a Quick Start Guide for OurDI that shows how it works.
Quick Start Guide
As any other modern DI framework, OurDI allows you to inject dependencies using annotations as in the following example:
public class UsesInjection {
@Inject MyBean myBean;
...
}
In order to configure OurDI, all you need to do is to populate a class named Injector with all the objects that can be injected at runtime; each object must be identified by a unique name. In the following example, we are going to configure the Injector with two classes MyBean and AnotherBean, bound to the names “myBean” and “anotherBean” respectively:
public class Main {
public final static void main(String[] args) {
Injector injector = Injector.instance();
injector.bind(“myBean”, new MyBean());
injector.bind(“anotherBean”, new AnotherBean();
}
}
So, when a method of the UsesInjection class (defined above) is called, a search will be done for the name of the attribute “myBean” on the Injector instance, and the value associated with that name will be injected.
Building the framework
Well, that was a quick Quick Start Guide! Now let's see how OurDI works underneath.
OurDI is composed of two classes and an annotation:
- A singleton class named Injector, that will store the bound objects.
- A class named InjectorInterceptor, which will use AOP (Aspect Oriented Programming) to inject the dependencies at runtime.
- The @Inject annotation.
Let’s take a look at the Injector class:
public class Injector {
private Map<String,Object> bindings = new HashMap<String,Object>();
// static instance, private constructor and static instance() method
public void bind(String name, Object value) {
bindings.put(name, value);
}
public Object get(String name) {
return bindings.get(name);
}
}
As you can see, Injector is singleton class backed by a Map<String,Object> that binds each object to a unique name. Now, let’s see how injection is done when an @Inject annotation is found.
We can use any AOP library to intercept method calls and look for all the dependencies that need to be injected on the target object. In this case, we are going to use AspectJ:
@Aspect
public class InjectorInterceptor {
@Around("call(* *.*(..)) && !this(InjectorInterceptor)")
public Object aroundInvoke(ProceedingJoinPoint joinPoint) throws Throwable {
// this should return the target class
Class clazz = joinPoint.getTarget().getClass();
// inject fields annotated with @Inject
for (Field field : clazz.getDeclaredFields()) {
if (field.isAnnotationPresent(Inject.class)) {
doInjection(joinPoint.getTarget(), field);
}
}
return joinPoint.proceed();
}
private void doInjection(Object target, Field field) throws IllegalAccessException {
String name = field.getName();
field.setAccessible(true);
field.set(target, Injector.instance().get(name));
}
}
Before each method call, the aroundInvoke method will be called. It will look at all the fields of the target object to see if it finds any @Inject annotation. For each of the annotated fields, it uses the name of the field to find the corresponding object in the Injector class.
Finally, here is our @Inject annotation:
@Target(value={FIELD})
@Retention(value=RUNTIME)
@Documented
public @interface Inject {}
That’s all! you can check the full source here. Now, let’s check some aspects of our solution and compare them with other popular frameworks out there.
Configuration
In OurDI, we have to manually populate the Injector instance with the objects that are going to be injected. This is similar to Google Guice. However, in Guice, there is no need to use a name and, instead of binding objects, we bind classes:
Injector injector = Guice.createInjector(new Module() {
public void configure(Binder binder) {
binder.bind(Notifier.class).to(SendSMS.class);
binder.bind(Database.class).to(MySqlDatabase.class);
}
});
JBoss Seam and CDI take a different approach. Instead of binding each class manually, they scan all your classes on bootstrap to populate the “injector”. In JBoss Seam, you will need to register the Seam Servlet Listener on your web.xml; for CDI, no configuration is required (besides the beans.xml descriptor), the application server will do the scanning automatically (one of the benefits of being part of the JEE stack).
Scanning all classes and choosing the ones suitable for injection can take a while. So, Google Guice (and OurDI) will be faster starting up. However, with JBoss Seam or CDI, you won’t have to worry about binding each class/object manually.
Type safety
As you might already have noticed, OurDI will break at runtime with a ClassCastException if the injected object is not of the expected type. The same happens with JBoss Seam, which also uses a name-based approach (components are named using the @Name annotation).
On the other hand, Google Guice and CDI are type safe, meaning that the injected objects are not identified by a name, but by their type.
Overriding/changing implementations
In OurDI, to override/change the implementation of an injected class, all we need to do is change the object bound to the Injector class. This is very similar to Google Guice as shown before.
For JBoss Seam and CDI, we need to provide more information about the classes we want to use. I won’t dive into the details but you can check how this works in the Weld documentation for CDI and in this post for JBoss Seam.
Constructor and method injection
Besides field injection, some DI frameworks provide constructor and method injection to initialize your objects. OurDI doesn’t support any of these, neither JBoss Seam. Both CDI and Google Guice support them.
Injection of external resources
Sometimes, you need to inject things you don’t have control of. For example, DataSources, 3rd party libraries, JNDI resources, etc. OurDI doesn’t support this. JBoss Seam supports it with factories and manager components, Google Guice supports it with provider methods and CDI supports it with producer methods and fields.
Scopes
Scopes, also known as Contexts, are a fundamental part of development. They allow you to define different lifecycles for each object (i.e. request, session, or application). OurDI doesn’t support scopes. However, Google Guice, JBoss Seam and CDI, all support scopes in one way or another. You can read more about scopes and their importance on this post.
You can also check the documentation of scopes for each framework/specification: Google Guice, JBoss Seam and CDI.
Static vs. Dynamic Injection
In OurDI, every time a method is called, it will scan the object to find dependencies that need to be injected. This is called Dynamic Injection and is how JBoss Seam works. The advantage of dynamic injection is that if a dependent object is changed, the new object will be injected in the next method call, so, you’ll always end up with the “correct” instance.
Google Guice and CDI uses Static Injection, which means that injection will only occur once after the object is created. The problem here, as stated above, is that if a dependent object is changed, you will still have the reference to the old object. CDI solves this problem by using proxies that will always point to the correct instance. So, even though it uses static injection (the field is injected only once), it works like dynamic injection.
Lazy Loading of objects
In OurDI, you will need to instantiate all the classes that need to be injected on startup. This is definitely not a good idea as you are loading things in memory that you are not still using. Google Guice, JBoss Seam and CDI all use a lazy loading approach where the objects will be loaded only when needed, which is a good thing.
Aspect Oriented Programming
Most DI frameworks provide some type of method interceptor mechanism that simplifies AOP development, usually with annotations. This is another feature that OurDI lacks! However, Google Guice, JBoss Seam and CDI, all support this feature with a very similar approach.
Integration
Integration with other environments is really simple with OurDI and Google Guice. You just need to configure the Injector on startup and that’s it. JBoss Seam is a complete development platform that integrates multiple technologies and it would be almost impossible to integrate it on a different environment different from JEE. CDI is part of the JEE 6 stack, however, implementations like Weld can run on different environments with the appropriate hookups.
Conclusion
Ok, let’s face it. Our own DI framework sucks!:
- It doesn’t support constructor or method injection (or provide an alternate solution).
- It doesn’t support injection of external resources.
- It needs all classes to be instantiated on startup.
- It doesn’t support scopes.
- It doesn’t provides a method interceptor mechanism.
However, I hope OurDI was useful enough to compare different DI frameworks. We only talked about Google Guice, JBoss Seam and CDI, but you can do the exercise with any other DI framework out there.