如何在方面关联中获取切入点上下文?

How to get pointcut context in aspect association?

因此,我正在 AspectJ 上做一些练习,以获得乐趣和练习,并希望实现一个记忆化方面,因为这是最常见的用例之一。

这包括使用此注释

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Memoized {
    String value();
}

我想向其传递一个 String 代表 Duration 使用基于 ISO-8601 秒的表示,所以,类似于:

    @Memoized("PT1H30M")
    public int memoizedMethod() {
        return new Random().nextInt();
    }

所以,为了让它起作用,我必须创建一个包含 Guava 缓存的方面,并使用一个使用方法签名作为键并缓存或从缓存中检索结果的周围建议,我得出了这个:

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);
        }
    }
}

然而,这并不能满足我的需求。如您所见,我在缓存创建中硬编码了 1 分钟的 Duration。我设法在切入点中捕获了 @Memoized 内容,并且可以在建议中使用它来执行 Duration.parse(memoized.value()),但我需要在方面关联中使用此值,即 pertarget(memoized())

我已经阅读了官方文档,但我仍然不确定方面关联中切入点的参数的真正含义,我只是将 Memoized 放在那里,因为我在示例中看到了它并且不会'否则编译。

有什么方法可以让我在那里捕获注释的 memoized 实例,然后像我在建议中那样在缓存实例化中使用它?

我认为您使用了错误的缓存键。您应该使用由方法参数表示组成的缓存键,而不是方法名称。否则,无论使用哪个参数,每次方法调用都会从缓存中得到完全相同的结果。

这里有一点MCVE,使用单例方面:

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()));
  }
}

如您所见,我在方面添加了一个额外的方法,因此我们可以在控制台上看到一些统计信息。运行驱动程序应用程序时,我们预计两种方法各有 2 次缓存未命中和 4 次命中。让我们看看控制台日志说了什么:

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}

太棒了!