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.