This is the second part of my "DI From Scratch" series. In the previous article, we discussed our basic example, and the problems there are with the "manual" approach. Now, we want to automate the wiring of our service network.
DI Stage 4: Automating the wiring
Find the source code of this section on github
Let's focus on the main()
method, and try to find a more automatic way of creating the service network. Certainly, forgetting to call a setter is a real threat here, and the compiler won't even be able to warn you about it. But our services are structurally different, and there is no uniform way to access them... or is there? Java Reflection to the rescue!
All we essentially want to provide our setup with is a list of our service classes. From there, the setup should construct and wire the service network. In particular, our main()
method is only really interested in ServiceA
, it doesn't even need ServiceB
. Let's rewrite it like so:
public static void main(String[] args) throws Exception {
Set<Class<?>> serviceClasses = new HashSet<>();
serviceClasses.add(ServiceAImpl.class);
serviceClasses.add(ServiceBImpl.class);
ServiceA serviceA = createServiceA(serviceClasses);
// call business logic
System.out.println(serviceA.jobA());
}
But how can we implement the "magic" createServiceA
method? Turns out, it's not that hard...
private static ServiceA createServiceA(Set<Class<?>> serviceClasses) throws Exception{
// step 1: create an instance of each service class
Set<Object> serviceInstances = new HashSet<>();
for(Class<?> serviceClass : serviceClasses){
Constructor<?> constructor = serviceClass.getConstructor();
constructor.setAccessible(true);
serviceInstances.add(constructor.newInstance());
}
// step 2: wire them together
for(Object serviceInstance : serviceInstances){
for(Field field : serviceInstance.getClass().getDeclaredFields()){
Class<?> fieldType = field.getType();
field.setAccessible(true);
// find a suitable matching service instance
for(Object matchPartner : serviceInstances){
if(fieldType.isInstance(matchPartner)){
field.set(serviceInstance, matchPartner);
}
}
}
}
// step 3: from all our service instances, find ServiceA
for(Object serviceInstance : serviceInstances){
if(serviceInstance instanceof ServiceA){
return (ServiceA)serviceInstance;
}
}
// we didn't find the requested service instance
return null;
}
Let's break it down. In Step 1 we iterate over our classes, and for each class, we attempt to get the default constructor (i.e. the constructor with no arguments). Since neither ServiceAImpl
nor ServiceBImpl
specifies any constructor (we deleted them when introducing the getters/setters), the Java compiler provides a public default constructor - so that will work fine. Then, we make this constructor accessible. That's just defensive programming to make sure that private constructors will work too. Finally, we call newInstance()
on the constructor to create the instance of the class, and add
it to our set of instances.
In Step 2 we want to wire together our individual service instances. To do so, we look at each service object one by one. We retrieve it's Java class via getClass()
, and ask that class for all of its declaredFields
(declared
means that private
fields will be returned too). Just like for the constructor, we make sure that the field is accessible, and then we check the Type
of the field. This will provide us with the service class we need to put into the field. All that's left to do is to find a suitable matchParter
, an object which is of the type specified by the field. Once we find one, we call field.set(...)
and assign the match partner to the field. Note that the first parameter of the field.set(...)
method is the object which will have its field value changed.
In Step 3, the network is already complete; all that's left to do is to find the instance of ServiceA
. We can simply scan through or instances and check if we found the right one by using instanceof ServiceA
.
This might be a little daunting, so maybe try to read this once more. Also, you might want to brush up on your knowledge of Java reflection basics if any of that seems weird to you.
So what did we gain?
- Our services are wired together automatically.
- We can no longer forget to call a setter (in fact, we don't need them anymore).
- Our application will fail on startup if the wiring fails, not during the business logic.
The primary pain that we need to treat next is the fact that we do not want to repeat this whole procedure every time we want to get a hold of a service; we want to have the ability to access every service in the network, not just one.
DI Stage 5: Encapsulating the Context
Find the source code of this section on github
The object which is responsible for holding the service network is called the Dependency Injection Container, or (in Spring terms) the Application Context. I'm going to use the "context" terminology, but the terms are really synonyms. The primary job of the context is to provide a getServiceInstance(...)
method which accepts a service class as parameter, and returns the (finished and wired) service instance. So here we go:
public class DIContext {
private final Set<Object> serviceInstances = new HashSet<>();
public DIContext(Collection<Class<?>> serviceClasses) throws Exception {
// create an instance of each service class
for(Class<?> serviceClass : serviceClasses){
Constructor<?> constructor = serviceClass.getConstructor();
constructor.setAccessible(true);
Object serviceInstance = constructor.newInstance();
this.serviceInstances.add(serviceInstance);
}
// wire them together
for(Object serviceInstance : this.serviceInstances){
for(Field field : serviceInstance.getClass().getDeclaredFields()){
Class<?> fieldType = field.getType();
field.setAccessible(true);
// find a suitable matching service instance
for(Object matchPartner : this.serviceInstances){
if(fieldType.isInstance(matchPartner)){
field.set(serviceInstance, matchPartner);
}
}
}
}
}
@SuppressWarnings("unchecked")
public <T> T getServiceInstance(Class<T> serviceClass){
for(Object serviceInstance : this.serviceInstances){
if(serviceClass.isInstance(serviceInstance)){
return (T)serviceInstance;
}
}
return null;
}
}
As you can see, the code didn't change much from the previous step, except that we now have an object to encapsulate the context (DIContext
). Internally, it manages a set of serviceInstances
which is created just like before from a collection of service classes. The Step 3 from above has moved into its own getServiceInstance
method, which accepts the class to retrieve as a parameter. Since we cannot use instanceof
anymore (it requires a hard-coded class, not a dynamic variable value), we have to fall back to serviceClass.isInstance(...)
to do the same thing.
We can use this class in our new main()
:
public static void main(String[] args) throws Exception {
DIContext context = createContext();
doBusinessLogic(context);
}
private static DIContext createContext() throws Exception {
Set<Class<?>> serviceClasses = new HashSet<>();
serviceClasses.add(ServiceAImpl.class);
serviceClasses.add(ServiceBImpl.class);
return new DIContext(serviceClasses);
}
private static void doBusinessLogic(DIContext context){
ServiceA serviceA = context.getServiceInstance(ServiceA.class);
ServiceB serviceB = context.getServiceInstance(ServiceB.class);
System.out.println(serviceA.jobA());
System.out.println(serviceB.jobB());
}
As you can see, we can now easily pull out complete service instances from the context by calling getServiceInstance
as often as we need to, with different input classes. Also note that the services itself can access each other simply by declaring a field of the proper type - they don't even have to know about the DIContext
object.
There are still some problems though. For example, what if we want to have a field in our services which does not refer to another service (say, an int
field)? We need a way to tell our algorithm which fields we want it to set - and which ones to leave alone.
DI Stage 6: Annotating fields
Find the source code of this section on github
So how can we tell our algorithm which fields it needs to assign? We could introduce some fancy naming scheme and parse the field.getName()
, but that's a very error prone solution. Instead, we will use an Annotation:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Inject {
}
@Target
tells the compiler on which elements we can use this annotation - we want it to be applicable on fields. With @Retention
we instruct the compiler to keep this annotation until runtime, and not to discard it during compilation.
Let's annotate our fields:
public class ServiceAImpl implements ServiceA {
@Inject
private ServiceB serviceB;
// rest is the same as before
}
public class ServiceBImpl implements ServiceB {
@Inject
private ServiceA serviceA;
// rest is the same as before
}
An annotation in and on itself does nothing. We need to actively read the annotation. So let's do it in the constructor of our DIContext
:
// wire them together
for(Object serviceInstance : this.serviceInstances){
for(Field field : serviceInstance.getClass().getDeclaredFields()){
// check that the field is annotated
if(!field.isAnnotationPresent(Inject.class)){
// this field is none of our business
continue;
}
// rest is the same as before
Run the main()
method again; it should work just like before. However, now you are free to add more fields to your services, and the wiring algorithm won't break.
Closing words
So far, we have created a DI container which is very basic but functional. It relies on us providing it with the collection of service classes. In the next part, we will discuss how we can actually discover our service classes.
Top comments (0)