Thursday, 6 October 2011

Looking into the Magic of Dependency Injection Using Annotations - Part 4

This is the fourth and last in a short series of blogs that looks into implementing dependency injection using annotations. The previous three blogs on this subject have covered:
Today’s blog ties up all the loose ends and does produce a limited, but fully functional, dependency injection factory class based upon annotations. As you can imagine, to demonstrate the complete idea requires more classes than I usually associate with each blog, so you can get hold of the complete source for this example from Captain Debug’s new GitHub1 repository: git://github.com/roghughe/captaindebug.git.


From the image above, you can see that the code neatly divides into several functional areas. Firstly, if we’re talking about dependency injections and annotations, then we need at least two annotations to prove the point and so I’ve got: @MyComponent from part 3 plus created a new annotation: @MyAutoWire. The @MyComponent annotation is a class level annotation used to denote our components, whilst @MyAutoWire is used to annotate fields and setter methods, marking them as inject-able. I’ve applied these annotations to a couple of test classes: ClassWithAttributes and InjectedClass. The idea here is that we set up a very simple dependency injection graph using these two classes, with InjectedClass injected into ClassWithAttributes.

@Documented
@Retention
(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.ANNOTATION_TYPE })
public @interface MyComponent {

}

@Documented
@Retention
(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.FIELD })
public @interface MyAutoWire {

}

@MyComponent
public class ClassWithAttributes {

 
private String str1;

 
private int value1;

 
@MyAutoWire
 
private InjectedClass injectedClass;

 
@MyAutoWire
 
public void setValue1(int value1) {
   
this.value1 = value1;
 
}

 
public String getStr1() {
   
return str1;
 
}

 
public void setStr1(String str1) {
   
this.str1 = str1;
 
}

 
public InjectedClass getInjectedClass() {
   
return injectedClass;
 
}

 
public void setInjectedClass(InjectedClass injectedClass) {
   
this.injectedClass = injectedClass;
 
}

 
public int getValue1() {
   
return value1;
 
}
}

@MyComponent
public class InjectedClass {
 
// Blank
}

We’ll also need to search the classpath for these test classes and to that end, I’m using the sample search code from parts one and two. The functionality hasn’t changed, but if you download the code, you’ll see that I’ve done a little refactoring, moving the code that creates a list of File objects into its own class. I’ve also removed all the main(...) method nonsense and added a couple of getInstance(...) methods. This code, like the annotation checker class AnnotationChecker, hasn’t changed enough to include in the blog.

The heart of the matter is really the code that glues the other pieces together. I pinched the idea of a Bean Factory from Spring and created a very simple interface, MyBeanFactory that has one method: getBean(...)

public interface MyBeanFactory {

 
/**
   * Method that returns a bean instance given a type
   */
 
public <T> T getBean(Class<T> clazz);

}

This interface has one implementation DependencyInjectionBeanFactory that, during its construction, loads and glues together the appropriate beans.

public class DependencyInjectionBeanFactory implements MyBeanFactory {

 
public final String packageName;

 
public final Map<Class<?>, Object> injectionGraph;

 
/**
   * Default Ctor - start looking in the root location
   */
 
public DependencyInjectionBeanFactory() throws IOException, ClassNotFoundException, InstantiationException,
      IllegalAccessException
{

   
this("");
 
}

 
public DependencyInjectionBeanFactory(String packageName) throws IOException, ClassNotFoundException,
      InstantiationException, IllegalAccessException
{
   
this.packageName = packageName;
    injectionGraph = loadAll
();
 
}

 
/*
   * Given a starting point on the classpath, load all the annotated classes,
   * returning them in a map where the key is either an id value or a class
   * name
   */
 
private Map<Class<?>, Object> loadAll() throws IOException, ClassNotFoundException, InstantiationException,
      IllegalAccessException
{

   
List<File> directories = getDirectories();
    List<Class<?>> classes = loadClasses
(directories);
   
return glueClassesTogether(classes);
 
}

 
private List<File> getDirectories() throws IOException {

   
FileLoader loader = FileLoader.getInstance(packageName);
   
return loader.getFileList();
 
}

 
private List<Class<?>> loadClasses(List<File> directories) throws ClassNotFoundException, IOException {

   
List<Class<?>> classes = new ArrayList<Class<?>>();

   
for (File directory : directories) {

     
if (isJarFile(directory)) {
       
classes.addAll(loadFromJarFile(directory));
     
} else {
       
classes.addAll(loadFromFileSystem(directory));
     
}
    }
   
return classes;
 
}

 
private boolean isJarFile(File directory) {
   
return directory.getParent().endsWith(".jar");
 
}

 
private List<Class<?>> loadFromJarFile(File directory) throws ClassNotFoundException, IOException {

   
return JarLoader.getInstance(directory, packageName).getClasses();
 
}

 
private List<Class<?>> loadFromFileSystem(File directory) throws ClassNotFoundException, IOException {

   
return FileSystemClassLoader.getInstance(directory, packageName).getClasses();
 
}

 
private Map<Class<?>, Object> glueClassesTogether(List<Class<?>> classes) throws InstantiationException,
      IllegalAccessException
{

   
Map<Class<?>, Object> components = getComponents(classes);
    glueComponents
(components);
   
return components;
 
}

 
private Map<Class<?>, Object> getComponents(List<Class<?>> classes) throws InstantiationException, IllegalAccessException {

   
Map<Class<?>, Object> result = new HashMap<Class<?>, Object>();

   
for (Class<?> clazz : classes) {

     
AnnotationChecker checker = new AnnotationChecker(clazz);
     
if (checker.isMyComponent()) {

       
Object obj = clazz.newInstance();
        result.put
(clazz, obj);
     
}
    }
   
return result;
 
}

 
private void glueComponents(Map<Class<?>, Object> components) throws IllegalArgumentException, IllegalAccessException {

   
Set<Class<?>> keys = components.keySet();
   
for (Class<?> key : keys) {
     
injectFields(key, components);
   
}
  }

 
private void injectFields(Class<?> clazz, Map<Class<?>, Object> components) throws IllegalArgumentException,
      IllegalAccessException
{

   
AnnotationChecker checker = new AnnotationChecker(clazz);
    Object target = components.get
(clazz);

    List<Field> fields = checker.getAutoWireFields
();
   
for (Field field : fields) {
     
injectInToField(target, field, components);
   
}
  }

 
private void injectInToField(Object target, Field field, Map<Class<?>, Object> components) throws IllegalArgumentException,
      IllegalAccessException
{

   
field.setAccessible(true); // If a field is private, then gain access to
                  // it.
   
Object injectedClass = getObjectToInject(components, field);
    field.set
(target, injectedClass);
 
}

 
private Object getObjectToInject(Map<Class<?>, Object> components, Field field) {
   
Class<?> classField = field.getType();
   
return components.get(classField);
 
}

 
@SuppressWarnings("unchecked")
 
public <T> T getBean(Class<T> clazz) {
   
return (T) injectionGraph.get(clazz);
 
}
}

For simplicity, I’ve limited the dependency injection to fields only, but you could also easily apply this to setter methods.

The headline method in this code is
loadAll()
. It contains three sub-methods: getDirectories(), loadClasses(...) and glueClassesTogether(). Of these glueClassesTogether() seems the most interesting. It sub-divided into two: getComponents(...) which uses the AnnotationChecker to search for classes annotated with @MyComponent and glueComponents(), which does the dependency injection using a bit of reflection and some jiggery-pokery. There are a couple of important lines of code to think about here. Firstly, we have to gain access to all private fields. This is done with the line:

    field.setAccessible(true); // If a field is private, then gain access to
                  // it.

...which you could argue breaks the encapsulation of the object: there are good reasons for private fields and perhaps they should stay that way and perhaps you should prefer annotating setter method... but that’s another argument.

I’m also assuming that all @MyComponent classes have default constructors and I’m using:

        Object obj = clazz.newInstance();

...to create class instances.

Lastly, ‘dependency injection’ sounds like something really clever, but it is really only setting a field in a target object with another value and when using reflection this boils down to nothing more than:

    field.set(target, injectedClass);

That wraps up this series of blogs, which has hopefully provided an insight in how to make a dependency injection factory bean using annotations and has also shown that the Guys at Spring and the EJB3 Team don’t really have a magic wand after all. As this is a sample, it does only provide limited functionality, I may, however, add in each features from time to time...


1 This is something that’s been requested before and something that I’ve been meaning to do for a while. My aim is to, from time to time, put some, but not all sample code on GitHub.

4 comments:

Craig Doremus said...

With respect to JSR-330, it appears that @MyAutoWire is equivalent to @Inject. What is @MyComponent equivalent to?

Craig Doremus said...

With respect to JSR-330, it appears that @MyAutoWire is equivalent to @Inject. What is @MyComponent equivalent to?

Roger Hughes said...

Both are shamelessly pinched from Spring.

@MyAutoWire -> @Autowired
@MyComponent -> @Component

@Component marks an object as a Spring object.

Craig Doremus said...

Yes, it was obviously borrowed from Spring. I did some research and discovered that @Component is equivalent to @Named in JSR-330 (and @Autowired=@Inject). It seems that you are a good part of the way toward a JSR-330 implementation. Care to take it any further?