Understanding Dependency Injection – Part 3 Contexts

So far this series has covered simple IoC dependency injection as well as bean initialization with @PostConstruct and constructor injection.

In this part we will introduce container managed contextual scopes to Voodoo DI.

A scope defines the life-cycle of a bean, allowing state to be preserved and or shared across multiple interactions. DI frameworks offer a fast variety of build-in and add-on scopes, such as @RequestScoped, @SessionScoped, @ApplicationScoped, etc…

So far Voodoo DI simply maps interfaces, supertypes and the target type directly to the concrete implementations.

//Map Interfaces and Supertypes to concrete implementation.
private final Map types = new ConcurrentHashMap<>();
...
public  T instance(Class clazz) {
    T newInstance = null;
    try {
      Constructor constructor = types.get(clazz).getConstructor(new Class[]{});
      newInstance = constructor.newInstance(new Object[]{});
      processFields(clazz, newInstance);
    } catch (Exception ex) {
      Logger.getLogger(Voodoo.class.getName()).log(Level.SEVERE, null, ex);
      throw new RuntimeException(ex);
    }
    return newInstance;
  }
...


For the container to manage contextual instances, the concrete implementation has to be wrapped into a contextual type.

public abstract class ContextualType {
  
  private final Class type;
  
  public ContextualType(Class type) {
    this.type = type;
  }
    
  public abstract T getContextualInstance();
  
  public Class getType() {
    return type;
  }
}

ContextualType.class V3

The actual life-cycle management is then performed in the context specific ContextualType implementation.

In the simplest case, similar to Spring’s prototype and CDI dependent scope, Voodoo default “Puppet” scope will return a new instance every time.

public class PuppetContextualType extends ContextualType {
  ...
  @Override
  public T getContextualInstance() {
    try {
      Constructor constructor = getType().getConstructor(new Class[]{});
      return constructor.newInstance(new Object[]{});
    } catch (Exception ex) {
      Logger.getLogger(PuppetContextualType.class.getName()).log(Level.SEVERE, null, ex);
      throw new RuntimeException(ex);
    }
  } 
}

PuppetContextualType.class V3

Instead of directly mapping the types to their concrete implementations, the concrete implementations have to be wrapped into the PuppetContextualType.

private void scan(String packageName) {
  List discoveredTypes = TypeScanner.find(packageName);
  discoveredTypes.stream()
          .filter((type) -> (!type.isInterface() && !Modifier.isAbstract(type.getModifiers())))
          .forEach((Class type) -> {
            ContextualType contextualType = new PuppetContextualType(type);
            types.put(type, contextualType);
            registerInterfaces(contextualType);
            registerSuperTypes(contextualType);
          });
}

VoodooDI.class V3

Voodoo DI now obtains the ContextualType from the types map. The actual instantiation and life-cycle management of the targeted instance is now performed by the ContextualType implementation.

  
private final Map types = new ConcurrentHashMap<>();
  ...
  public  T instance(Class clazz) {
      T newInstance = null;
      try {
         newInstance = (T) types.get(clazz).getContextualInstance();
         processFields(clazz, newInstance);
      } catch (Exception ex) {
         Logger.getLogger(Voodoo.class.getName()).log(Level.SEVERE, null, ex);
      }
    return newInstance;
  }

Singleton

Introducing Singleton Scope to Voodoo DI is a simple matter of implementing a singleton ContextualType.

public class SingletonContextualType extends ContextualType {

    private final ReentrantLock reentrantLock = new ReentrantLock();
    private T singleton;

    public SingletonContextualType(Class type) {
        super(type);
    }

    @Override
    public T getContextualInstance() {
        if (singleton == null) {
            initalizeSingleton();
        }
        return singleton;
    }

    private void initalizeSingleton() {
        reentrantLock.lock();
        if (singleton == null) {
            singleton = (T) createInstance(getType());
        }
        reentrantLock.unlock();
    }
}

SingletonContextualType.class V4

JSR-330 provides a standardized @Singleton annotation with which types can be marked to be managed by the singleton context.

@Singleton
public class Highlander {
 ...
}

During initialization all identified types are checked for @Singleton annotation, if present the type will be wrapped into a SingletonContextualType instead of the default PuppetContextualType.

...
  private ContextualType buildContextualInstance(Class type) {
    Annotation singleton = type.getAnnotation(Singleton.class);
    if(singleton == null) {
      return new PuppetContextualType(type);
    } else {
      return new SingletonContextualType(type);
    }
  }
...

Voodoo.class V4

Custom Scopes

So far basic context have been introduced to Voodoo DI, however these are implemented in a rather static non-extensible fashion (if(context) {…} else {…}).

To allow for custom contexts to be added we will have to enable Voodoo DI to discover available contexts on the classpath.

public abstract class Context {
    public Set initalizeContextualTypes(Set types) {
        //Find class with ContextAnnotation and build contextual instances.
        ...
    }
    //Context Annotation used for contextual type scanning. 
    public abstract Class getContextAnnotation();

    protected abstract ContextualType buildContextualType(Class type);
}

For this all available contexts have to extend Context.

So far we have relied on Voodoo DI TypeScanner, as stated previously this was solely implemented to proof that its possible. From here on Voodoo DI will utilize on Ron Mamo reflections library for type scanning.

To start with, Reflections scans the entire classpath, just as was the case with the TypeScanner. With reflections.getSubTypesOf(Context.class) we then discover all available contexts.

...
public static Map process(String packageName) {
    //Scan entire classpath
    Reflections reflections = new Reflections(new ConfigurationBuilder().addUrls(ClasspathHelper.forManifest()));
    Set> contexts = reflections.getSubTypesOf(Context.class);
    ...
}
...

ContextManager.class V5

Each discovered context is constructed(5) and initializes all its contextual types(6).

...
public static Map process(String packageName) {
    ...
    Stream contextualTypes = contexts.parallelStream()
            .map(contextType -> constructContext(contextType))
            .map(context -> initalizeContextualTypes(context, reflections))
            .flatMap(ctypes -> ctypes.stream());

    return mapTypes(contextualTypes);
}
...

ContextManager.class V5

The contextual types from the initialized contexts are merged(7) and as before mapped(9) to their respective type, supertypes and interfaces.

If we wanted to create a context that returns a random contextual instance (a.k.a. Pooled) to Voodoo DI we simply have to define an context annotation to mark the targeted beans.

@Scope
@Inherited
public @interface Random {}

Implement the Context, defining the annotation and implementing the contextual instance factory method.

public class RandomContext extends Context {

    private static final Class SCOPE_ANNOTATION = Random.class;

    @Override
    public Class getContextAnnotation() {
        return SCOPE_ANNOTATION;
    }

    @Override
    protected ContextualType buildContextualType(Class type) {
        return new RandomContextualType(type);
    }

}

Last but not least we will implement the contextual logic in by implementing the RandomContextualType.

public class RandomContextualType extends ContextualType {
    
    private final List randomInstance = new ArrayList<>();    

    public RandomContextualType(Class type) {
        for(int i = 0; i < 10; i++ {
            randomInstance.add(createInstance(type));
        }
        super(type);
        
    }

    @Override
    public T getContextualInstance() {
        int index = new Random().nextInt(catalogue.size());
        return catalogue.get(index);
    }
}

Using the new random context is a simple matter of annotating the targeted bean with @Random.

@Random
public class MyRandomBean {
   ...
}

In this post we covered one of the most useful features of DI frameworks, managing contextual instances. In the next post of this series we will have a look at interceptors.

Acknowledgements

I would like to thank Björn Sonntag and Robert Meyer for reviewing my posts.

2 thoughts on “Understanding Dependency Injection – Part 3 Contexts”

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.