So I'm doing some exercises on AspectJ for fun and practice and wanted to implement a memoization aspect since this is one of the most usual use cases.
This included using this annotation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Memoized {
String value();
}
To which I'd like to pass a String
representing a Duration
using ISO-8601 seconds-based representation, so, something like:
@Memoized("PT1H30M")
public int memoizedMethod() {
return new Random().nextInt();
}
So, for this to work, I have to create an aspect which holds a Guava cache, and use an around advice that uses the method signature as the key and caches or retrieves from cache the result, I arrived at this:
public aspect Memoize pertarget(memoized(Memoized)) {
private final Cache<String, Object> cache = CacheBuilder.newBuilder().expireAfterWrite(Duration.ofMinutes(1)).build();
public pointcut memoized(Memoized memoized) : call(@Memoized * *.*()) && @annotation(memoized) ;
Object around(Memoized memoized): memoized(memoized) {
var key = thisJoinPoint.getSignature().toShortString();
try {
return cache.get(key, () -> proceed(memoized));
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}
}
This however, doesn't fulfill my needs. As you can see, I'm hardcoding a Duration
of 1 minute in the cache creation. I managed to capture the @Memoized
contents in the pointcut, and could use it in the advice to do a Duration.parse(memoized.value())
, but I need this value at the aspect association, i.e. the pertarget(memoized())
.
I've read the official documentation but I'm still unsure of what the parameter to the pointcut in the aspect association really means, I just put Memoized
there because I saw it in an example and wouldn't compile otherwise.
Is there any way I could just capture the memoized
instance of the annotation there and then use it in the cache instantiation like I can do in the advice?
I think that you are using the wrong cache keys. You should use cache keys which are composed of a representation of method arguments, not method name. Otherwise, you would always get the exact same result from the cache for each method call, no matter which arguments are used.
Here is a little MCVE, using a singleton aspect:
package de.scrum_master.app;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
@Retention(RUNTIME)
@Target(METHOD)
public @interface Memoized {
String value();
}
package de.scrum_master.app;
import org.aspectj.lang.Aspects;
import de.scrum_master.aspect.Memoize;
public class Application {
public static void main(String[] args) {
Application application;
for (int i = 0; i < 3; i++) {
application = new Application();
System.out.println(application.capitalise("foo"));
System.out.println(application.triple(11));
System.out.println(application.capitalise("bar"));
System.out.println(application.triple(22));
}
Aspects.aspectOf(Memoize.class).printCacheStats();
}
@Memoized("PT1M45S")
public String capitalise(String string) {
System.out.println("Capitalising " + string);
return string.toUpperCase();
}
@Memoized("PT1H30M")
public int triple(int i) {
System.out.println("Tripling " + i);
return 3 * i;
}
}
package de.scrum_master.aspect;
import java.time.Duration;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import org.aspectj.lang.SoftException;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import de.scrum_master.app.Memoized;
public aspect Memoize {
private final Map<String, Cache<String, Object>> caches = new HashMap<>();
public pointcut memoized(Memoized memoized) : execution(* *(..)) && @annotation(memoized);
Object around(Memoized memoized): memoized(memoized) {
String cacheID = thisJoinPoint.getSignature().toString();
Cache<String, Object> cache = caches.get(cacheID);
if (cache == null) {
cache = CacheBuilder.newBuilder().recordStats().expireAfterWrite(Duration.parse(memoized.value())).build();
caches.put(cacheID, cache);
}
System.out.println(thisJoinPoint + " -> " + cache);
String args = Arrays.deepToString(thisJoinPoint.getArgs());
try {
return cache.get(cacheID + "#" + args, () -> proceed(memoized));
} catch (ExecutionException e) {
throw new SoftException(e);
}
}
public void printCacheStats() {
caches.entrySet().forEach(entry -> System.out.println(entry.getKey() + " -> " +entry.getValue().stats()));
}
}
As you can see, I added an extra method to the aspect, so we can see some stats on the console. When runing the driver application, we would expect 2 cache misses and 4 hits for each of the two methods. Let us find out what the console log says:
execution(String de.scrum_master.app.Application.capitalise(String)) -> com.google.common.cache.LocalCache$LocalManualCache@4ee285c6
Capitalising foo
FOO
execution(int de.scrum_master.app.Application.triple(int)) -> com.google.common.cache.LocalCache$LocalManualCache@7dc5e7b4
Tripling 11
33
execution(String de.scrum_master.app.Application.capitalise(String)) -> com.google.common.cache.LocalCache$LocalManualCache@4ee285c6
Capitalising bar
BAR
execution(int de.scrum_master.app.Application.triple(int)) -> com.google.common.cache.LocalCache$LocalManualCache@7dc5e7b4
Tripling 22
66
execution(String de.scrum_master.app.Application.capitalise(String)) -> com.google.common.cache.LocalCache$LocalManualCache@4ee285c6
FOO
execution(int de.scrum_master.app.Application.triple(int)) -> com.google.common.cache.LocalCache$LocalManualCache@7dc5e7b4
33
execution(String de.scrum_master.app.Application.capitalise(String)) -> com.google.common.cache.LocalCache$LocalManualCache@4ee285c6
BAR
execution(int de.scrum_master.app.Application.triple(int)) -> com.google.common.cache.LocalCache$LocalManualCache@7dc5e7b4
66
execution(String de.scrum_master.app.Application.capitalise(String)) -> com.google.common.cache.LocalCache$LocalManualCache@4ee285c6
FOO
execution(int de.scrum_master.app.Application.triple(int)) -> com.google.common.cache.LocalCache$LocalManualCache@7dc5e7b4
33
execution(String de.scrum_master.app.Application.capitalise(String)) -> com.google.common.cache.LocalCache$LocalManualCache@4ee285c6
BAR
execution(int de.scrum_master.app.Application.triple(int)) -> com.google.common.cache.LocalCache$LocalManualCache@7dc5e7b4
66
String de.scrum_master.app.Application.capitalise(String) -> CacheStats{hitCount=4, missCount=2, loadSuccessCount=2, loadExceptionCount=0, totalLoadTime=19678400, evictionCount=0}
int de.scrum_master.app.Application.triple(int) -> CacheStats{hitCount=4, missCount=2, loadSuccessCount=2, loadExceptionCount=0, totalLoadTime=252000, evictionCount=0}
Tadaa! 🙂