我接触异步概念最早的时候是在大学学习前端中,是一个很老的技术叫Ajax。异步编程很重要!很重要!很重要!在接口性能优化中我常用“sql优化、业务优化、算法优化”三把斧。其中“业务优化”中有“是否有可以异步进行的”这么一小则。今天的主题我们来看看Spring中相关异步中异常的那点事。
本文不是异步入门,默认读者有初步的异步知识。还不懂异步的读者请先网上查找相关资料先了解。
异步方法有两种形式:
- 无返回值:void
- 有返回值:可以声明常规的java.util.concurrent.Future返回类型,还可以声明 Spring 的org.springframework.util.concurrent.ListenableFuture,或者声明为 Spring 4.2 的 JDK 8 的java.util.concurrent.CompletableFuture:与异步任务进行更丰富的交互,并通过进一步的处理步骤立即进行组合。推荐CompletableFuture封装返回值
CompletableFuture是个好东西,网上资料也很多。推荐读者学习!
这两种方式的不同,导致了异步方法中异常管理不同。在有返回值的异步方法,调用者可以在调用get阻塞时捕获异步方法中的抛出的异常并进行处理。但是对于无返回值的异步方法,则不能如此了。那我们该怎么办呢?
我们先来看一下异步的例子。定义一个异步任务组件并交由spring管理
@Component
@Slf4j
public class AsyncTask {
/**
* 有返回值的异步方法
*/
@Async("honeyAsyncExecutor")
public CompletableFuture<Integer> asyncFuture() throws InterruptedException {
log.info("start async task");
Thread.sleep(1000)
// 故意抛出异常
int a = 1 % 0;
log.info("end async task");
return CompletableFuture.completedFuture(2);
}
/**
* 无返回值的异步方法
*/
@Async("honeyAsyncExecutor")
public void async() throws InterruptedException {
log.info("start async task");
Thread.sleep(1000);
// 故意抛出异常
int a = 1 % 0;
log.info("end async task");
}
}
测试类
@Slf4j
@SpringBootTest
@RunWith(SpringRunner.class)
public class AsyncTest {
@Autowired
private AsyncTask asyncTask;
@Test
public void asyncTest() throws InterruptedException {
log.info("start.....");
CompletableFuture<Integer> future = asyncTask.asyncFuture();
log.info("start main bs");
Thread.sleep(2000);
log.info("main bs end...");
log.info("start request the result");
try {
Integer integer = future.get();
log.info(String.valueOf(integer));
} catch (ExecutionException exception) {
log.info("catch the exception");
}
}
}
在结果中我们成功捕获了异常并进行了处理。注意:异步方法中抛出的异常会被包装成ExecutionException。
我们接着看一下没有返回值的异常该怎么捕获并处理
@Slf4j
@SpringBootTest
@RunWith(SpringRunner.class)
public class AsyncTest {
@Autowired
private AsyncTask asyncTask;
@Test
public void testAsync() throws InterruptedException {
log.info("start.....");
asyncTask.async();
log.info("start main bs");
Thread.sleep(2000);
log.info("main bs end...");
}
}
结果中看到了SimpleAsyncUncaughtExceptionHandler这么个类。我们具体看一下这个类。
这个类是spring默认提供的针对没有返回值抛出异常时捕获到异常后,仅仅是打印,其他啥都没干。
public class SimpleAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler {
private static final Log logger = LogFactory.getLog(SimpleAsyncUncaughtExceptionHandler.class);
@Override
public void handleUncaughtException(Throwable ex, Method method, Object... params) {
if (logger.isErrorEnabled()) {
logger.error("Unexpected exception occurred invoking async method: " + method, ex);
}
}
}
相信读者看到这里,应该懂该怎么针对性处理无返回值异步方法的异常了。就是自定义一个我们的AsyncUncaughtExceptionHandler 。但是在这里,博主并不推荐直接像spring那样实现AsyncUncaughtExceptionHandler 接口来提供我们的异步未捕获异常处理器。如果我们使用异步,我们要养成这么一个良好习惯:我们要根据我们系统的运行情况、配置情况等配置AsyncExecutor。所以比较好的做法是实现AsyncConfigurer 接口,在接口里提供AsyncExecutor,并提供上面的AsyncUncaughtExceptionHandler 。并在异步方法上面的异步注解指定Executor。
getAsyncExecutor前面为什么加个@Bean是为了可以给@Async指定Executor
@Slf4j
@Configuration
public class HoneyAsyncConfigurer implements AsyncConfigurer {
@Bean("honeyAsyncExecutor")
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(8);
executor.setMaxPoolSize(16);
executor.setQueueCapacity(64);
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.setThreadNamePrefix("HoneyAsyncThread-");
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (Throwable ex, Method method, Object... params) -> {
String errorBuilder = "Async execution error on method:" + method.toString() + " with parameters:"
+ Arrays.toString(params);
log.error(errorBuilder);
};
}
}
这里博主也没进行什么特殊处理,为了演示,仅仅是打印而已。在博主的Honey-OSS存储服务中,针对异步上传中也有涉及没有没有返回值的异步方法,在遇到预期中的异常时(像网络导致的链接异常)会利用重试机制进行重试。
说到这个重试,博主封装了基于注解的重试机制工具。后面分享出来。
- 本文就到此结束了,我们下文见,谢谢
- github: honey开源系列组件作者