An InjectorRegistry
is a class able to provide instances of classes from recipes of object creation.
There are two ways to get such instances either explicitly using lookupInstance(type)
or
implicitly on constructors or setters annotated by the annotation @Inject
.
Usual injection framework like Guice, Spring or CDI/Weld provides 3 ways to implicitly get an instance of a class
- constructor based dependency injection, the constructor annotated with @Inject is called with the instances as arguments, it's considered as the best way to get a dependency
- setter based dependency injection, after a call to the default constructor, the setters are called. The main drawback is that the setters can be called in any order (the order may depend on the version of the compiler/VM used)
- field based dependency injection, after a call to the default constructor, the fields are filled with the instances, this methods bypass the default security model of Java using deep reflection, relying on either not having a module declared or the package being open in the module-info.java. Because of that, this is not the recommended way of doing injection.
We will only implement the constructor based and setter based dependency injection.
When injecting instances, an error can occur if the InjectorRegistry
has no recipe to create
an instance of a class. Depending on the implementation of the injector, the error can be
detected either
- when a class that asks for injection is registered
- when an instance of a class asking for injection is requested
The former is better than the later because the configuration error are caught earlier, but here, because we want to implement a simple injector, all configuration errors will appear late when an instance is requested.
There are several ways to configure an injector, it can be done
- using an XML file, this the historical way (circa 2000-2005) to do the configuration.
- using classpath/modulepath scanning. All the classes of the application are scanned and classes with annotated members are added to the injector. The drawback of this method is that this kind of scanning is quite slow, slowing down the startup time of the application. Recent frameworks like Quarkus or Spring Native move the annotation discovery at compile time using an annotation processor to alleviate that issue.
- using an API to explicitly register the recipe to get the dependency.
We will implement the explicit API while the classpath scanning is implemented in the part 2.
The class InjectorRegistry
has 4 methods
lookupInstance(type)
which returns an instance of a type using a recipe previously registeredregisterInstance(type, object)
register the only instance (singleton) to always return for a typeregisterProvider(type, supplier)
register a supplier to call to get the instance for a typeregisterProviderClass(type, class)
register a bean class that will be instantiated for a type
As an example, suppose we have a record Point
and a bean Circle
with a constructor Circle
annotated
with @Inject
and a setter setName
of String
also annotated with @Inject
.
record Point(int x, int y) {}
class Circle {
private final Point center;
private String name;
@Inject
public Circle(Point center) {
this.center = center;
}
@Inject
public void setName(String name) {
this.name = name;
}
}
We can register the Point(0, 0)
as the instance that will always be returned when an instance of Point
is requested.
We can register a Supplier
(here, one that always return "hello") when an instance of String
is requested.
We can register a class Circle.class
(the second parameter), that will be instantiated when an instance of Circle
is requested.
var registry = new InjectorRegistry();
registry.registerInstance(Point.class, new Point(0, 0));
registry.registerProvider(String.class, () -> "hello");
registry.registerProviderClass(Circle.class, Circle.class);
var circle = registry.lookupInstance(Circle.class);
System.out.println(circle.center); // Point(0, 0)
System.out.println(circle.name); // hello
The unit tests are in InjectorRegistryTest.java
-
Create a class
InjectorRegistry
and add the methodsregisterInstance(type, instance)
andlookupInstance(type)
that respectively registers an instance into aMap
and retrieves an instance for a type.registerInstance(type, instance)
should allow registering only one instance per type andlookupInstance(type)
should throw an exception if no instance have been registered for a type. Then check that the tests in the nested class "Q1" all pass.Note: for now, the instance does not have to be an instance of the type
type
. You can use Map.putIfAbsent() to detect if there is already a pair/entry with the same key in theMap
in one call. -
We want to enforce that the instance has to be an instance of the type taken as parameter. For that, declare a
T
and say that the type of theClass
and the type of the instance is the same. Then use the same trick forlookupInstance(type)
and check that the tests in the nested class "Q2" all pass.Note: inside
lookupInstance(type)
, now that we now that the instance we return has to be an instance of the type, we can use Class.cast() to avoid an unsafe cast. -
We now want to add the method
registerProvider(type, supplier)
that register a supplier (a function with no parameter that return a value) that will be called each time an instance is requested. An astute reader can remark that a supplier can always return the same instance thus we do not need twoMap
s, but only one that stores suppliers. Add the methodregisterProvider(type, supplier)
and modify your implementation to support it. Then check that the tests in the nested class "Q3" all pass. -
In order to implement the injection using setters, we need to find all the Bean properties that have a setter annotated with
@Inject
. Write a helper methodfindInjectableProperties(class)
that takes a class as parameter and returns a list of all properties (PropertyDescriptor
) that have a setter annotated with@Inject
. Then check that the tests in the nested class "Q4" all pass.Note: The class
Utils
already defines a methodbeanInfo()
. -
We want to add a method
registerProviderClass(type, providerClass)
that takes a type and a class, theproviderClass
implementing that type and register a recipe that create a new instance ofproviderClass
by calling the default constructor. This instance is initialized by calling all the setters annotated with@Inject
with an instance of the corresponding property type (obtained by callinglookupInstance
). Write the methodregisterProviderClass(type, providerClass)
and check that the tests in the nested class "Q5" all pass.Note: The class
Utils
defines the methodsdefaultConstructor()
,newInstance()
andinvokeMethod()
. -
We want to add the support of constructor injection. The idea is that either only one of the public constructors of the
providerClass
is annotated with@Inject
or a public default constructor (a constructor with no parameter) should exist. Modify the code that instantiate theproviderClass
to use that constructor to creates an instance. Then check that the tests in the nested class "Q6" all pass. -
To finish, we want to add a user-friendly overload of
registerProviderClass
,registerProviderClass(providerClass)
that takes only aproviderClass
and is equivalent toregisterProviderClass(providerClass, providerClass)
.