Spring AOP:如何在返回类型为 void 的异步方法中重新抛出异常

问题描述 投票:0回答:1

我有以下应用程序(与 Gradle + Spring Boot 相同的应用程序在这里 https://www.dropbox.com/s/vizr5joyhixmdca/demo.zip?dl=0):

Writer.java
包含一些在
@Async
注释的帮助下异步运行的代码。一种方法返回
void
,另一种方法返回
Future
。根据文档,这两种变体都是允许的。

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

import java.util.concurrent.Future;

@Component
@Async("customExecutor")
public class Writer {

    public void write() {
        System.out.println("Writing something");
        throw new RuntimeException("Writer exception");
    }

    public Future<Void> writeFuture() {
        System.out.println("Writing something with future");
        throw new RuntimeException("Writer exception with future");
    }
}

ErrorHandlingThreadPoolExecutor.java
是一个自定义执行器。与
ThreadPoolExecutor
的唯一区别是它的错误处理。
afterExecute
实现与方法的 javadoc 中建议的完全相同。所以这里的想法是当异常发生时打印
"[ERROR] " + ex

import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

@Component("customExecutor")
public class ErrorHandlingThreadPoolExecutor extends ThreadPoolExecutor {

    public ErrorHandlingThreadPoolExecutor() {
        super(1, 1, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
    }

    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);
        if (t == null && r instanceof Future<?>) {
            try {
                ((Future<?>) r).get();
            } catch (CancellationException ce) {
                t = ce;
            } catch (ExecutionException ee) {
                t = ee.getCause();
            } catch (InterruptedException ie) {
                Thread.currentThread().interrupt();
            }
        }
        if (t != null) {
            handleError(t);
        }
    }

    private void handleError(Throwable ex) {
        System.out.println("[ERROR] " + ex);
    }
}

Config.java
支持异步处理+调度。它还按计划调用
writer.write

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;

@Configuration
@EnableScheduling
@EnableAsync
public class Config {

    private final Writer writer;

    public Config(Writer writer) {
        this.writer = writer;
    }

    @Scheduled(fixedRate = 1000)
    public void writeBySchedule() {
        writer.write();
//        writer.writeFuture();
    }
}

当我运行此应用程序时,我看到以下输出:

Writing something
2020-07-14 21:16:33.791 ERROR 19860 --- [pool-1-thread-1] .a.i.SimpleAsyncUncaughtExceptionHandler : Unexpected exception occurred invoking async method: public void com.example.demo.Writer.write()

java.lang.RuntimeException: Writer exception
    at com.example.demo.Writer.write(Writer.java:14) ~[main/:na]
    at com.example.demo.Writer$$FastClassBySpringCGLIB$$cd00988d.invoke(<generated>) ~[main/:na]
    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.2.7.RELEASE.jar:5.2.7.RELEASE]
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:771) ~[spring-aop-5.2.7.RELEASE.jar:5.2.7.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-5.2.7.RELEASE.jar:5.2.7.RELEASE]
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749) ~[spring-aop-5.2.7.RELEASE.jar:5.2.7.RELEASE]
    at org.springframework.aop.interceptor.AsyncExecutionInterceptor.lambda$invoke$0(AsyncExecutionInterceptor.java:115) ~[spring-aop-5.2.7.RELEASE.jar:5.2.7.RELEASE]
    at java.util.concurrent.FutureTask.run(FutureTask.java:266) ~[na:1.8.0_242]
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) ~[na:1.8.0_242]
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) ~[na:1.8.0_242]
    at java.lang.Thread.run(Thread.java:748) ~[na:1.8.0_242]
...

同时,如果我评论

writer.write()
并取消评论
writer.writeFuture()
,我会得到以下信息:

Writing something with future
[ERROR] java.lang.RuntimeException: Writer exception with future
...

后者是我试图通过

ErrorHandlingThreadPoolExecutor
实现的目标。不过我想保留我的方法返回
void
。 我发现我的异常没有达到自定义
ErrorHandlingThreadPoolExecutor.handleError()
方法的原因在这里:https://github.com/spring-projects/spring-framework/blob/master/spring-aop/src/main/java /org/springframework/aop/interceptor/AsyncExecutionAspectSupport.java#L308。该方法在我的自定义方法之前执行,并且似乎无法重新抛出
void
方法的异常。我知道
AsyncConfigurerSupport
类允许自定义异常处理,但异常仍然无法从
AsyncExecutionAspectSupport.handleError()
中逃脱。

总而言之,如果我的异常声明

ErrorHandlingThreadPoolExecutor.handleError()
作为返回类型,是否有任何方法可以将异常从异步执行的方法传播到
void
?现在看来我可以直接使用执行器而不使用
@Async
,但是使用
@Async
可以吗?如果不是,什么是“侵入性较小”的修复(需要更改和维护的代码更少)?我有很多异步方法返回
void

更新:根据已接受的答案,我提出了以下方面:

import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Map;

@Component
@Aspect
public class ErrorHandlingAspect implements ApplicationListener<ContextRefreshedEvent> {

    public static final String DEFAULT_EXECUTOR_BEAN_NAME = "defaultExecutor";

    private Map<String, ErrorHandlingThreadPoolExecutor> errorHandlingExecutors;

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        // initializing here because not all beans come if initialized in constructor
        this.errorHandlingExecutors = event.getApplicationContext()
                .getBeansOfType(ErrorHandlingThreadPoolExecutor.class);
    }

    @Pointcut(
            // where @Async is on class level
            "@within(org.springframework.scheduling.annotation.Async)"
                    // where @Async is on method level
                    + " || @annotation(org.springframework.scheduling.annotation.Async)")
    public void asyncMethods() {
    }

    @Around("asyncMethods()")
    public Object runWithErrorHandling(ProceedingJoinPoint joinPoint) throws Throwable {
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        Async annotation = method.getAnnotation(Async.class);
        if (annotation == null) {
            annotation = method.getDeclaringClass().getAnnotation(Async.class);
        }
        if (annotation == null) {
            // shouldn't happen because of pointcut configuration, just for safety
            return joinPoint.proceed();
        }

        String asyncExecutorName = annotation.value();
        if (StringUtils.isEmpty(asyncExecutorName)) {
            asyncExecutorName = DEFAULT_EXECUTOR_BEAN_NAME;
        }

        ErrorHandlingThreadPoolExecutor asyncExecutor = errorHandlingExecutors.get(asyncExecutorName);
        if (asyncExecutor == null) {
            // can happen if the declared executor isn't extending ErrorHandlingThreadPoolExecutor
            // or if @Async uses the default executor which is either not registered as a bean at all
            // or not named DEFAULT_EXECUTOR_BEAN_NAME
            return joinPoint.proceed();
        }

        try {
            return joinPoint.proceed();
        } catch (Throwable throwable) {
            asyncExecutor.handleError(throwable);
            return null;
        }
    }
}

优点:

  1. 允许处理异步执行代码中的错误,而无需处理线程。
  2. 可以根据执行器有不同的错误处理。
  3. 可以包装返回
    void
    Future<>
    的方法。

缺点:

  1. 无法处理调用线程中的错误(仅在被调用线程中)。
  2. 需要将默认执行器注册为bean并为其指定特定名称。
  3. 仅适用于
    @Async
    注释,不适用于通过
    submit()
    直接传递给执行器的异步代码。
spring asynchronous spring-aop
1个回答
1
投票

如果您使用这样的方面,您可以摆脱执行器中的错误处理块,或者只使用普通的执行器并完全删除整个(没有真正起作用的)错误处理执行器。我做了并且有效:

package com.example.demo;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class ErrorHandlingAspect {
  // If necessary, narrow down the pointcut more here
  @Around("@within(org.springframework.scheduling.annotation.Async)")
  public Object advice(ProceedingJoinPoint joinPoint) {
    try {
      return joinPoint.proceed();
    }
    catch (Throwable throwable) {
      handleError(throwable);
      // Can also return empty future here for non-void methods
      return null;
    }
  }

  private void handleError(Throwable ex) {
    System.out.println("[ERROR] " + ex);
  }
}

当我删除

ErrorHandlingThreadPoolExecutor
时,将
Writer
上的注释更改为仅
@Async
Config.writeBySchedule
,如下所示:

@Scheduled(fixedRate = 1000)
public void writeBySchedule() {
  writer.write();
  writer.writeFuture();
}

控制台日志如下所示:

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.1.8.RELEASE)

2020-07-15 07:41:02.314  INFO 18672 --- [           main] com.example.demo.DemoApplication         : Starting DemoApplication on Xander-Ultrabook with PID 18672 (C:\Users\alexa\Documents\java-src\spring-aop-playground\target\classes started by alexa in C:\Users\alexa\Documents\java-src\spring-aop-playground)
(...)
2020-07-15 07:41:06.839  INFO 18672 --- [           main] o.s.s.c.ThreadPoolTaskScheduler          : Initializing ExecutorService 'taskScheduler'
Writing something
Writing something with future
[ERROR] java.lang.RuntimeException: Writer exception
[ERROR] java.lang.RuntimeException: Writer exception with future
Writing something
[ERROR] java.lang.RuntimeException: Writer exception
Writing something with future
[ERROR] java.lang.RuntimeException: Writer exception with future
Writing something
Writing something with future
[ERROR] java.lang.RuntimeException: Writer exception
[ERROR] java.lang.RuntimeException: Writer exception with future
(...)
© www.soinside.com 2019 - 2024. All rights reserved.