【问题标题】:Around annotion executed twice using WebFlux使用 WebFlux 执行两次周围注释
【发布时间】:2021-06-03 15:25:34
【问题描述】:

在将 AOP 与 AspectJ 一起使用时,我遇到了一种奇怪的行为。

基本上 @Around 方法被调用一次或两次,在尝试调试时我找不到它执行两次的原因(我的意思是触发该方法的第二次执行的原因)

这里有一些代码:

@Aspect
@Slf4j
public class ReactiveRedisCacheAspect {
  @Pointcut("@annotation(com.xxx.xxx.cache.aop.annotations.ReactiveRedisCacheable)")
  public void cacheablePointCut() {}
      
  @Around("cacheablePointCut()")
  public Object cacheableAround(final ProceedingJoinPoint proceedingJoinPoint) {
     log.debug("ReactiveRedisCacheAspect cacheableAround.... - {}",  proceedingJoinPoint);
    MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature();
    Method method = methodSignature.getMethod();
    Class<?> returnTypeName = method.getReturnType();
    Duration duration = Duration.ofHours(getDuration(method));
    String redisKey = getKey(method, proceedingJoinPoint);

    if (returnTypeName.isAssignableFrom(Flux.class)) {
        log.debug("returning Flux");
        return cacheRepository.hasKey(redisKey)
                .filter(found -> found)
                .flatMapMany(found -> cacheRepository.findByKey(redisKey))
                .flatMap(found -> saveFlux(proceedingJoinPoint, redisKey, duration));
    } else if (returnTypeName.isAssignableFrom(Mono.class)) {
        log.debug("Returning Mono");
        return cacheRepository.hasKey(redisKey)
                .flatMap(found -> {
                    if (found) {
                        return cacheRepository.findByKey(redisKey);
                    } else {
                        return saveMono(proceedingJoinPoint, redisKey, duration);
                    }
                });
    } else {
        throw new RuntimeException("non reactive object supported (Mono,Flux)");
    }
  }

 private String getKey(final Method method, final ProceedingJoinPoint proceedingJoinPoint) {
        ReactiveRedisCacheable annotation = method.getAnnotation(ReactiveRedisCacheable.class);
        String cacheName = annotation.cacheName();
        String key = annotation.key();
        cacheName = (String) AspectSupportUtils.getKeyValue(proceedingJoinPoint, cacheName);
        key = (String) AspectSupportUtils.getKeyValue(proceedingJoinPoint, key);
        return cacheName + "_" + key;
    }

}
public class AspectSupportUtils {

    private static final ExpressionEvaluator evaluator = new ExpressionEvaluator();

    public static Object getKeyValue(JoinPoint joinPoint, String keyExpression) {
        if (keyExpression.contains("#") || keyExpression.contains("'")) {
            return getKeyValue(joinPoint.getTarget(), joinPoint.getArgs(), joinPoint.getTarget().getClass(),
                    ((MethodSignature) joinPoint.getSignature()).getMethod(), keyExpression);
        }
        return keyExpression;
    }

    private static Object getKeyValue(Object object, Object[] args, Class<?> clazz, Method method, String keyExpression) {
        if (StringUtils.hasText(keyExpression)) {
            EvaluationContext evaluationContext = evaluator.createEvaluationContext(object, clazz, method, args);
            AnnotatedElementKey methodKey = new AnnotatedElementKey(method, clazz);
            return evaluator.key(keyExpression, methodKey, evaluationContext);
        }
        return SimpleKeyGenerator.generateKey(args);
    }
}
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ReactiveRedisCacheable {
  String key();
  String cacheName();
  long duration() default 1L;
}
@RestController
@RequestMapping("api/pub/v1")
public class TestRestController{

@ReactiveRedisCacheable(cacheName = "test-cache", key = "#name", duration = 1L)
    @GetMapping(value = "test")
    public Mono<String> getName(@RequestParam(value = "name") String name){
        return Mono.just(name);
    }
}
@Configuration
public class Config {
   @Bean
    public ReactiveRedisCacheAspect reactiveRedisCache (ReactiveRedisCacheAspect reactiveRedisCacheAspect) {
        return reactiveRedisCacheAspect;
    }
}

日志:

ReactiveRedisCacheAspect cacheableAround.... - {}execution(Mono com.abc.def.xxx.rest.TestRestcontroller.getName(String))
2021-06-04 15:36:23.096  INFO [fo-bff,f688025287be7e7c,f688025287be7e7c] 20060 --- [ctor-http-nio-3] c.m.s.c.a.i.ReactiveRedisCacheAspect     : Returning Mono
2021-06-04 15:36:23.097  INFO [fo-bff,f688025287be7e7c,f688025287be7e7c] 20060 --- [ctor-http-nio-3] c.m.s.c.repository.CacheRepositoryImpl   : searching key: (bff_pippo)
ReactiveRedisCacheAspect cacheableAround.... - {}execution(Mono com.abc.def.xxx.rest.TestRestcontroller.getName(String))
2021-06-04 15:36:23.236  INFO [fo-bff,f688025287be7e7c,f688025287be7e7c] 20060 --- [ioEventLoop-7-2] c.m.s.c.a.i.ReactiveRedisCacheAspect     : Returning Mono
2021-06-04 15:36:23.236  INFO [fo-bff,f688025287be7e7c,f688025287be7e7c] 20060 --- [ioEventLoop-7-2] c.m.s.c.repository.CacheRepositoryImpl   : searching key: (bff_pippo)
2021-06-04 15:36:23.250  INFO [fo-bff,f688025287be7e7c,f688025287be7e7c] 20060 --- [ioEventLoop-7-2] c.m.s.c.repository.CacheRepositoryImpl   : saving obj: (key:bff_pippo) (expiresIn:3600s)
2021-06-04 15:36:23.275  INFO [fo-bff,f688025287be7e7c,f688025287be7e7c] 20060 --- [ioEventLoop-7-2] c.m.s.c.repository.CacheRepositoryImpl   : saving obj: (key:bff_pippo) (expiresIn:3600s)

到目前为止,我预计 cacheableAround 只会执行一次,但发生的事情有点奇怪,如果对象存在于 redis 上,则该方法仅执行一次,但如果不存在,则该方法执行两次没有意义,而且应该是管理方法内部做什么的业务逻辑。

提前致谢!

【问题讨论】:

  • 具有完整代码的可重现测试用例将有助于避免大量猜测工作。当对象不在缓存中时会发生什么?我认为它会尝试满足切入点表达式的回退。
  • 行为非常简单,如果对象存在于缓存中,则返回它,否则保存它。正如我之前所说,这是业务逻辑的一部分,它不应该涉及“调用者”。没有回退,因为所有内容都封装在该方法中。我一直在查看源代码,或多或少的缓存注释与它实际上让我感到惊讶的是相同的,并且如果它被触发两次的方法和它不是一种感知而是一个真实的事实,我大多会感到困惑,而且我可以看到相同的保存操作在 Redis 上进行了两次,而查找操作只进行了一次。
  • 某事应该触发目标方法调用两次,这会导致意外的建议。您是否尝试在建议方法处设置断点并检查调用堆栈以验证行为?
  • 代码不完整。你能分享一下getKey(method, proceedingJoinPoint)在做什么吗?分享前请检查代码完整性
  • AOP 不是问题。你的老板不是问题。我支持他的想法,即集中维护这个横切关注点的逻辑,而不是在所有现有和未来的服务中分散和重复它,这将是维护的噩梦。如果您使用它一段时间,AOP 并不难。就像每一个新范式一样,这只是一开始很困难。 “一开始可能看起来很困难,但所有事情一开始都很困难。”宫本武藏,五环之书

标签: spring-webflux aop aspectj spring-aop


【解决方案1】:

您没有提到您是通过加载或编译时编织还是简单地使用 Spring AOP 来使用本机 AspectJ。因为我在你的方面没有看到 @Component 注释,所以它可能是原生的 AspectJ,除非你通过配置类或 XML 中的 @Bean 工厂方法配置你的 bean。

假设您使用完整的 AspectJ,来自 Spring AOP 的新手的一个常见问题是,他们不习惯 AspectJ 不仅拦截 execution 连接点,而且还拦截 call 连接点。这导致表面上认为相同的连接点被截获两次。但实际上,它是一次方法调用(在进行调用的类中)和一次方法执行(在目标方法所在的类中)。这很容易确定是否在您的建议方法开始时您只是记录连接点。在你的情况下:

System.out.println(proceedingJoinPoint);

如果然后在控制台上你看到类似的东西

call(public void org.acme.MyClass.myMethod())
execution(public void org.acme.MyClass.myMethod())

那么你就知道发生了什么。

如果您使用 Spring AOP,可能是方面或 Redis 缓存行为的问题与您的预期不同。

【讨论】:

  • 切面通过配置类中的@Bean注解。查看 Redis 上的监视器,每当我必须保存时,我都可以正确看到两个“设置”请求,如果对象已经存在于缓存中,则只有一个“获取”请求,因此我认为这不是一种看法,而是一个正确的事实.还查看调试,我可以看到在一种情况下,当前状态变量设置为 1,即当对象在缓存中时,如果不是相同的变量设置为 2(在这种情况下,该方法被执行两次)。
  • 你很高兴,可以看到这一切。我什么也看不到,而且您的示例代码不构成MCVE。所以就像 R.G 已经说过的:请提供一个。您的解释是不可测试或可调试的。如果您在这里需要帮助,请确保让其他人能够真正帮助您。没有人愿意在这里根据不完整的事实进行猜测和推测。我已经开始后悔了。
  • 我为之前的回答道歉。我即将上传更具体的信息,这些信息可以为我所面临的问题提供更广泛的观点。感谢您的帮助!
猜你喜欢
  • 1970-01-01
  • 2016-06-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多