Annotation based Dependency Injection: Breaking Down the Basics
Jim has been coding for many years. Slowly he went from novice techie to battered veteran. The soft skin on his chin in now covered by a lush beard. The JVM does no longer hold it’s secrets like it did before. But one thing still bothers him: "Most Web-based frameworks use some kind of annotation-based Dependency Injection. How do they make it work? And could he do it himself?"
The most courageous act is still to think for yourself
If you’re like Jim thinking about this question, you probably know all about using dependency injection. But to be sure, let’s start by a definition:
DI is a design pattern and a technique used to achieve loose coupling between components or classes in an application. It allows objects to be independent of their dependencies by providing them externally.
If you are using the Java ecosystem, then Service classes looking like
@Singleton
public class UserService {
private UserRepository userRepository;
@Inject
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
// UserService methods...
}
are a very common thing[1].
When you consider the stars, our affairs don’t seem to matter
Let’s consider what frameworks should do to make Dependency Injection work:
-
Classes use some kind of annotation to indicate that they are part of the DI system
-
Constructors have an annotation to mark them as injectable[2]
-
The framework somehow scans all marked classes to create class instances
-
The framework should first create the classes without dependencies and be able to create the classes with dependencies from there
Most things in our work are less magical than you think, if you just stop and think about them! |
Walk beside me… just be my friend
If you follow above rules, it is not that difficult to create such a simplified DI framework yourself[3].
First, we create a @Bean
annotation, so we can mark classes to be picked up by our framework:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Bean {}
Then we just need to make a scanner to retrieve all marked classes.
This scanner should be able to traverse all defined class files in a package and all its subpackages.
Writing a function to recursively parse classes from a directory is a bit of a silly job, so let’s use the reflections dependency to do the heavy lifting.
This dependency provides a Reflections object, which we can use to grab all objects from a package.
By filtering out all classes without the @Bean
annotation, we are left with a nice list of all the classes we need.
static List<Class<?>> getSuitableClasses() {
return new Reflections("com.example", new SubTypesScanner(false))
.getSubTypesOf(Object.class).stream()
.filter(it -> it.isAnnotationPresent(Bean.class))
.toList();
}
You here to finish me off, Sweetheart?
Now there is 'only' one the left to do. Loop through all the classes and create an instance for each class. Notice any class which depends on another class cannot be instantiated until all dependent classes have been created. So we use a map to cache the initialized the classes. Once a class is initialized, a dependent class can simply retrieve that instance from the map and use it to initialize itself. After each class has been constructed, the initialization process is completed.
static void initializeClasses() {
var initializedClasses = new HashMap<Class<?>, Object>(); (1)
var suitableClasses = getSuitableClasses();
while (initializedClasses.size() != suitableClasses.size()) { (2)
for (var clazz : suitableClasses.stream().filter(it -> !initializedClasses.containsKey(it)).toList()) { (3)
var constructor = clazz.getDeclaredConstructors()[0]; (4)
var dependencies = Arrays.stream(constructor.getParameters()) (5)
.map(it -> initializedClasses.get(it.getType()))
.filter(Objects::nonNull)
.toList();
if (constructor.getParameterCount() == dependencies.size()) (6)
initializedClasses.put(clazz, constructor.newInstance(dependencies.toArray())); (7)
} (8)
}
}
1 | Create the initialized classes cache |
2 | Loop until all classes have been initialized |
3 | Take al uninitialized classes and loop through them |
4 | Get the constructor of uninitialized class (assuming each class has only one constructor) |
5 | Pick all initialized dependent classes (can be a list of zero or more) |
6 | Check whether all initialized dependent classes are equal the desired number of dependencies |
7 | If so, initialize the class and place it in the initialized classes cache |
8 | Classes without dependencies are initialized first; classes with dependencies are initialized in the next rounds |
And that’s it, your little DI framework is a fact! Jim loves to check out the code and try it out himself. If you feel the same way, click here for a working example!
@Autowired
annotation on a constructor is optional.