Create a Simple IoC Container Using Annotations Blog

Version 2

    {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 assetAccoundingDataSource(DataSource ds). The container will then look for the data source namedAccountingDataSource with the typeDataSource. 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, andctx 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 RUNTIMEretention policy. This includes class, field, method, and method parameter annotations. A new method found in both theConstructor 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 fromRef annotation. To reduce the size of the example, the above code used only the first value from the array, stored inRef 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 andmappings properties. This way, we can use theRef 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 thegetForwardReplicator() 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.

      
    http://today.java.net/im/a.gif