Tuesday, April 20, 2010

Implementing Dependency injection using Annotations

Today in this blog post I am going to elaborate a strategy to implement the Dependency Injection pattern using annotations in Java.

Since their introduction in Java 5, annotations have been widely debated. However, as powerful as they are, Annotations are here to stay. This post requires that the reader has basic knowledge of Annotations and a little bit of reflection. Annotations are extremely handy when used in conjunction with the Reflection API.

In the following examples, i will demonstrate a way to inject fields and methods into a class using annotations.
We will use annotations to indicate that the fields need to be injected.

I have a very simple class hierarchy- ParentClass and ChildClass. The ChildClass extends the ParentClass as indicated by the name.

Lets see the declaration of these basic classes.

ParentClass.java
package annotationExp.di2;

/**
 *
 * @author ryan
 */

class ParentClass {

    public void method() {
        System.out.println("Parent Class");
    }
}



ChildClass.java
package annotationExp.di2;

/**
 *
 * @author ryan
 */
class ChildClass extends ParentClass{

    public void method() {
        System.out.println("Child Class");
    }
}

As seen, there is nothing out of the ordinary in these classes.

Now lets create a class called SampleClass that contains a reference of a parent class.

SampleClass.java (Without any annotations)
import java.util.Date;

/**
 * This is the class that needs dependecy injection of
 * the implementation class of the ParentClass reference.
 * @author ryan
 */
class SampleClass {

    
    private ParentClass parentReference;

    private Date startDate;
    private Date endDate;

    public ParentClass getParentReference() {
        return parentReference;
    }

    public void setParentReference(ParentClass parentReference) {
        this.parentReference = parentReference;
    }

    public Date getEndDate() {
        return endDate;
    }

    public Date getStartDate() {
        return startDate;
    }

    
    public void setEndDate(Date endDate) {
        this.endDate = endDate;
    }


    
    public void setStartDate(Date startDate) {
        this.startDate = startDate;
    }
    
}

Now suppose, at runtime, we need to inject an object of the child class in the parent class reference. And also, we need the startDate and endDate fields to have default values.

If we were to implement this in the normal way, we would have to write the initialization code for the parentReference and the dates in the main class where we create the object of our SampleClass, by using getters and setters.


But since such a requirement may exist at several places, we may end up writing the same piece of code redundantly at all the places where such an initialization is required.

Let us now see how annotations can help us clean up this mess.

We first begin by declaring the annotations.

FieldInject.java
package annotationExp.di2;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * This is a field level injector
 * @author ryan
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface FieldInject {
}



DateInject.java
package annotationExp.di2;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * This is a method level injector
 * @author ryan
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DateInject {int delay() default 0;}



As you can see, these annotations have been marked with a retention policy of RUNTIME because we need to inspect the annotations at runtime to inject the necessary values.

The annotation DateInject also specifies the parameter to indicate the offset number of days from the current date that should be used when calculating the date.


Now, lets declare our sample class again, but this time we use annotations to indicate that we need certain methods and fields to be injected.

SampleClass.java (With Annotations)
import java.util.Date;

/**
 * This is the class that needs dependecy injection of
 * the implementation class of the ParentClass reference.
 * @author ryan
 */
class SampleClass {

    @FieldInject
    private ParentClass parentReference;

    private Date startDate;
    private Date endDate;

    public ParentClass getParentReference() {
        return parentReference;
    }

    public void setParentReference(ParentClass parentReference) {
        this.parentReference = parentReference;
    }

    public Date getEndDate() {
        return endDate;
    }

    public Date getStartDate() {
        return startDate;
    }

    @DateInject(delay=10)
    public void setEndDate(Date endDate) {
        this.endDate = endDate;
    }


    @DateInject
    public void setStartDate(Date startDate) {
        this.startDate = startDate;
    }
    
}


Now lets write our main class that creates an object of SampleClass and also handles dependency injection by using a few methods.


TestDI.java

package annotationExp.di2;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Calendar;
import java.util.Date;

/**
 * This class demonstrates handling dependency injection for annotations placed
 * methods and variables.
 * @author ryan
 */
public class TestDI {

    public static void main(String[] args) throws Exception{

        SampleClass sc = new SampleClass();

        /*
        * This function handles dependency injection(DI) for the given object.
        * In a framework,this can be handled by interceptors or preprocessors.
        */
        handleAnnotations(sc);

        /*
        * There is no need to initialize the parentReference as
        * it has already been initialized via dependency injection.
        * Hence we will not get a null pointer exception here
        * as long as the function that handles DI is executed and
        * the field is marked for DI.
        */
        sc.getParentReference().method();
        System.out.println("Start Date : "+sc.getStartDate());
        System.out.println("End Date : "+sc.getEndDate());

    }

    public static void handleAnnotations(T object) throws Exception
    {
        handleFieldAnnotations(object);
        handleMethodAnnotations(object);
    }


    public static  void handleFieldAnnotations(T object) throws Exception{
        Field[] fields = object.getClass().getDeclaredFields();
        handleFieldInject(fields, object);
        //handle other field level annotations one by one
    }

    public static  void handleMethodAnnotations(T object) throws Exception{
        Method [] methods = object.getClass().getMethods();
        handleDateInject(methods, object);
    }

    public static void handleDateInject(Method[] methods,T object) throws Exception{
       System.out.println("Method : "+methods.length);
        for (Method method : methods) {
            System.out.println("Method : "+method.getName());
            System.out.println("Annotations : "+method.getAnnotations().length);
            System.out.println("Is DateInject Present : "+method.isAnnotationPresent(DateInject.class));
            if (method.isAnnotationPresent(DateInject.class)) {
                Class[] parameterTypes = method.getParameterTypes();
                if(parameterTypes.length==1&&parameterTypes[0].equals(Date.class))
                {
                    Annotation[] annotations= method.getAnnotations();
                    DateInject annotation = method.getAnnotation(DateInject.class);
                    if(annotation.delay()==0)
                    {
                        method.invoke(object, new Object[]{new Date()});
                    }
                    else
                    {
                        int delay = annotation.delay();
                        Calendar c = Calendar.getInstance();
                        c.add(Calendar.DAY_OF_MONTH,delay);

                        method.invoke(object, new Object[]{c.getTime()});
                    }
                }
                else
                {
                    System.out.println("Method "+method.getName()+" : Cannot be injected with a Date");
                }
            }
        }
    }

    public static void handleFieldInject(Field[] elems,T object) throws Exception{
        System.out.println("Fields : "+elems.length);
        for (Field elem : elems) {
            System.out.println("Field : "+elem.getName());
            System.out.println("Annotations : "+elem.getAnnotations().length);
            System.out.println("Is Inject Present : "+elem.isAnnotationPresent(FieldInject.class));
            if (elem.isAnnotationPresent(FieldInject.class)) {
                if(elem.getType().equals(ParentClass.class))
                {
                    elem.setAccessible(true);
                    elem.set(object, new ChildClass());
                    System.out.println("Child class is set");
                }
            }
        }
    }

}


As can be seen in the main function, the task of handling the dependency injection has been delegated to a separate method that analyzes the annotations present on the SimpleClass and injects the necessary values into the fields.
In a framework like struts, you can easily use this feature to handle dependency injection in an interceptor and mark the fields in your struts action to be injected using annotations. This will also help in centralizing the point of object creation an open the doorway to ways of implementing other creational design patterns such as the AbstractFactory and FactoryMethod.

I hope this helps.

Happy Programming :)

Signing Off
Ryan

No comments: