DIY Annotations
Since Java 5 there have been annotations in Java. I wanted to make my own annotation just to see what it takes. However, I found out that they were just interfaces.
There is the rub
Interfaces have no teeth behind them. Some piece of code has to implement it. I figured this is where the rubber hits the road and I really find a way to do this.
To start, I would need a purpose
I picked one recent hot topic, caching. I didn’t want to implement JSR 109(JCache) but I didn’t want to do the typical “Hello World” either. I picked implementing two annotations, one without any parameters and one with a parameter. I also needed a caching provider. Might as well bring a real caching library to the mix if I am going to do this. It also follows my design philosophy to use products/libraries to reach a goal instead of home spinning everything. After careful consideration, I chose hazelcast to be my caching engine. It is the fastest on the market and it is free.
More Decisions
After my purpose was chosen, I still needed to find out how to put teeth behind them. After some digging around I found two methods:
Reflection
Almost every time I have used reflection, I have felt sorry for making such a clunky piece of code. Plus, to do it the way I would like, I would have to create my own framework. Sounds like a lot of work for two annotations.
Aspect Oriented Programming(AOP)
This was a perfect fit for what I wanted to do. AOP deals in reducing boilerplate code into a single place. This would be convenient and dovetails into caching because caching breaks down into the following steps:
- Check to see if this situation was done before.
- If so:
- retrieve the stored result
- if not:
- run the function
- store the result
- return the result
That maybe an oversimplification but in a nut shell it is true. Like in all things, the devil is in the details.
Meanwhile, Back at the AOP Ranch
While I knew AOP was the place for me, I did not know much about it. I found that Spring has an AOP library and that a well known library is AspectJ. AspectJ is unfamiliar to me and needs a runtime engine to work. I am much more familiar with Spring so I picked it. As I dug into Spring’s AOP, I found that I had to delve into AspectJ’s annotations so I was stuck with AspectJ in some form or fashion anyway.
New Concepts, New Vocabulary
Writing aspects aren’t like writing objects. They are objects but not really so, of course, a new set of terms are needed. The ones I used are in the Spring AOP Documentation
I really needed to read the page a couple of times to grasp what is being said. One is highly recommended to do the same or the rest of the post is going to sound like gibberish.
What Makes the Pointcut and How to Advise it
The pointcut design was easy since I was only interested in methods that had the annotation. The advise it needed was the around advice because I needed to be able to circumvent calling the method if there was a matching call already done.
Finally the Code
Maven Pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.darylmathison</groupId> <artifactId>annotation-implementation</artifactId> <version>1.0-SNAPSHOT</version> <properties> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <spring.version>4.2.4.RELEASE</spring.version> </properties> <description> This project is an example of how to implement an annotation via Spring AOP. </description> <scm> <url>https://github.com/darylmathison/annotation-implementation-example.git</url> <connection>scm:git:https://github.com/darylmathison/annotation-implementation-example.git</connection> <developerConnection>scm:git:git@github.com:darylmathison/annotation-implementation-example.git</developerConnection> </scm> <issueManagement> <system>GitHub</system> <url>https://github.com/darylmathison/annotation-implementation-example/issues</url> </issueManagement> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>${spring.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.8.8</version> </dependency> <dependency> <groupId>com.hazelcast</groupId> <artifactId>hazelcast</artifactId> <version>3.6</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> </dependencies> <reporting> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-project-info-reports-plugin</artifactId> <version>2.7</version> <reportSets> <reportSet> <reports> <report>dependencies</report> <report>index</report> <report>project-team</report> <report>issue-tracking</report> <report>scm</report> </reports> </reportSet> </reportSets> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-report-plugin</artifactId> <version>2.18.1</version> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-javadoc-plugin</artifactId> <version>2.10.3</version> <reportSets> <reportSet> <reports> <report>javadoc</report> <report>test-javadoc</report> </reports> </reportSet> </reportSets> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jxr-plugin</artifactId> <version>2.5</version> <configuration> <linkJavadoc>true</linkJavadoc> </configuration> <reportSets> <reportSet> <reports> <report>jxr</report> <report>test-jxr</report> </reports> </reportSet> </reportSets> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-changelog-plugin</artifactId> <version>2.3</version> <configuration> <type>range</type> <range>90</range> </configuration> </plugin> </plugins> </reporting> </project>
The Annotations
CacheMe
Cute name for a caching annotation, right?
package com.darylmathison.ai.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Created by Daryl on 2/19/2016. */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) public @interface CacheMe { }
CacheMeNow
package com.darylmathison.ai.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Created by Daryl on 2/19/2016. */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) public @interface CacheMeNow { String key(); }
Spring Configuration
I decided to use Java based configuration instead of XML like I normally use for a change of pace. The EnableAspectJAutoProxy annotation is key to getting Spring AOP to start working. I was beside myself until I read this about this little jewel. Sometimes it is the easiest thing that burns a day.
AppConfig
package com.darylmathison.ai.config; import com.darylmathison.ai.cache.CacheAspect; import com.darylmathison.ai.service.FibonacciService; import com.darylmathison.ai.service.FibonacciServiceImpl; import com.hazelcast.config.Config; import com.hazelcast.config.EvictionPolicy; import com.hazelcast.config.MapConfig; import com.hazelcast.core.Hazelcast; import com.hazelcast.core.HazelcastInstance; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; import java.util.HashMap; import java.util.Map; /** * Created by Daryl on 2/20/2016. */ @Configuration @ComponentScan(basePackages = "com.darylmathison.ai") @EnableAspectJAutoProxy public class AppConfig { @Bean public Map<String, Object> cache() { Config config = new Config(); MapConfig mapConfig = new MapConfig(); mapConfig.setEvictionPercentage(50); mapConfig.setEvictionPolicy(EvictionPolicy.LFU); mapConfig.setTimeToLiveSeconds(300); Map<String, MapConfig> mapConfigMap = new HashMap<>(); mapConfigMap.put("cache", mapConfig); config.setMapConfigs(mapConfigMap); HazelcastInstance instance = Hazelcast.newHazelcastInstance(config); return instance.getMap("cache"); } @Bean public FibonacciService fibonacci() { return new FibonacciServiceImpl(); } @Bean public CacheAspect cacheAspect() { return new CacheAspect(); } }
Service Code
Classic Spring based design needs a service right? Because Spring uses proxies to implement their AOP, it is highly advised to define an interface for the annotated class to implement.
FibonacciService
package com.darylmathison.ai.service; /** * Created by Daryl on 2/20/2016. */ public interface FibonacciService { long calculate(int rounds); long calculateWithKey(int rounds); }
FibonacciServiceImpl
package com.darylmathison.ai.service; import com.darylmathison.ai.annotation.CacheMe; import com.darylmathison.ai.annotation.CacheMeNow; /** * Created by Daryl on 2/20/2016. */ public class FibonacciServiceImpl implements FibonacciService { @Override @CacheMe public long calculate(int rounds) { return sharedCalculate(rounds); } @Override @CacheMeNow(key = "now") public long calculateWithKey(int rounds) { return sharedCalculate(rounds); } private static long sharedCalculate(int rounds) { long[] lastTwo = new long[] {1, 1}; for(int i = 0; i < rounds; i++) { long last = lastTwo[1]; lastTwo[1] = lastTwo[0] + lastTwo[1]; lastTwo[0] = last; } return lastTwo[1]; } }
AOP Stuff
This is heart of the annotation implementation. Everything else is support to do the source that follows.
SystemArch
According to Spring documentation, centralizing the pointcut definitions are a good idea.
package com.darylmathison.ai.cache; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; /** * Created by Daryl on 2/20/2016. */ @Aspect public class SystemArch { @Pointcut("@annotation(com.darylmathison.ai.annotation.CacheMe)") public void cacheMeCut() { } @Pointcut("@annotation(com.darylmathison.ai.annotation.CacheMeNow)") public void cacheMeNowCut() { } }
CacheAspect
The Around annotations take the full method names of the pointcut class to define what to advise. The advice for the CacheMeNow annotation includes an extra condition so the annotation can be defined so the key parameter can be read. There is a design bug in CacheMeNow that is revealed in the test code.
package com.darylmathison.ai.cache; import com.darylmathison.ai.annotation.CacheMeNow; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.beans.factory.annotation.Autowired; import java.util.Map; /** * Created by Daryl on 2/20/2016. */ @Aspect public class CacheAspect { @Autowired private Map<String, Object> cache; @Around("com.darylmathison.ai.cache.SystemArch.cacheMeCut()") public Object simpleCache(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { StringBuffer keyBuffer = new StringBuffer(); for(Object o: proceedingJoinPoint.getArgs()) { keyBuffer.append(o.hashCode()); } String key = keyBuffer.toString(); Object ret = cache.get(key); if(ret == null) { ret = proceedingJoinPoint.proceed(); cache.put(key, ret); } return ret; } @Around("com.darylmathison.ai.cache.SystemArch.cacheMeNowCut() && @annotation(cacheMeNow)") public Object simpleCacheWithParam(ProceedingJoinPoint proceedingJoinPoint, CacheMeNow cacheMeNow) throws Throwable { Object ret = cache.get(cacheMeNow.key()); if(ret == null) { ret = proceedingJoinPoint.proceed(); cache.put(cacheMeNow.key(), ret); } return ret; } }
Test Code
Driver code to show that the annotations do cause caching.
FibonacciTest
package com.darylmathison.ai.service; import com.darylmathison.ai.config.AppConfig; import org.junit.Assert; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; /** * Created by Daryl on 2/20/2016. */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {AppConfig.class}) public class FibonacciTest { private static final int ROUNDS = 12; private static final long ANSWER = 377; @Autowired private FibonacciService fibonacci; @org.junit.Test public void testCalculate() throws Exception { long start = System.currentTimeMillis(); Assert.assertEquals(ANSWER, fibonacci.calculate(ROUNDS)); long middle = System.currentTimeMillis(); Assert.assertEquals(ANSWER, fibonacci.calculate(ROUNDS)); long end = System.currentTimeMillis(); Assert.assertTrue((end - middle) < (middle - start)); } @org.junit.Test public void testCalculateWithKey() throws Exception { Assert.assertEquals(ANSWER, fibonacci.calculateWithKey(ROUNDS)); // This test should not pass Assert.assertEquals(ANSWER, fibonacci.calculateWithKey(13)); } }
Conclusion
Annotations do not have to be hard to implement. Using AOP programming, I was able to implmement two annotations with little coding.
Reference: | DIY Annotations from our JCG partner Daryl Mathison at the Daryl Mathison’s Java Blog blog. |