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 is now covered by a lush beard. The JVM no longer holds its 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 and thinking about this question, you probably know all about using dependency injection. But to be sure, let’s start with 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.
In the Java ecosystem, service classes that look like this:
@Singleton
public class UserService {
private UserRepository userRepository;
@Inject
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
// UserService methods...
}
are a very common sight[1].
When you consider the stars, our affairs don’t seem to matter
Let’s consider what frameworks must do to make Dependency Injection work:
-
Classes use an annotation to indicate they are part of the DI system.
-
Constructors have an annotation to mark them as injectable[2].
-
The framework scans all marked classes to create class instances.
-
The framework first creates classes without dependencies and then resolves those 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 the rules above, 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 chore, so let’s use the Reflections library to do the heavy lifting.
This library provides a Reflections object that can scan the classpath and build an index of available classes and their metadata, including annotations.
Once indexed, we can query for classes based on specific annotations.
By requesting all classes annotated with @Bean, we directly obtain the set of components we are interested in.
static Set<Class<?>> getSuitableClasses() {
return new Reflections("com.di", Scanners.TypesAnnotated)
.getTypesAnnotatedWith(Bean.class);
}
You here to finish me off, sweetheart?
Now there is only one thing left to do: loop through all the classes and create an instance for each one. Notice that any class which depends on another cannot be instantiated until all its dependent classes have been created. So, we use a map to cache the initialized 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 complete.
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 a cache for initialized classes |
| 2 | Loop until all discovered classes have been initialized |
| 3 | Filter for and iterate through all uninitialized classes |
| 4 | Get the constructor (assuming each class has only one) |
| 5 | Retrieve all already-initialized dependencies |
| 6 | Check if the number of initialized dependencies matches the required parameter count |
| 7 | If so, initialize the class and add it to the cache |
| 8 | Classes without dependencies are initialized in the first round; those with dependencies follow in subsequent rounds |
And that’s it, your own DI framework is a reality! 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.