During my current assignment I develop business logic for a OSGi system running an Apache Karaf OSGi container. OSGi is an architecture which defines how your entire application is to be structured. In OSGi all code resides in bundles managed by the OSGi runtime. Within a bundle one or more services may be defined. A service communicates with services in other bundles. OSGi defines very strict rules for how bundles, and thereby services, may depend upon each other. These strict rules and explicit dependencies enables OSGi to provide many nice features, such as the ability to replace any part (bundle) of an application in runtime. For those interested I recommend reading a few OSGi resources such as this: http://www.osgi.org/Technology/Wh
I was recently tasked with writing a cache which allow responses between OSGi services to be cached based on the arguments passed. This would have the effect that only the first invocation with a specific set of arguments would trigger the method to be invoked. As well as returning the method invocation result the result would also be stored in a cache. Any subsequent invocations with the same arguments would access the cache and retrieve the last returned value. One fairly straightforward approach to implement this behavior is to create a proxy between the caller and the callee. From now on the caller will be referred to as client, and the service which is invoked by the client will be referred to as server. The cache-proxy which logically resides between the client and the server is responsible for checking and manipulating the cache. The actual procedure of caching the return value of a method invocation is very similar regardless of what the server interface looks like. Because of this, generating the cache-proxy is a good candidate for automation.
To trigger generation of a cache-proxy we opted to use annotations. Methods exposed by a OSGi service interface annotated with a CacheMethodResult annotation would have a cache proxy generated. Any invocations of that method would be intercepted by the cache-proxy. The idea to use annotations to control caching behavior is not new, there exists implementations for both the Guice framework (http://code.google.com/p/gjutil/wiki/CachingAnnotations) and in Spring (http://code.google.com/p/ehcache-spring-annotations/). These implementations can however not be directly used within an OSGi container. The main reason for this is that OSGi specifies very strict rules regarding isolation of class definitions declared within bundles. In practice this means that each bundle maintain it’s own classloader. The rules for sharing of class definitions between bundles is somewhat complicated and more information on the subject can be found here http://moi.vonos.net/java/osgi-classloaders/. Therefore any type of DI-framework needs to consider the special classloader hierarchy that exists within all OSGi containers.
Beyond letting the user control which methods to cache it was also required that the user would be able to configure cache parameters of each cached method separately and that it should be possible to reconfigure each cached method at runtime. In practice this meant that the cache-proxy would need to maintain a reference to a special configuration service provided as a part of the containers core services. Each annotated method would also need to generate a unique configuration key which would be the base of any cache properties. An example: In the final implementation the cache parameter Time To Idle for the cache annotated method getCustomer(String id) in class com.company.package.ExampleClass would have the configuration property key:
Compile time method verification
When using annotations to generate cache proxies we can not specify all restrictions that must be imposed on cache methods using only the normal annotation restrictions. (such as retention scope, annotation target type etc). I.e. simply annotating a method with CachedServiceMethod does not ensure that the method is a valid candidate for caching. The method may for instance not have a return value or it may lack valid arguments which make it impossible or unnecessary to generate a cache for the method. All these possible issues will only be apparent at runtime, even though nothing theoretically prevents us from checking them at compile time. To get around this problem it is possible to use a feature released in java 1.6, annotation processors (http://jcp.org/en/jsr/detail?id=269,
Annotation processors allows us, among other things, to get access to the symbol tree of our java code before the actual compilation to byte code. With this information it is possible to verify many of the requirements of our cached methods at compile time. We can for example verify that a cached method have a return type, that the return type is serializable and that the parameter types of the cache annotated method is valid (for example that they override the default implementation of hashCode since this will be used to generate the cache key). To get all these benefits we must be able to guarantee that the annotation processor will be run at every build. In our case this was easy since we run a maven only setup with a single parent for all our many OSGi service projects. Configuring the annotation processor the be run in the parent pom ensured that all child projects would have their CachedServiceMethod annotations examined during builds.
Cache Proxy implementation
It is possible to imagine several different approaches of intercepting method invocations to check the cache.
- AOP. Here a compile time weaving option such as AspectJ is preferable since this circumvents possible class loader issues.
- Dynamic Proxies. Generating dynamic proxies using Java’s reflection API.
- Interception by OSGi service lookup mechanisms. Whenever a OSGi service wants a reference to another service, it queries a “bundle registry” which contains references to all local OSGi bundles, and thereby services. Using the OSGi API it is possible to make the bundle registry respond with a cache-proxy bundle instead of the actual bundle whenever a service makes a query.
Using dynamic proxies was in our case deemed to be best, primarily since it felt more self-explanatory and easier to understand. However using dynamic proxies in an environment with many separate class loaders is a bit tricky. A dynamic proxy created through an invocation of Java’s reflection API is only specified within the local classloader. If the server were to instantiate the cache-proxy within its classloader scope, the invoking client would not be able to resolve the dynamic proxy which the server would return, generating a ClassDefNotFoundException.
To get around this problem without having to tinker with neither the client or servers classloader hierarchy the dynamic proxy is instantiated in the invoking client bundle. The proxy instance factory is passed a reference to both the server service and a “CacheService”. The proxy factory then generates a proxied version of the server service, a cache-proxy. The CacheService maintains all service caches within in the local OSGi container. All values stored in it is serialized before it’s API is invoked by a cache-proxy instance, which means it does not need to maintain class information of all stored classes. Since the annotation processor enforces rules regarding the serializability of the return value etc, we can be confident that no NotSerializableException will be thrown.
Centralizing the cache also means that the CacheService is the only service which needs to manage cache configuration settings. Only the CacheService bundle is therefore required to listen for configuration change events. It also means it is possible to isolate the actual implementation of the cache from the rest of the system. In our project we have been using EhCache, but this could be changed at runtime.
What will the developer see?
To use the automatic cache generation functionality in our current environment a developer needs to do three things.
- Ensure that the method which is to be cached have the CachedServiceMethod annotation.
- Ensure that the maven project in which the annotated method resides compiles. If the annotations is placed on a method which violates the CacheServiceMethod requirements an error message explaining the violation is generated.
- Instantiate the cache proxy in the invoking bundle by using a static cache factory method exposed by the CacheService API bundle. This is preferably done in the blueprint xml specification. Blueprint is essentially the OSGi version of springs DI features.