Skip to main content

Create a Simple IoC Container Using Annotations

July 5, 2005

{cs.r.title}









Contents
Property-Based Injection
Constructor-Based Dependency Injection
Generic Service Factory Builder
Conclusion

Annotations, introduced in Java 5.0, allow you to add metadata to your
code and then reuse the information it contains at compile time or at
run time. In this article, you will see how to use annotations to
automatically resolve component dependencies. This can help to build a
flexible container that can be used to inject dependencies into the custom components.

Inversion of Control (IoC), or
Dependency Injection (DI),
is a concept that allows
for the removal of responsibilities from the service component, so it doesn't have
to worry about obtaining required services or components. In this architecture,
such responsibility is moved to the component of the container that is creating or
managing our component. In other words, our component never creates or
looks up instances of the required subcomponents, but instead it declares what types of dependencies are required for this component.

This concept had been implemented in several
popular containers, such as PicoContainer and
Spring, and these frameworks are
capable of creating the whole hierarchy of dependent components.

This approach is also a key point in a EJB3 design.

There are two popular types of injection. The first one is
based on property/setter and second is based on dependency injection using
a constructor. This article will look at each in turn.

Property-Based Injection

With property- or setter-based injection, the container that is managing dependencies
is introspecting all setter methods or class fields and trying to find named
dependencies that match the property and the setter method name. This makes it easy
for developers, as they only need to add a setter method, such as
setAccoundingDataSource(DataSource ds). The container will then
look for the data source named AccountingDataSource
with the type DataSource. You can see how this enables
autowiring (automatic dependency resolution), because the names of the properties
identify the dependency names.

Typical code that a container can use to instantiate a component and inject dependencies may look like this:

  public static Object buildWithSetters( String name, 
                                Class<?> c,
                                Map<String, Object> ctx) {
    try {
      Object component = c.newInstance();
 
      Method[] methods = c.getMethods();
      for( int i = 0; i<methods.length; i++) {
        Method m = methods[ i];
        String mname = m.getName();
        Class[] types = m.getParameterTypes();
        if(mname.startsWith( "set") &&
            types.length==1 &&
            m.getReturnType()==Void.TYPE) {
          String dname = mname.substring( 3);
          m.invoke( component, new Object[] {
              ctx.get( dname.toLowerCase())});
        }
      }
     
      ctx.put(name, component);
      return component;
     
    } catch( Exception ex) {
      String msg = "Initialization error";
      throw new RuntimeException( msg, ex);
   
    }
  }

The name parameter is the name of component that is going to be instantiated,
class parameter is its type, and ctx Map is the collection of
already instantiated components. For simplicity, supporting singletons or
object pool is moved out of the scope of this article.

The component can be implemented like this:

public class Replicator {
  private DataSource input;
  private DataSource output;

  public void setInput(DataSource input) {
    this.input = input;
  }

  public void setOutput(DataSource output) {
    this.output = output;
  }

  public void replicate() {
    // ...
  }
}

You may have noticed several disadvantages in this approach.
First of all, even with two dependencies, the code is quite verbose because of these set methods.
The component can be instantiated in an invalid state if
not all dependencies will be set by the managing container.
The created instance can't be protected from the modification by the client code
(unless the object factory will add some decorator/wrapper around the original instance).

Constructor-Based Dependency Injection

With constructor-based dependency injection, a component has to declare a single constructor with all required dependencies. The container will pass resolved dependencies to this constructor when instantiating a component. This allows atomic initialization.

Constructor-based dependency injection naturally enforces the component contract for mandatory dependencies, as all of them have to be passed to the constructor. It also does a better job of protecting dependencies from modification, because there is no need to expose mutating methods.

Unfortunately, the Java reflection API does not allow retrieval of the formal parameter names for class methods and constructors, so we can only look up dependencies by the types of the constructor's parameters.

  public static Object buildWithConstructor( 
                                String name,
                                Class<?> c,
                                Map<String,Object> ctx) {
    try {
      Constructor[] constructors = c.getDeclaredConstructors();
      assert constructors.length!=1 :
        "Component must have single constructor";
       
      Constructor cc = constructors[ 0];
      Class[] types = cc.getParameterTypes();
     
      Object[] params = new Object[ types.length];

      for( int i = 0; i < names.length; i++) {
        params[ i] = context.get( types[ i]);
      }
     
      Object component = cc.newInstance( params);;
      ctx.put(name, component);
      return component;
     
    } catch( Exception ex) {
      String msg = "Initialization error";
      throw new RuntimeException( msg, ex);
   
    }
  }

Now we can try to rewrite the Replicator component for
Constructor dependency injection.

public class Replicator {
  private final DataSource input;
  private final DataSource output;

  public Replicator(
      DataSource input,
      DataSource output) {
    this.input = input;
    this.output = output;
  }

  public void replicate() {
    // ...
  }
}

As you can see, the code is slightly shorter, but there is a fundamental
problem. We can't resolve dependencies automatically, because there are two dependencies of the same type.
This is the limitation of autowiring using constructor parameters. As a result, components can't use more then one dependency of any given type.

Luckily, with Java 5, we can attach metadata to classes,
methods, and parameters, and access this information at run time.
So,we can declare an annotation for constructor parameters that
will allow us to enrich information about method and constructor
parameters available at run time:

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.PARAMETER,
          ElementType.METHOD,
          ElementType.CONSTRUCTOR})
public @interface Ref {
  String[] value();
}

This annotation allows to assign one or more aliases to constructor, method, or
method parameter. Using this annotation constructor, the Replicator
component will look like this:

// ...

  public class Replicator(
      @Ref("input") DataSource input,
      @Ref("output") DataSource output) {
    this.input = input;
    this.output = output;
  }

  // ...

Now we have enough information to implement a builder
for constructor-based injection that will use
named dependencies:

  public static Object buildWithConstructor( 
      String name,
      Class<?> c,
      Map<String,Object> ctx) {
    try {
      Constructor[] constructors =
        c.getDeclaredConstructors();
      assert constructors.length!=1 :
        "Component must have single constructor";

      Constructor<?> cc = constructors[ 0];
      Class[] types = cc.getParameterTypes();

      Annotation[][] anns =
          cc.getParameterAnnotations();

      String[] names = new String[ types.length];
      for( int i = 0; i<anns.length; i++) {
        Annotation[] ann = anns[ i];
        for( int j = 0; j<ann.length; j++) {
          if( ann[ j] instanceof Ref) {
            String[] v = (( Ref) ann[ j]).value();
            names[ i] = v[ 0];
          }

        }
      }
     
      Object[] params = new Object[ types.length];
      for( int i = 0; i<types.length; i++) {
        params[ i] = ctx.get( names[ i]);
      }

      Object component = cc.newInstance( params);;
      ctx.put(name, component);
      return component;
     
    } catch( Exception ex) {
      String msg = "Initialization error";
      throw new RuntimeException( msg, ex);
   
    }
  }

New methods added to the reflection API since Java 5 allows you to
retrieve annotations defined with the RUNTIME retention policy. This includes class, field, method, and method parameter annotations.

A new method found in both the Constructor and the Method classes,
getParameterAnnotations(), returns an array of arrays. The
size of the first array is the same as the number of parameters in the constructor. The nested array contains annotations declared for the corresponding
constructor parameter, and will have a size of 0 if there are no
annotations declared. Using this, we can iterate through parameter annotations
and collect values from Ref annotation. To reduce the size of
the example, the above code used only the first value from the array, stored
in Ref annotation.
A similar approach can be used to implement a generic service factory.

Generic Service Factory Builder

A service factory is used to create objects based on statically bound parameters.
It usually has to resolve all dependencies. This adds another layer of
indirection, because you have to map concrete services to the declared dependency
types. Let's look at an example.

public interface ReplicatorFactory {
  Replicator getForwardReplicator();
  Replicator getBackwardReplicator();
}

A concrete implementation of ReplicatorFactory will have
to map components from a context to the named dependencies for the component
that is being generated, and then it can use either a property-based or a
constructor-based builder. For example:

public class ReplicatorFactoryImpl implements
      ReplicatorFactory {
  private final Map<String, Object> ctx;

  public ReplicatorFactoryImpl(Map<String,Object> ctx) {
    this.ctx = ctx;
  }
     
  public Replicator getForwardReplicator() {
    Map<String,Object> childCtx =
        new HashMap<String, Object>();
    childCtx.put("input",ctx.get("source1"));
    childCtx.put("output",ctx.get("source2"));

   
    return (Replicator)
        Builder.buildWithConstructor(
            "forwardReplicator",
            Replicator.class, childCtx);
  }

As you can see, the code is quite verbose and hard to read and generalize.
However, we can again use annotations to declare a dependency mapping right in
the interface, and then use dynamic proxy to create an instance. Let's introduce the
additional annotation type Mapping, which will be used to define mappings:

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD})
public @interface Mapping {
  String param();
  String ref();
}

To actually declare multiple mappings, we can add an additional attribute to
the Ref annotation.

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.PARAMETER,
          ElementType.METHOD,
          ElementType.CONSTRUCTOR})
public @interface Ref {
  String[] value() default {};
  Mapping[] mappings() default {};
}

Notice the defaults for the value and mappings properties.
This way, we can use the Ref annotation
only with a value, or only with mappings if we need to.
Using that we can annotate dependency mappings in the ReplicationFactory interface:

public interface ReplicatorFactory {

  @Ref( mappings={
      @Mapping( param="input", ref="source1"),
      @Mapping( param="output", ref="source2")
    })

  Replicator getForwardReplicator();

  @Ref( mappings={
      @Mapping( param="input", ref="source1"),
      @Mapping( param="output", ref="source2")
    })

  Replicator getBackwardReplicator();

}

Now, instances of the ReplicatorFactory can be created with dynamic proxy:

public static Object buildFactory( Class c, 
      Map<String,Object> context) {
  return Proxy.newInstance( c.getClassLoader(),
      new Class[] { c},
      new AutoWireInvocationHandler(c, context));
}

Actual instantiation and dependency injection is happening in the
AutoWireInvocationHandler class, which is retrieving all
methods declared in the given interface and creating an instance of the return
type and initializing it, based on the methods' Ref annotation.
When these methods are invoked, a corresponding instance is returned. Primitive implementation of
the InvocationHandler may look something like this:

public final class AutoWireInvocationHandler 
    implements InvocationHandler {
  private Map<String, Object> services =
      new HashMap<String, Object>();

  public AutoWireInvocationHandler( Class c,
      Map<String,Object> ctx) throws Exception {
    Method[] methods = c.getDeclaredMethods();
    for( int i = 0; i<methods.length; i++) {
      Method m = methods[ i];
      Ref ref = m.getAnnotation(Ref.class);
      if( ref!=null) {
        services.put( m.getName(),
          createInstance( m.getReturnType(),
              ref.mappings(), ctx));
      }
    }
  } 

  private Object createInstance(Class<?> type,
      Mapping[] mappings, Map<String,Object> ctx) {
    Map<String,Object> childCtx =
        new HashMap<String, Object>();
    for( int i = 0; i < mappings.length; i++) {
      Mapping m = mappings[ i];
      childCtx.put(m.param(), ctx.get(m.ref()));
    }
    return Builder.buildWithConstructor(type, childCtx);
  }


  public Object invoke( Object proxy, Method m, Object[] args) {
    return services.get( m.getName());
  }

}

The method createInstance() is applying a similar dependency mapping
as the original implementation of the getForwardReplicator() method.
In the above code, components created for each method of the factory interface
are stored in the map, and the factory returns the same component instance on every get call.
If needed, this can be extended using an additional annotations to
return either a new component instance, or use the object pool to reuse and share created instances.

Conclusion

The use of annotations is a powerful feature that allows you to make code clearer and easier
to read and maintain. Paired with dependency injection, annotations allow you to
build systems that are more dynamic and less coupled.

width="1" height="1" border="0" alt=" " />
Eugene Kuleshov is an independent consultant with over 15 years of experience in software design and development.