HK2 - Dependency Injection Kernel

A light-weight and dynamic dependency injection framework

Aspect Oriented Programming (AOP) Example

This example illustrates how to use Aspect Oriented Programming (AOP) with HK2 services.

AOP is generally used for cross-cutting concerns such as security or transactions. This example will illustrate how you can use AOP in HK2 in order to elegantly solve these sorts of issues.

HK2 AOP allows you to use AOP Alliance method and constructor interceptors with most services that HK2 constructs. This example will present simple caching interceptors to illustrate how to write interceptors and then use those interceptors with HK2 services.

Caching Interceptors

The method interceptor shown here assumes that the methods it will be used with take one input parameter and return some sort of result. When the method is called with an input parameter it has already seen the interceptor will not call the underlying method, but will instead find the previous result in the cache and return it. This saves the method from performing the same calculation over again. Here is the code for the method interceptor:

public class CachingMethodInterceptor implements MethodInterceptor {
    private final HashMap<CacheKey, Object> cache = new HashMap<CacheKey, Object>();

    public Object invoke(MethodInvocation invocation) throws Throwable {
        Object args[] = invocation.getArguments();
        if (args.length != 1) {
            return invocation.proceed();
        }
        
        Object arg = args[0];
        if (arg == null) {
            return invocation.proceed();
        }
        
        Method m = invocation.getMethod();
        CacheKey key = new CacheKey(m.getDeclaringClass().getName(), m.getName(), arg);
        
        if (!cache.containsKey(key)) {
            Object retVal = invocation.proceed();
            
            cache.put(key, retVal);
            
            return retVal;
        }
        
        // Found it in the cache!  Do not call the method
        return cache.get(key);
    }
}

The cache is the HashMap field named cache. After some defensive checking the interceptor creates a CacheKey from the input parameters. The interceptor then checks to see if the cache contains the corresponding key. If the cache does not contain the corresponding key then it goes ahead and calls the underlying method with the call to proceed, saving the output.
The method interceptor then puts that key and the result into the cache. However if it does find the CacheKey in the cache then it just returns the result without calling the proceed method, and thus the underlying method on the service will NOT get called.

Here is the code for the CacheKey, which is an immutable object composed of the name of the class, the name of the method called, and the argument sent to the method:

public class CacheKey {
    private final String className;
    private final String methodName;
    private final Object input;
    
    public CacheKey(String className, String methodName, Object input) {
        this.className = className;
        this.methodName = methodName;
        this.input = input;
    }
    
    public int hashCode() {
        return className.hashCode() ^ methodName.hashCode() ^ input.hashCode();
    }
    
    public boolean equals(Object o) {
        if (o == null) return false;
        if (!(o instanceof CacheKey)) return false;
        CacheKey other = (CacheKey) o;
        
        if (!other.className.equals(className)) return false;
        if (!other.methodName.equals(methodName)) return false;
        return other.input.equals(input);
    }
}

The CacheKey has an appropriate hashCode and equals method for objects that will be used as a key in a HashMap. The CacheKey will also bin be used in the constructor interceptor.

The constructor interceptor works exactly the same as the method interceptor, although the effect it has on the service is somewhat different than the effect it has on the method, since a constructor controls creation of the instance of the class. Here is the constructor interceptor:

public class CachingConstructorInterceptor implements ConstructorInterceptor {
    private final HashMap<CacheKey, Object> cache = new HashMap<CacheKey, Object>();

    public Object construct(ConstructorInvocation invocation) throws Throwable {
        Object args[] = invocation.getArguments();
        if (args.length != 1) {
            return invocation.proceed();
        }
        
        Object arg = args[0];
        if (arg == null) {
            return invocation.proceed();
        }
        
        Constructor<?> c = invocation.getConstructor();
        CacheKey key = new CacheKey(c.getDeclaringClass().getName(), "<init>", arg);
        
        if (!cache.containsKey(key)) {
            Object retVal = invocation.proceed();
            
            cache.put(key, retVal);
            
            return retVal;
        }
        
        // Found it in the cache!  Do not call the method
        return cache.get(key);
    }

This code should look familiar, as it follows exactly the same pattern as the method interceptor. This interceptor will only work on constructors with one input parameter and will short-circuit the creation of a new object by returning the value in the cache (assuming one is found). If no corresponding object is found in the cache then a new object will be created with the call to proceed.

Both the method and constructor interceptors use no HK2 API and can therefore be used in any AOP alliance system. However there is nothing preventing writers of AOP alliance interceptors from using HK2 services as AOP alliance interceptors, in which case they could do things like inject transaction or security managers.

Using Interceptors in HK2

In order to use AOP Alliance interceptors in HK2 an instance of the InterceptionService must be added to the HK2 registry. An implementation of InterceptionService must be in the Singleton scope. It has the job of determining which HK2 services (ActiveDescriptors) are candidates to use interception and then to specify exactly which methods and constructors of those services should be intercepted.

This example uses an annotation called Cache that when placed on a method or constructor indicates that that method or constructor should be intercepted with the caching interceptor. This is the definition of the Cache annotation:

@Retention(RUNTIME)
@Target( { METHOD, CONSTRUCTOR })
public @interface Cache {
}

The example InterceptionService is named HK2InterceptionService and is in the Singleton scope, as must all implementations of InterceptionService. When HK2 creates a new service it will look through all of the implementations of InterceptionService looking for ones that are appropriate for the services’ ActiveDescriptor. If the ActiveDescriptor passes through the filter returned by the InterceptionService then all the methods of that service will be given to the InterceptionService in order to determine what method interceptors should be called for that method. The single constructor that HK2 would normally use will also be given to the InterceptionService in order to determine what constructor interceptors should be called for that constructor.

The example InterceptionService inspects the input methods and constructors to see if they are annotated with @Cache, and if they are it returns the caching interceptors described in the previous section. The filter that the example InterceptionService uses to select HK2 services is the BuilderHelper allFilter, since any method or constructor might be a candidate for caching. Here is the code for the example caching HK2 InterceptionService:

@Service
public class HK2InterceptionService implements InterceptionService {
    private final static MethodInterceptor METHOD_INTERCEPTOR = new CachingMethodInterceptor();
    private final static ConstructorInterceptor CONSTRUCTOR_INTERCEPTOR = new CachingConstructorInterceptor();
    
    private final static List<MethodInterceptor> METHOD_LIST = Collections.singletonList(METHOD_INTERCEPTOR);
    private final static List<ConstructorInterceptor> CONSTRUCTOR_LIST = Collections.singletonList(CONSTRUCTOR_INTERCEPTOR);

    public Filter getDescriptorFilter() {
        return BuilderHelper.allFilter();
    }

    public List<MethodInterceptor> getMethodInterceptors(Method method) {
        if (method.isAnnotationPresent(Cache.class)) {
            return METHOD_LIST;
        }
        
        return null;
    }

    public List<ConstructorInterceptor> getConstructorInterceptors(
            Constructor<?> constructor) {
        if (constructor.isAnnotationPresent(Cache.class)) {
            return CONSTRUCTOR_LIST;
        }
        
        return null;
    }
}

In the next section we will use our caching annotation and the caching interceptors on an example set of services.

Using @Cache on Methods

The runner project under examples/caching in the HK2 source tree contains an example of method and constructor injection using the system described in the above secion (which is under examples/caching/system). It also contains unit tests to ensure that everything in the example is working as expected.

The ExpensiveMethods class has a method on it that performs an expensive operation. The expensive method counts the number of times it has been called so that the class can easily demonstrate that the caching code has worked.

@Service @Singleton
public class ExpensiveMethods {
    private int timesCalled = 0;
    
    @Cache
    public int veryExpensiveCalculation(int input) {
        timesCalled++;
        return input + 1;
    }
    
    public int getNumTimesCalled() {
        return timesCalled;
    }
    
    public void clear() {
        timesCalled = 0;
    }
}

The ExpensiveMethods service is in the Singleton scope and hence will get created once. However, the method named veryExpensiveCalculation will only get called once per input integer. So if the method is called ten times with an input parameter of 1 the method on ExpensiveMethods will only get called once. The output from that call will be saved in the cache and every other time the method is called by the application the returned value will come from the cache rather than calling the method again. This can be seen with the test code:

    @Test
    public void testMethodsAreIntercepted() {
        ExpensiveMethods expensiveMethods = testLocator.getService(ExpensiveMethods.class);
        
        // Now call the expensive method
        int result = expensiveMethods.veryExpensiveCalculation(1);
        Assert.assertEquals(2, result);
        
        // The expensive method should have been called
        Assert.assertEquals(1, expensiveMethods.getNumTimesCalled());
        
        // Now call the expensive method ten more times
        for (int i = 0; i < 10; i++) {
            result = expensiveMethods.veryExpensiveCalculation(1);
            Assert.assertEquals(2, result);
        }
        
        // But the expensive call was never made again, since the result was cached!
        Assert.assertEquals(1, expensiveMethods.getNumTimesCalled());
    }

Using @Cache on a Constructor

Caching on a constructor can limit the number of times a service is created. The example class called ExpensiveConstructor is in the PerLookup scope. However, since @Cache has been place on its constructor, it will in fact only get created when different input parameters are given to the constructor. Static fields are used in ExpensiveConstructor to keep track of how many times the service has been created. Here is the ExpensiveConstructor service:

@Service @PerLookup
public class ExpensiveConstructor {
    private static int numTimesConstructed;
    private final int multiplier;
    
    @Inject @Cache
    public ExpensiveConstructor(int multiplier) {
        // Very expensive operation
        this.multiplier = multiplier * 2;
        numTimesConstructed++;
    }
    
    public int getComputation() {
        return multiplier;
    }
    
    public static void clear() {
        numTimesConstructed = 0;
    }
    
    public static int getNumTimesConstructed() {
        return numTimesConstructed;
    }
}

The ExpensiveConstructor class takes an integer input parameter. In order to create an integer input that can change values there is an implementation of Factory called InputFactory which produces integers. The provide method of InputFactory changes the value it returns based on how it is currently configured. Here is the code for InputFactory:

@Service
public class InputFactory implements Factory<Integer> {
    private int input;

    @PerLookup
    public Integer provide() {
        return input;
    }

    public void dispose(Integer instance) {
    }
    
    public void setInput(int input) {
        this.input = input;
    }
}

The test code for this demonstrates how to change the input parameter for the ExpensiveConstructor service. The test then uses the static methods to show that the service is only created when the input parameter changes, even though the service is in the PerLookup scope.

    @Test
    public void testConstructorsAreIntercepted() {
        InputFactory inputFactory = testLocator.getService(InputFactory.class);
        inputFactory.setInput(2);
        
        ExpensiveConstructor instanceOne = testLocator.getService(ExpensiveConstructor.class);
        
        int computation = instanceOne.getComputation();
        Assert.assertEquals(4, computation);
        
        Assert.assertEquals(1, ExpensiveConstructor.getNumTimesConstructed());
        
        ExpensiveConstructor instanceTwo = testLocator.getService(ExpensiveConstructor.class);
        
        computation = instanceTwo.getComputation();
        Assert.assertEquals(4, computation);
        
        // Amazingly, the object was NOT recreated:
        Assert.assertEquals(1, ExpensiveConstructor.getNumTimesConstructed());
        
        // Further proof that it was not recreated:
        Assert.assertTrue(instanceOne == instanceTwo);
        
        // Now change the input parameter
        inputFactory.setInput(8);
        
        ExpensiveConstructor instanceThree = testLocator.getService(ExpensiveConstructor.class);
        
        computation = instanceThree.getComputation();
        Assert.assertEquals(16, computation);
        
        Assert.assertFalse(instanceOne.equals(instanceThree));
    }

AOP Requirements

In order to use method interceptors proxies are used. Therefore services that use method interceptors must not be final nor have any final methods. Proxies must be supported on the platform on which hk2 is running. Constructor interception does NOT use proxies so these limitations do not extend to constructor injection. Any method to be intercepted must be public, protected or package visibility. Private methods will not be intercepted. Constructors to be intercepted can have any visibility.

The proxies created for method interception (as opposed to those created for proxiable scopes) do not implement the ProxyCtl interface. Instead they implement the AOPProxyCtl interface, which allows access to the underlying descriptor for the service whose instance was proxied.

Interception in general is only supported when HK2 is constructing the services itself. In particular services created via a Factory can not use AOP nor can services that come from third-parties. Any constant service can not use interception as HK2 did not create the service.

Conclusion

HK2 AOP can be used to solve many cross-cutting concerns. Because it uses AOP Alliance interceptors the interceptors developed for other systems may be appropriate in HK2 as well.