Dependency Injection, be it Spring IoC, CDI or Google GUICE is often seen as hard to understand or even referred to as “black magic”.
As with all complex frameworks the actual working basis is fairly simple and straight forward. In this series I am going to demonstrate, in a rather simplified manner, the internal workings of dependency injection frameworks by developing one from scratch, Voodoo DI.
Please note, Voodoo DI is meant SOLELY for educational purposes, if you are looking for production ready DI frameworks please look at CDI, Spring IoC or Google GUICE.
In this series I am going to concentrate on the annotation driven approach based on JSR-330 Dependency Injection for Java annotations used by all major DI frameworks.
JSR-330 Dependency Injection for Java specifies a common set of annotations that are used by Spring, Guice and CDI.
- @Inject – defines injection points on fields, methods and constructors.
- @Qualifier – A qualifier may annotate an injectable field or parameter and, combined with the type, identify the implementation to inject.
- @Named – Similar to @Qualifier, it allows the identification of the correct implementation based on name and type.
- @Singleton – Singleton scope, only one instance is created by the DI framework.
- @Scoped – Used to define new scope annotations. The DI Framework may use these to define new contexts (e.g. @RequestScoped).
Annotation Driven Injection
The most basic building block of any DI framework is the actual dependency injection. Prior to the introduction of annotations in Java 1.5 this was usually performed by XML configurations, which is still part of Spring IoC framework to this day.
In the simplest case we simply want to inject a new instance of a given type into a field, in this case we want a Hougan (Voodoo priest) to summon a spirit.
public class Houngan {
@Inject
private Spirit spirit;
public void interact(String name) {...}
}
public class Spirit {...}
Just as with Spring IoC and CDI (standardized in CDI 2.0) for standalone applications we will start our container from a main method and obtain the constructed instance from the Voodoo container.
public static void main(String[] args) throws IOException {
Voodoo container = Voodoo.initialize();
Houngans houngans = container.instance(Houngan.class);
System.out.println(hougans.summon("Spirit");
}
First a new instance of the requested type Hougan is constructed, just as is the case with CDI
we require a default constructor, for simplicity we will not support constructor injection.
//Obtain default constructor and initialize object.
Constructor constructor = clazz.getConstructor(new Class[]{});
targetInstance = constructor.newInstance(new Object[]{});
[java]
To find the injection points all declared fields are scanned for @Inject annotation.
for(Field fld : clazz.getDeclaredFields()) {
Inject annotation = field.getAnnotation(Inject.class);
if(annotation != null) {
//Recursive call to initialize dependent beans.
Object fieldInstance = instance(field.getType());
field.setAccessible(true);
field.set(targetInstance, fieldInstance);
}
}
For each field annotated with @Inject a new instance of the fields type is created and injected into the field, in our case creating an new Spirit. Note that for the creation of the field type the instance method is called recursively, thus insuring that all injected types are also processed by Voodoo DI. All of this is done using standard Java reflection and introspection.
This is equivalent to CDI default dependent scope or Spring's prototype scope. Creating a new instance for every injection point.
Supertype/Interface Injection
So far Voodoo DI only supports concrete bean injection. To add support for interface and supertype injection Voodoo DI must be enabled to lookup concrete implementations for any given interface or supertype.
One of the easier solutions is to map all concrete implementations by their type, supertypes and interfaces.
...
private final Map types = new ConcurrentHashMap<>();
...
public static Voodoo initalize(String packageName) {
final Voodoo voodoo = new Voodoo();
voodoo.scan(packageName);
return voodoo;
}
private void scan(String packageName) {
List types = TypeScanner.find(packageName);
types.stream()
.filter((type) ->
(!type.isInterface() && !Modifier.isAbstract(type.getModifiers())))
.forEach((type) -> {
types.put(type, type);
registerInterfaces(type);
registerSuperTypes(type);
});
}
...
The actual workings of the TypeScanner is beyond the scope of this post. Never the less, I have included it to prove that class scanning is possible with standard Java API. We will be switching to one of the major reflections libraries later on in the series.
Now instead of instantiating the injection point field type, which may be an interface or abstract type, we lookup the concrete target implementation via the types map.
public T instance(Class clazz) {
...
Constructor constructor = types.get(clazz).getConstructor(new Class[]{});
newInstance = constructor.newInstance(new Object[]{});
processFields(clazz, newInstance);
...
}
Now we can handle interface and supertype injection.
public interface Spirit {...}
public class WaterSpirit implements Spirit {
public String interact(string name) {
return Strings.format("Water spirit %s summoned.", name);
}
}
The concrete class WaterSpirit will be registered to the type WaterSpirit, as well as the interface Spirit in the Voodoo container.
@Inject
private Spirit spirit;
@Inject
private WaterSpirit waterSpirit;
Any injection point with the type Spirit or WaterSpirit will be assigned a new instance of WaterSpirit.
This works fine as long as only one valid implementation is available. As soon as we add a second implementation of Spirit its impossible to tell which implementation will get injected for Spirit.
public class EarthSpirit implements Spirit {..}
At the moment which ever concrete class is scanned last will overwrite the map entry for the Spirit type. Other DI container usually handle this by throwing exceptions during initialization.
To keep it simple Voodoo DI will check if a type has already been registered for the given interface/supertype and throw a RuntimeException.
private void registerSuperTypes(Class type) {
Class> supertype = type.getSuperclass();
while (supertype != Object.class) {
if (types.containsKey(supertype)) {
throw new RuntimeException("Ambigious Puppet for " + supertype);
}
types.put(supertype, type);
supertype = type.getSuperclass();
}
}
private void registerInterfaces(Class type) {
types.put(type, type);
for (Class interFace : type.getInterfaces()) {
if (types.containsKey(interFace)) {
throw new RuntimeException("Ambigious Puppet for " + interFace);
}
types.put(interFace, type);
}
}
If a second concrete implementation tries to register for the same interface/supertype a RuntimeException is thrown during initialization.
CDI and Spring works slightly differently, rather than disallowing ambiguous beans being registered, they scan all types and validate on an a per inject point basis whether the injection point can be satisfied.
So far Voodoo DI only provides inversion of control functionality. The real strength of dependency injection frameworks is providing extra functionality, be it providing contextual scopes such as singleton, interceptors, security or concurrency mechanisms.
In part 2 we will cover JSR-330 constructor injection and JSR-250 @PostConstruct.
2 thoughts on “Understanding Dependency Injection – Part 1 IoC”