20051017 Monday October 17, 2005

Parameterized Dependency Injection

A couple of weeks ago, I blogged about using annotations to specify the kind of Expression instances that need to be injected into objects that have a requirement for such Expression instances.

I basically wanted to be able to do something like this:

...
@Expression("a * (b + c)") private Expression expression;
...

... and rely on some kind of Dependency Injection mechanism to use the @Expression annotation to construct the appropriate Expression instance into the object that requires it.

This weekend, I constructed the code that allowed me to do this using Plexus. This implementation differs from Chris Nockleberg's implementation in number of ways:

  • It uses the BigNumbers foundation. ;^)
  • It does not rely on a specialized container.
  • It injects objects, whereas Chris's implementation adds implementations of abstract operations.

So I did not want my container to have any special knowledge of the annotations that I'm using (unlike EJB 3.0), and I neither wanted to wire my objects together in a special configuration file. I wanted instances of my classes to snap together automatically, and rely entirely on the annotations found at runtime to configure themselves correctly.

I haven't found too much evidence that this can be done by the default mechanisms in existing IoC frameworks, like Pico Container, Nano Container or Spring. So what I needed was a kind of callback mechanism, informing objects of the fact that they had been injected into another object, passing in the annotations associated to the field in which they had been injected.

Since none of the IoC frameworks seemed to offer something like this - most of the lifecycle callbacks are defined on the object in which data is injected - I decided to craft my own DI mechanism... Just kidding. I decided that I would tweak Plexus a little to support the mechanism I needed.

Now the key thing that I added to the Plexus container is an Injectable interface. The Injectable interface allows objects to be notified whenever they are injected. The injected operation defined on Injectable accepts a java.lang.reflect.AnnotatedElement, containing all metadata of the field into which the object is injected. This allows the object to tweak itself to fit the requirements of the receiving field.

In project BigNumbers, that results in the following general structure:

In the figure above, the PlexusExpression is a fairly simple implementation of the ordinary Expression interface. This Expression implementation is however not constructed using an ExpressionBuilder, but it can simply be instantiated. It has one field of its own that is also subject to Dependency Injection by the PlexusContainer, which is the builder field. Because of the way Plexus works, we can configure this field by simply adding one of the ExpressionBuilder jars to the classpath of a project. Plexus will recognize the need of a builder and inject the only implementation found into the PlexusExpression's instance builder field.

The CalculatedObject has just been made up for demonstration purposes:

public class CalculatedObject {

  @BigExpression("a + b")
  private Expression sum;

  public BigDecimal useSum(BigDecimal a, BigDecimal b) {
    return .....;
  }
}

Plexus will notice the Expression requirement of instances of this object (using regular Plexus mechanisms), and notice that there is one implementation available: the PlexusExpression class. It will construct and instance and inject it into the CalculatedObject. Now here comes the real difference with the existing version of Plexus. Normally, this would basically mean the end of the composition phase in Plexus. However, I added a new type of composer to Plexus. This composer (that has been set as the default composer for PlexusExpression) will invoke the Injectable.injected(AnnotatedElement) operation on composed object, if it implements the Injectable interface. Since PlexusExpression does implement this interface, it will have an opportunity to find out which expression it needs to represent, using the field annotations passed in.

Now with all of this, it seems like a great deal of work to get this working, but the opposite is true. As long as I load an instance of CalculatedObject using my slightly reconfigure version of the DefaultPlexusContainer, I get the rest for free. Again, note that the container itself is not aware of something like Expressions. It simply allows the injected object to adapt itself to the requirements of the receiving field, at runtime. Hence, "parameterized dependency injection".

PlexusContainer container = ...;
container.initialize();
container.start();
CalculatedObject object = (CalculatedObject) container.lookup("CalculatedObject");
BigDecimal result = object.sum(BigDecimal.ONE, BigDecimal.ONE); // 2
container.dispose();

( Oct 17 2005, 09:06:52 AM CEST ) Permalink Comments [0]