Friday, June 11, 2010

Implementing your own Dependency Injection, Part 2

In my previous post "Implementing Your own DI", I discussed how to implement a simple dependency injection using Java Annotations and Reflection API. In this post, I will discuss about adding some extra features to the previous implementation.

The new features included are:
  • Constructor level injection.
  • Injecting methods with multi-parameters.
  • Resolve dependencies by specifying an id, if not specified use the member's name (field's name or setter method's name) instead, or by class type.
  • Dependencies can be marked as optional.
  • Adding foreign objects. Objects that are not created by this implementation but are dependencies of classes created and wired by this implementation.

First of all, I want to mention that not all projects need to use DI. You can not just go ahead and use DI for every project you are working on simply because you can and you know how to use a DI framework. Projects that have only one or two implementations of each class/interface are easy to handle and wire dependencies without a DI framework. Such projects adding a new implementation and/or changing dependencies may not be needed at all or at least it is something that is rarely needed. In general, when instantiating objects and wiring dependencies could be a very complex task that will require a lot of time and effort, only then you should consider using a DI framework or implementing your own DI.

Another thing that I want to mention is that you should consider using a DI Framework such as Spring Framework, Google Guice, PicoContainer. These frameworks are stable, have lots of capabilities and were written for you, so you don't have to write your own DI, plus they are free and open source. So, you should always consider using one of these DI frameworks, unless you have a good reason not to.

Now, let's start by exploring the Annotations we will use to define dependencies.

Annotations
I made some changes to the @Wire annotation which is now as follows:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,ElementType.METHOD,ElementType.CONSTRUCTOR})
public @interface Wire {
 String value() default "";
 boolean required() default true;
}
ElementType.CONSTRUCTOR enables this annotation to be used for Constructors. The value() method, which holds the dependency object id, is now optional (default ""). If no id specified the dependency should be resolved by member's name or class type. Added a new method required() with default set to true which is used to determine whether the dependency is required or not.

I also, added a new annotation @WireParam which is used for constructor and method parameters.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface WireParam {
 String value() default "";
 boolean required() default true;
}
Uses of @Wire and @WireParam annotations
All members (constructor, field, method) of a class and it's super classes (except constructors, only one constructor can be wired and it should be a member of the same wired class), whether public or protected or private, that need to be injected should be annotated with @Wire annotation.

Wiring Fields
Resolving the dependency of a @Wire field depends on whether @Wire.value() was specified or not. If specified, the object with id specified will be injected into this field. If not specified, the field's name will be use as the required object's id, if no object was found, the field's type will be used to find the required object (there should be only one object of the field's type otherwise a DIException will be thrown). If the dependency object was not found and @Wire.required() was not set to false an ObjectNotFoundException will be thrown. Here are some examples:
// Object with id "aDependency" should be injected to this field
@Wire("aDependency")
private ADependency aDependency;

// Object with id "bDependency" will be injected to this field if found
@Wire(value="bDependency",required=false)
protected BDependency bDependency;

// Object with id "cDependency" will be injected to this field if found
// otherwise an instance object of type CDependency should be injected
@Wire
protected CDependency cDependency;

// Object with id "dDependency" will be injected to this field if found
// otherwise an instance object of type DDependency will be injected
// if found
@Wire(required=false)
private DDependency dDependency;
Wiring Constructors and Methods
Constructors and methods with no arguments are not allowed to be annotated with the @Wire annotation (IllegalWiringException will be thrown). Parameters are annotated using the optional @WireParam annotation. If the @WireParam is not specified on a parameter the @Wire is used instead.
// A constructor with both arguments required
// aDependency will be resolved using it's class type ADependency
// bDependency will be resolved using dependency id bDependency
@Wire
private MyClass(ADependency aDependency, @WireParam("bDependency") BDependency bDependency){
 this.aDependency=aDependency;
 this.bDependency=bDependency;
}

// A method with three arguments
// aDependency will be resolved using dependency id bDependency.
// Required since default required() value of @WireParam is true. 
// bDependency will be resolved using it's class type BDependency.
// Optional since no @WireParam is present and @Wire.require is set to false.
// cDependency will be resolved using it's class type CDependency.
// Required since default required() value of @WireParam is true. 
@Wire(required=false)
public void setDependencies(@WireParam("aDependency") ADependency aDependency, BDependency bDependency, @WireParam CDependency cDependency){
 this.aDependency=aDependency;
 this.bDependency=bDependency;
 this.cDependency=cDependency;
}

// A method with one required argument
// Object with id "aDependency" should be injected to this method
@Wire("aDependency")
public void setADependency(ADependency aDependency){
 this.aDependency=aDependency;
}

// A method with one required argument and no dependency 
// name specified
// Object with id "bDependency" should be injected to this method
// otherwise dependency will be resolved using class type BDependency
@Wire
public void setBDependency(BDependency bDependency){
 this.bDependency=bDependency;
}
Parameters are resolved by using dependency id (specified in @WireParam.value() or Wire.value()). If required object id was not specified the parameter type will be used. Why not we resolve Parameters by their names before resolving them by their types, similar to fields? The reason is because unlike fields, parameter names can not be retrieved at runtime, the Java Reflection API does not offer a way to retrieve parameter names. So, all constructor and method parameters are resolved by dependency id or parameter type. One exception though, is for setter methods (that their name start with set, setXxx, and have one argument), for these methods, if dependency id was not specified, the method name (or the name of the property this method is setting, ie setMyDep will be myDep) will be used as the dependency id before trying to use the parameter's type.

The @Wire annotation can only be used for one constructor of a given class and that constructor should be a member of the same class (not it's super classes).

Foreign Objects
Almost in all our projects we use classes from libraries that are not part of our code. Since the instantiation and wiring of these classes are handled by the library itself, we only care about wiring these classes to our classes. Our DI implementation should enable us to add instances of these classes and have them wired to our own classes.

Spring provides a set of classes in it's API in order to ease integrating Spring with some well known libraries such as Hibernate, Quartz, and others.

Overridden Methods
One of the issues that we will need to clarify is how will we deal with overridden methods? For example, if a class A has the following method
@Wire
public void doCalc(@WireParam("x") InputX x, @WireParam("y") InputY y){
// code here
}
Now, if class B extends A and needs to override the doCalc() method does it need to have it's own wiring annotations? Or should the wiring information be inherited from A.doCalc() method? At first, the solution might be that the wiring information in A.doCalc() method should also apply to B.doCalc() by default unless B.doCalc() specifies it's own wiring information. But what if B.doCalc() does not need to be wired? In this case we may need to add a new annotation @NoWire to use it for B.doCalc() in order not to be wired.

In my opinion, since B.doCalc() is overriding A.doCalc() it should also override wiring information meaning that it should have it's own wiring annotations and if it's not annotated with @Wire it will not be wired (Spring applies the same for @AutoWired methods).

Circular Dependency
Another issue, is dealing with Circular Dependency problems. A Circular Dependency happens when to objects or classes depend on each other. Consider the following classes:
class A{
// depends on B
@Wire
B b;
}

class B{
// depends on A
@Wire
A a;
}
It is easy to solve this Circular Dependency problem by first creating an instance of one class say A and then creating an instance of the second class B after that wire each instance. But what if classes A and B where declared like this
class A{
B b;
// depends on B
@Wire
A(B b){
this.b=b;
}
}

class B{
A a;
// depends on A
@Wire
B(A a){
this.a=a;
}
}
In Spring this will throw a BeanCurrentlyInCreationException. Constructor level Circular Dependencies are almost impossible to solve.

So, since we are not able to solve constructor level Circular Dependencies we'll just throw an ObjectIncreationException when we detected one. In order to able to deal with field and method level Circular Dependencies we'll consider instantiating objects before wiring dependencies. But this raises another issue. Suppose we have classes A, B, and Z. B depends on Z and A depends on both B and Z. Someone may think that instead of wiring both B and Z dependencies to A, why not wire B only and retrieve Z from B. Here how classes A, B and Z can look like:
public class A{
private B b;
private Z z;

@Wire
public A(B b){
this.b = b;
// Z may have not been wired to B
// therefore b.getZ() will return null
this.z = b.getZ();
}
}  

public class B{
@Wire
private Z z;
}  

public class Z{
// some code
}
Since we are consider instantiating objects before wiring their dependencies to avoid Circular Dependencies, a dependency is not available directly after instantiation of an object. In the above code, before instantiating A we will need to create an object of B. Therefore, an instance of B is created and is used for instantiating A. Inside the constructor of A Z is requested from B, getZ(), but B does not have a Z instance yet, as a result, A's z property will be null.

I think that it's pretty obvious that this is considered a bad practice. Objects should not be responsible for retrieving their dependencies their dependencies should be injected to them. The above problem can be easily avoided by wiring the z property of class A.

So, our implementation should be able to detect constructor level Circular Dependencies and solve field and method level Circular Dependencies.

Implementation
Let's transform the requirements and rules above to classes that can perform the job. Since we are dealing with classes, constructors, fields, methods and objects by keeping information and performing operations on them, we can create a class for each of them. In addition, we will need a class that will act as an interface for our implementation.

AbstractApplicationContext
An abstract class that acts as an interface to the DI implementation. Defines some abstract methods which all sub classes need to implement. Some of these methods are:
// Returns the object with id objectId
public abstract Object getObject(String objectId);

// Returns the object that is of type clazz
public abstract <T> T getObject(Class<T> clazz);

// Checks whether an object with the specified objectId is defined
public abstract boolean isObjectIdDefined(String objectId);

// Checks whether an object of the specified type is defined
public abstract <T> boolean isObjectTypeDefined(Class<T> clazz);

// Adds a foreign object
public abstract void addObject(String objectId, Object object);

// Removes an object
public abstract void removeObject(String objectId);

// Removes all objects
public abstract void removeAll();

// Adds a wired class
public abstract void addWiredClass(String objectId, Class clazz);
The AbstractApplicationContext could be extended by sub classes that provide additional features such as saving objects in different scopes but in this DI implementation we will use a simple sub class called SimpleApplicationContext which does not have any extra features. SimpleApplicationContext has the following constructors:
// will load di.properties if found 
SimpleApplicationContext()
// loads wired classes from the specified properties file 
SimpleApplicationContext(String propertiesFilePath)
// loads wired classes from the specified properties
SimpleApplicationContext(Properties properties)

The properties file can look something like this:
myClass=com.simpledi.MyClass
aDependency=com.simpledi.ADependency
The above properties file specifies that you have a class named com.simpledi.MyClass that you will need to instantiate and wire and that the id of wired object is myClass. The same applies to the second class com.simpledi.ADependency.

WiredMember
The base class of all wired member classes WiredConstructor, WiredField and WiredMethod. Each sub class is responsible of injecting the class member it represents. WiredMember provides the basic operations needed by it's sub classes such as extracting information from @Wire and @WireParam annotations.

WiredClass
Holds information about a wired class such as wired constructor, wired fields list, and wired methods list. Uses WiredMembersExtractor class to extract all wired members of a class.

WiredObject
Holds information about an object such as id, it's class, it's status and the object instance itself. Its responsible of instantiating the object and resolving it's dependencies.

Here a simple example of how our DI implementation can be used:
public class MyClass {

@Wire
private String helloMessage;
 
public void sayHello(){
 System.out.println(helloMessage);
}
public static void main(String[] args) {
 SimpleApplicationContext context = new SimpleApplicationContext();
 context.addWiredClass("myClass", MyClass.class);
 context.addObject("helloMessage", "hello");
 context.getObject(MyClass.class).sayHello();
 }
}
The above example uses three of the most important methods defined in AbstractApplicationContext addWiredClass(), addObject(), and getObject(). Because of their importance I will discuss about what happens when each of them is called in detail.


The addWiredClass() Method
Each time a new wired class is added, using addWiredClass(), a WiredClass instance is created for it (if none was already created). The WiredClass instance uses the WiredMembersExtractor to extract all wired members from the wired class. The WiredClass init() method looks like this:
WiredMembersExtractor membersExtractor = new WiredMembersExtractor(this);
// gets the @Wire constructor if found 
// if not found returns the default constructor
// throws IllegalWiringException if none of the above constructors were found
// or if the @Wire constructor had no arguments
// returns a WiredConstructor object
wiredConstructor = membersExtractor.extractWiredConstructor();

// extracts all wired fields of the @Wire class and it's super classes
// returns a list of WiredField objects
wiredFields = membersExtractor.extractWiredFields();

// extracts all wired methods of the @Wire class and it's super classes
// while taking care of overridden methods
// throws IllegalWiringException if a @Wire method had no arguments
// returns a list of WiredMethod objects
wiredMethods = membersExtractor.extractWiredMethods(); 
Next a WiredObject instance is created without creating an instance of the wired class. The WiredObject instance is given all it's requirements through it's constructor
// the id of the target object
// the WiredClass instance which will be used for instantiating and wiring the target object
// the context (SimpleApplicationContext) used for resolving the target object dependencies 
public WiredObject(String id,WiredClass wiredClass,AbstractApplicationContext context) {
 this.id=id;
 this.wiredClass=wiredClass;
 this.context = context;
 this.status=ObjectStatus.INIT;
}
As you can see the WiredObject keeps track of the object's status which can be
public enum ObjectStatus{
 /**
  * Object's initial status
  */
 INIT,
 /**
  * Object is in the process of creation (instantiation)
  */
 INCREATION,
 /**
  * Object has been created (instantiated)
  */
 CREATED,
 /**
  * Object's dependencies are being wired to it
  */
 WIRING,
 /**
  * Object has been created (instantiated) and wired
  */
 WIRED,
 /**
  * Object's status is unknown. Objects that are not
  * created and wired using this DI implementation
  * are always in UNKOWN state
  */
 UNKOWN
}

The addObject() Method
Now when the addObject() method is called a WiredClass is created for the passed object's class (if none was already created) but the WiredClass init() method does not get called since, we do not want to instantiate or wire this class. Also, a WiredObject instance is created with status UNKOWN.


The getObject() Method
All WiredObject instances are kept inside a Map until the getObject() of SimpleApplicationContext is called. When the getObject() is called the WiredObject instance is retrieved from the Map. If the WiredObject instance was not found an ObjectNotFoundException will be thrown. If the found the getObject() method will call and return the WiredObject getWiredInstance() method.
public Object getWiredInstance() {
 if(status==ObjectStatus.UNKOWN || status==ObjectStatus.WIRED){
  return instance;
 }
 if(status==ObjectStatus.INCREATION || status==ObjectStatus.WIRING){
   throw new ObjectIncreationException("Object with id '"+id+"' is currently in creation. Object status : "+status);
  }
 if(status==ObjectStatus.INIT){
  instantiateObject();
 }
 if(status==ObjectStatus.CREATED){
  wireObject();
 }
  
 return instance;
}
The reason why the instantiation code, instantiateObject() method, and the wiring code, wireObject() method, is separated is to enable us instantiate the object before wiring to avoid Field and Method level Circular Dependency problems, as we will see later when resolving dependencies.
private void instantiateObject(){
  
 status=ObjectStatus.INCREATION;
 instance = wiredClass.createInstance(this);
 status=ObjectStatus.CREATED;
}
 
 
private void wireObject(){
  
 status=ObjectStatus.WIRING;
  
 wiredClass.injectWiredFields(this);
 wiredClass.injectWiredMethods(this);
  
 wireDependencies();
  
 status=ObjectStatus.WIRED;
}
The WiredObject uses WiredClass for instantiating and wiring the target object. The WiredClass in return call the inject method of the WiredMember class.
public Object inject(WiredObject wiredObject){
 // make class member accessible if required
 makeAccessible();
 return injectDependencies(wiredObject, processAnnotations(wiredObject));
}
The processAnnotations() method reads the @Wire and @WireParam annotations and resolves dependencies. It returns an Array which holds the dependencies. After that the abstract method injectDependencies() which all WiredMember sub classes implement is called.
protected Object[] processAnnotations(WiredObject wiredObject){
 Wire wire = getWireAnnotation();
 Annotation[][] annotations = getAnnotations();
 Class classes[] = getDependencyTypes();
 Object dependencies[]=new Object[classes.length];
  
 for(int i=0;i<annotations.length;i++){
  String dependencyId=wire.value();
  boolean required = wire.required();
   
  for(int j=0;j<annotations[i].length;j++){
   if(annotations[i][j] instanceof WireParam){
    WireParam wireParam=(WireParam) annotations[i][j];
    dependencyId=wireParam.value();
    required = wireParam.required();
   }
  }
   
  dependencies[i]=resolveDependency(wiredObject, dependencyId, classes[i]);
   
  if(dependencies[i]==null && required){
    throw new ObjectNotFoundException("Required dependency for object ... etc.");
  }
 }
 return dependencies;
}
The getWireAnnotation(), getAnnotations(), and getDependencyTypes() are all abstract methods that need to be implemented by WiredMember subclasses. The WiredMember getDependency() method determines how the dependency should be resolved, by dependency id, member name, or dependency type.
protected Object getDependency(WiredObject wiredObject,String dependencyId,Class type){
 Object dependency = null;
 if(!dependencyId.isEmpty()){
  dependency = wiredObject.resolveDependency(dependencyId);
 }else{ 
  if(!getDefaultDependencyId().isEmpty()){
   dependency = wiredObject.resolveDependency(getDefaultDependencyId());
  }
   
  if(dependency==null){
   dependency = wiredObject.resolveDependency(type);  
  }
 }
  
 return dependency;
}
If the dependency id was not specified in @Wire or @WireParam annotations the getDefaultDependencyId() is used, which is overridden by WiredField to return the fields name and by WiredMethod which it's returned value dependens on whether the method is a setter method or not. The last attempt used to resolve the dependency is by it's type. The WiredMemeber getDependency() method calls WiredObject resolveDependency() methods.
Object resolveDependency(Class paramType){
 if(!context.isObjectTypeDefined(paramType)){
  return null;
 } 
 return resolveDependency(context.getObjectId(paramType));
}
 
Object resolveDependency(String dependencyId){

 if(!context.isObjectIdDefined(dependencyId)){
   return null;
  }

 WiredObject wiredDependency = context.getWiredObject(dependencyId);
 
 if(wiredDependency.getStatus()==ObjectStatus.INCREATION){
   throw new ObjectIncreationException("Circular Dependency detected between objects '"+id+"' and '"+dependencyId+"'");
  }

 if(wiredDependency.getStatus()==ObjectStatus.INIT){
  wiredDependency.instantiateObject();
 }
  
 if(!dependencies.add(wiredDependency)){
  logger.warning("dependency '"+dependencyId+"' was already wired for object '"+id+"'");
 }
 return wiredDependency.getInstance();  
}
The resolveDependency(String) retrieves the WiredObject instance, wiredDependency, that holds the dependency object information, from the context object. First, it checks if it's currently in creation. If it's currently in creation this means that there is a Circular Dependency the two objects. Then if the wiredDependency has not yet been instantiated it's instantiateObject() method is called which creates an instance of the target dependency object without wiring it's dependency which prevents Field and Method Circular Dependency problems. Last the wiredDependency is added to the dependencies Set (used afterwards in the wireDependencies() method) of the current wiredObject. If the wiredDependency has already been added as a dependency for this wiredObject a warning is logged. This can happen when more than one member of the same class where wired with the same dependency. The following class shows an example of a class that declares the same dependency three times.
class MyClass{

@Wire("aDependency")
private ADependency aDependency;

@Wire
public MyClass(@WireParam("aDependency") ADependency aDependency){
}

@Wire("aDependency")
public void setADependency(ADependency aDependency){
}

}
Wiring Dependecies' Dependecies
What about the dependencies that were instantiated in the WiredObject's resolveDependency(String) method? When will they get their dependencies wired? If we look back to WiredObject's wireObject() method we will find that there is a call to the wireDependencies() method which takes care of this issue.
private void wireDependencies(){
 for(WiredObject wiredDependency:dependencies){
  if(wiredDependency.getStatus()!=ObjectStatus.WIRED && wiredDependency.getStatus()!=ObjectStatus.WIRING
    && wiredDependency.getStatus()!=ObjectStatus.UNKOWN){
   wiredDependency.wireObject();
  }
 }
}
The wireDependencies() method loops over all the dependencies that were added as result of calling resolveDependency(String) method and call their wireObject() method.

That's it, after instantiating the object and wiring it and it's dependencies the object is returned.

Source Code
The full source code can be downloaded here.

The source code contains two folders "src" which contains the implementation and "test" which contains a single JUnit test and other classes used for testing.

Feel free to use and/or modify the code. Note that this code was never used or tested in a real project.

No comments:

Post a Comment