@Recover 方法未被 Spring AOP 建议拦截
@Recover method not intercepted by Spring AOP advice
在使用 Spring/Java 和面向方面的编程编写代码时,我遇到了一个问题。
在服务 class 中,我有使用 @Retryable 的重试方法和使用 @Recover 的恢复方法。
这两种方法中的每一种都附加到方面。
Retryable 方法 - TestProcessService 中的“triggerJob”附加到 TestAspect class 中的这些方法 - beforeTestTriggerJobsAdvice、afterTestTriggerJobsAdvice、onErrorTestTriggerJobsAdvice。
他们都工作正常,并在正确的时间被触发。
问题陈述:
恢复方法 - TestProcessService 中的“恢复”附加到 TestAspect class 中的这些方法 - beforeRecoveryTestJobsAdvice、onErrorRecoveryTestTriggerJobsAdvice 和 afterRecoveryTestTriggerJobsAdvice。
但是一旦代码到达 TestProcessService 中的恢复方法,就会调用这些方面的方法NONE。
代码如下:
SCHEDULER CLASS(定时触发TEST_MyProcessServiceclass里面的方法)
@Slf4j
@Component
public class TEST_ScheduledProcessPoller {
private final TEST_MyProcessService MyProcessService;
private final MyServicesConfiguration MyServicesConfiguration;
public TEST_ScheduledProcessPoller(TEST_MyProcessService MyProcessService,
MyServicesConfiguration MyServicesConfiguration) {
this.MyProcessService = MyProcessService;
this.MyServicesConfiguration = MyServicesConfiguration;
}
@Scheduled(cron = "0 0/2 * * * *")
public void scheduleTaskWithFixedDelay() {
try {
log.info("scheduleTaskWithFixedDelay");
this.triggerMyJobs(true);
} catch (Exception e) {
log.error(e.getMessage());
}
}
protected void triggerMyJobs(boolean isDaily) throws Exception {
log.info("triggerMyJobs");
MyServiceType serviceType = this.MyServicesConfiguration.getMy();
this.MyProcessService.triggerJob(serviceType, isDaily, 1L);
}
}
服务 CLASS:
@Slf4j
@Service
public class TEST_MyProcessService {
@Retryable(maxAttemptsExpression = "${api.retry.limit}", backoff = @Backoff(delayExpression = "${api.retry.max-interval}"))
public void triggerJob(MyServiceType MyServiceType, boolean isDaily, long eventId) {
// Some code here that can throw exceptions.
log.info("triggerJob");
throw new RuntimeException("triggerJob");
}
@Recover
public void recover(MyServiceType MyServiceType, boolean isDaily, long eventId) {
log.info("recover");
// Some code here that can throw exceptions.
throw new RuntimeException();
}
}
方面CLASS:
@Component
@Slf4j
public class TEST_MyAspect {
@Pointcut("execution(* packgName.otherProj.services.TEST_MyProcessService.triggerJob(..))")
public void MyTriggerJobs() {
}
@Pointcut("execution(* packgName.otherProj.services.TEST_MyProcessService.recover(..))")
public void MyRecoverJobs() {
}
@Before("MyTriggerJobs()")
public void beforeMyTriggerJobsAdvice(JoinPoint joinPoint) {
log.info("log beforeMyTriggerJobsAdvice");
}
@AfterReturning("MyTriggerJobs()")
public void afterMyTriggerJobsAdvice(JoinPoint joinPoint) {
log.info("log afterMyTriggerJobsAdvice");
}
@AfterThrowing(value = "MyTriggerJobs()", throwing = "error")
public void onErrorMyTriggerJobsAdvice(JoinPoint joinPoint, Throwable error) {
log.info("log onErrorMyTriggerJobsAdvice");
}
@Before("MyRecoverJobs()")
public void beforeMyRecoverJobsAdvice(JoinPoint joinPoint) {
log.info("log beforeMyRecoverJobsAdvice");
}
@AfterThrowing(value = "MyRecoverJobs()", throwing = "error")
public void onErrorRecoveryMyTriggerJobsAdvice(JoinPoint joinPoint, Throwable error) {
log.info("log onErrorRecoveryMyTriggerJobsAdvice");
}
@AfterReturning("MyRecoverJobs()")
public void afterRecoveryMyTriggerJobsAdvice(JoinPoint joinPoint) {
log.info("log afterRecoveryMyTriggerJobsAdvice");
}
}
日志输出:
2021-06-02 20:56:00.016 INFO [,60b7f0602b2ecb0deefc04d6840b6274,eefc04d6840b6274,true] 92605 --- [ scheduling-1] c.c.p.r.c.TEST_ScheduledProcessPoller : scheduleTaskWithFixedDelay
2021-06-02 20:56:00.016 INFO [,60b7f0602b2ecb0deefc04d6840b6274,eefc04d6840b6274,true] 92605 --- [ scheduling-1] c.c.p.r.c.TEST_ScheduledProcessPoller : triggerBdaJobs
2021-06-02 20:56:00.051 INFO [,60b7f0602b2ecb0deefc04d6840b6274,eefc04d6840b6274,true] 92605 --- [ scheduling-1] c.c.p.r.component.aspect.TEST_BdaAspect : log beforeBdaTriggerJobsAdvice
2021-06-02 20:56:00.060 INFO [,60b7f0602b2ecb0deefc04d6840b6274,eefc04d6840b6274,true] 92605 --- [ scheduling-1] c.c.p.r.services.TEST_BdaProcessService : triggerJob
2021-06-02 20:56:00.061 INFO [,60b7f0602b2ecb0deefc04d6840b6274,eefc04d6840b6274,true] 92605 --- [ scheduling-1] c.c.p.r.component.aspect.TEST_BdaAspect : log onErrorBdaTriggerJobsAdvice
2021-06-02 20:56:05.065 INFO [,60b7f0602b2ecb0deefc04d6840b6274,eefc04d6840b6274,true] 92605 --- [ scheduling-1] c.c.p.r.component.aspect.TEST_BdaAspect : log beforeBdaTriggerJobsAdvice
2021-06-02 20:56:05.066 INFO [,60b7f0602b2ecb0deefc04d6840b6274,eefc04d6840b6274,true] 92605 --- [ scheduling-1] c.c.p.r.services.TEST_BdaProcessService : triggerJob
2021-06-02 20:56:05.066 INFO [,60b7f0602b2ecb0deefc04d6840b6274,eefc04d6840b6274,true] 92605 --- [ scheduling-1] c.c.p.r.component.aspect.TEST_BdaAspect : log onErrorBdaTriggerJobsAdvice
2021-06-02 20:56:10.070 INFO [,60b7f0602b2ecb0deefc04d6840b6274,eefc04d6840b6274,true] 92605 --- [ scheduling-1] c.c.p.r.component.aspect.TEST_BdaAspect : log beforeBdaTriggerJobsAdvice
2021-06-02 20:56:10.070 INFO [,60b7f0602b2ecb0deefc04d6840b6274,eefc04d6840b6274,true] 92605 --- [ scheduling-1] c.c.p.r.services.TEST_BdaProcessService : triggerJob
2021-06-02 20:56:10.070 INFO [,60b7f0602b2ecb0deefc04d6840b6274,eefc04d6840b6274,true] 92605 --- [ scheduling-1] c.c.p.r.component.aspect.TEST_BdaAspect : log onErrorBdaTriggerJobsAdvice
2021-06-02 20:56:10.070 INFO [,60b7f0602b2ecb0deefc04d6840b6274,eefc04d6840b6274,true] 92605 --- [ scheduling-1] c.c.p.r.services.TEST_BdaProcessService : recover
2021-06-02 20:56:10.070 ERROR [,60b7f0602b2ecb0deefc04d6840b6274,eefc04d6840b6274,true] 92605 --- [ scheduling-1] c.c.p.r.c.TEST_ScheduledProcessPoller : null
我不是 Spring 用户,但对所有 AOP 感兴趣,包括 AspectJ 和 Spring AOP。我喜欢你的小拼图。感谢您的 MCVE,我能够重现问题并进行调试。这是一个完美的例子,说明了为什么 MCVE 比简单地发布一堆代码片段要优越得多。所以谢谢你,请保持这种提问方式。
在调试器中查看情况时,您会看到当方面进入 triggerJob
时,在某个时候我们在方法 AnnotationAwareRetryOperationsInterceptor.invoke
中,我们有以下代码:
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
MethodInterceptor delegate = getDelegate(invocation.getThis(), invocation.getMethod());
if (delegate != null) {
return delegate.invoke(invocation);
}
else {
return invocation.proceed();
}
}
此时 Spring 选择构造稍后将用于调用 recover
的委托。它是用目标对象 invocation.getThis()
构造的,它指向原始对象,即 TEST_BdaProcessService
实例。此时,代码可以简单地使用 invocation.getProxy()
代替,它将指向 AOP 代理,即 TEST_BdaProcessService$$EnhancerBySpringCGLIB$f8076ac
实例。问题是目标对象引用被传递到 recover
被调用的点,而那个点对应的恢复器实例只知道目标对象,不再有对应的代理对象的线索。
当我实验性地将代理分配给委托作为目标时,调用了您的建议方法。
所以我们在这里讨论的是 Spring 限制。我不知道这是为了避免任何其他相关问题而有意选择还是仅仅是疏忽。
更新: 我为您创建了 Spring Retry issue #244。你想订阅它,所以你可以发现 if/when 它会被修复。
更新 2: 问题已修复,已合并到主分支中,可能会成为即将发布的 1.3.2 版本的一部分,无论何时释放。现在,您可以直接克隆 Spring 重试,自己构建并使用快照。我针对您的 MCVE 进行了重新测试,该方面现在按恢复方法的预期启动。
在使用 Spring/Java 和面向方面的编程编写代码时,我遇到了一个问题。 在服务 class 中,我有使用 @Retryable 的重试方法和使用 @Recover 的恢复方法。
这两种方法中的每一种都附加到方面。 Retryable 方法 - TestProcessService 中的“triggerJob”附加到 TestAspect class 中的这些方法 - beforeTestTriggerJobsAdvice、afterTestTriggerJobsAdvice、onErrorTestTriggerJobsAdvice。 他们都工作正常,并在正确的时间被触发。
问题陈述: 恢复方法 - TestProcessService 中的“恢复”附加到 TestAspect class 中的这些方法 - beforeRecoveryTestJobsAdvice、onErrorRecoveryTestTriggerJobsAdvice 和 afterRecoveryTestTriggerJobsAdvice。
但是一旦代码到达 TestProcessService 中的恢复方法,就会调用这些方面的方法NONE。
代码如下:
SCHEDULER CLASS(定时触发TEST_MyProcessServiceclass里面的方法)
@Slf4j
@Component
public class TEST_ScheduledProcessPoller {
private final TEST_MyProcessService MyProcessService;
private final MyServicesConfiguration MyServicesConfiguration;
public TEST_ScheduledProcessPoller(TEST_MyProcessService MyProcessService,
MyServicesConfiguration MyServicesConfiguration) {
this.MyProcessService = MyProcessService;
this.MyServicesConfiguration = MyServicesConfiguration;
}
@Scheduled(cron = "0 0/2 * * * *")
public void scheduleTaskWithFixedDelay() {
try {
log.info("scheduleTaskWithFixedDelay");
this.triggerMyJobs(true);
} catch (Exception e) {
log.error(e.getMessage());
}
}
protected void triggerMyJobs(boolean isDaily) throws Exception {
log.info("triggerMyJobs");
MyServiceType serviceType = this.MyServicesConfiguration.getMy();
this.MyProcessService.triggerJob(serviceType, isDaily, 1L);
}
}
服务 CLASS:
@Slf4j
@Service
public class TEST_MyProcessService {
@Retryable(maxAttemptsExpression = "${api.retry.limit}", backoff = @Backoff(delayExpression = "${api.retry.max-interval}"))
public void triggerJob(MyServiceType MyServiceType, boolean isDaily, long eventId) {
// Some code here that can throw exceptions.
log.info("triggerJob");
throw new RuntimeException("triggerJob");
}
@Recover
public void recover(MyServiceType MyServiceType, boolean isDaily, long eventId) {
log.info("recover");
// Some code here that can throw exceptions.
throw new RuntimeException();
}
}
方面CLASS:
@Component
@Slf4j
public class TEST_MyAspect {
@Pointcut("execution(* packgName.otherProj.services.TEST_MyProcessService.triggerJob(..))")
public void MyTriggerJobs() {
}
@Pointcut("execution(* packgName.otherProj.services.TEST_MyProcessService.recover(..))")
public void MyRecoverJobs() {
}
@Before("MyTriggerJobs()")
public void beforeMyTriggerJobsAdvice(JoinPoint joinPoint) {
log.info("log beforeMyTriggerJobsAdvice");
}
@AfterReturning("MyTriggerJobs()")
public void afterMyTriggerJobsAdvice(JoinPoint joinPoint) {
log.info("log afterMyTriggerJobsAdvice");
}
@AfterThrowing(value = "MyTriggerJobs()", throwing = "error")
public void onErrorMyTriggerJobsAdvice(JoinPoint joinPoint, Throwable error) {
log.info("log onErrorMyTriggerJobsAdvice");
}
@Before("MyRecoverJobs()")
public void beforeMyRecoverJobsAdvice(JoinPoint joinPoint) {
log.info("log beforeMyRecoverJobsAdvice");
}
@AfterThrowing(value = "MyRecoverJobs()", throwing = "error")
public void onErrorRecoveryMyTriggerJobsAdvice(JoinPoint joinPoint, Throwable error) {
log.info("log onErrorRecoveryMyTriggerJobsAdvice");
}
@AfterReturning("MyRecoverJobs()")
public void afterRecoveryMyTriggerJobsAdvice(JoinPoint joinPoint) {
log.info("log afterRecoveryMyTriggerJobsAdvice");
}
}
日志输出:
2021-06-02 20:56:00.016 INFO [,60b7f0602b2ecb0deefc04d6840b6274,eefc04d6840b6274,true] 92605 --- [ scheduling-1] c.c.p.r.c.TEST_ScheduledProcessPoller : scheduleTaskWithFixedDelay
2021-06-02 20:56:00.016 INFO [,60b7f0602b2ecb0deefc04d6840b6274,eefc04d6840b6274,true] 92605 --- [ scheduling-1] c.c.p.r.c.TEST_ScheduledProcessPoller : triggerBdaJobs
2021-06-02 20:56:00.051 INFO [,60b7f0602b2ecb0deefc04d6840b6274,eefc04d6840b6274,true] 92605 --- [ scheduling-1] c.c.p.r.component.aspect.TEST_BdaAspect : log beforeBdaTriggerJobsAdvice
2021-06-02 20:56:00.060 INFO [,60b7f0602b2ecb0deefc04d6840b6274,eefc04d6840b6274,true] 92605 --- [ scheduling-1] c.c.p.r.services.TEST_BdaProcessService : triggerJob
2021-06-02 20:56:00.061 INFO [,60b7f0602b2ecb0deefc04d6840b6274,eefc04d6840b6274,true] 92605 --- [ scheduling-1] c.c.p.r.component.aspect.TEST_BdaAspect : log onErrorBdaTriggerJobsAdvice
2021-06-02 20:56:05.065 INFO [,60b7f0602b2ecb0deefc04d6840b6274,eefc04d6840b6274,true] 92605 --- [ scheduling-1] c.c.p.r.component.aspect.TEST_BdaAspect : log beforeBdaTriggerJobsAdvice
2021-06-02 20:56:05.066 INFO [,60b7f0602b2ecb0deefc04d6840b6274,eefc04d6840b6274,true] 92605 --- [ scheduling-1] c.c.p.r.services.TEST_BdaProcessService : triggerJob
2021-06-02 20:56:05.066 INFO [,60b7f0602b2ecb0deefc04d6840b6274,eefc04d6840b6274,true] 92605 --- [ scheduling-1] c.c.p.r.component.aspect.TEST_BdaAspect : log onErrorBdaTriggerJobsAdvice
2021-06-02 20:56:10.070 INFO [,60b7f0602b2ecb0deefc04d6840b6274,eefc04d6840b6274,true] 92605 --- [ scheduling-1] c.c.p.r.component.aspect.TEST_BdaAspect : log beforeBdaTriggerJobsAdvice
2021-06-02 20:56:10.070 INFO [,60b7f0602b2ecb0deefc04d6840b6274,eefc04d6840b6274,true] 92605 --- [ scheduling-1] c.c.p.r.services.TEST_BdaProcessService : triggerJob
2021-06-02 20:56:10.070 INFO [,60b7f0602b2ecb0deefc04d6840b6274,eefc04d6840b6274,true] 92605 --- [ scheduling-1] c.c.p.r.component.aspect.TEST_BdaAspect : log onErrorBdaTriggerJobsAdvice
2021-06-02 20:56:10.070 INFO [,60b7f0602b2ecb0deefc04d6840b6274,eefc04d6840b6274,true] 92605 --- [ scheduling-1] c.c.p.r.services.TEST_BdaProcessService : recover
2021-06-02 20:56:10.070 ERROR [,60b7f0602b2ecb0deefc04d6840b6274,eefc04d6840b6274,true] 92605 --- [ scheduling-1] c.c.p.r.c.TEST_ScheduledProcessPoller : null
我不是 Spring 用户,但对所有 AOP 感兴趣,包括 AspectJ 和 Spring AOP。我喜欢你的小拼图。感谢您的 MCVE,我能够重现问题并进行调试。这是一个完美的例子,说明了为什么 MCVE 比简单地发布一堆代码片段要优越得多。所以谢谢你,请保持这种提问方式。
在调试器中查看情况时,您会看到当方面进入 triggerJob
时,在某个时候我们在方法 AnnotationAwareRetryOperationsInterceptor.invoke
中,我们有以下代码:
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
MethodInterceptor delegate = getDelegate(invocation.getThis(), invocation.getMethod());
if (delegate != null) {
return delegate.invoke(invocation);
}
else {
return invocation.proceed();
}
}
此时 Spring 选择构造稍后将用于调用 recover
的委托。它是用目标对象 invocation.getThis()
构造的,它指向原始对象,即 TEST_BdaProcessService
实例。此时,代码可以简单地使用 invocation.getProxy()
代替,它将指向 AOP 代理,即 TEST_BdaProcessService$$EnhancerBySpringCGLIB$f8076ac
实例。问题是目标对象引用被传递到 recover
被调用的点,而那个点对应的恢复器实例只知道目标对象,不再有对应的代理对象的线索。
当我实验性地将代理分配给委托作为目标时,调用了您的建议方法。
所以我们在这里讨论的是 Spring 限制。我不知道这是为了避免任何其他相关问题而有意选择还是仅仅是疏忽。
更新: 我为您创建了 Spring Retry issue #244。你想订阅它,所以你可以发现 if/when 它会被修复。
更新 2: 问题已修复,已合并到主分支中,可能会成为即将发布的 1.3.2 版本的一部分,无论何时释放。现在,您可以直接克隆 Spring 重试,自己构建并使用快照。我针对您的 MCVE 进行了重新测试,该方面现在按恢复方法的预期启动。