异常处理方式
异步任务中异常处理方式,不是很难,我们只要在方法中将整个代码块 try...catch
即可。
try { // 其他代码 } catch (Exception e) { e.printStackTrace(); }
一般来说,我们只需要捕获 Exception
异常,就可以应对大部分情况
但是极端情况下,比如方法内发生 OOM
,将会抛出 OutOfMemoryError
。如果发生Error
错误,以上的捕获代码就会失效。
Spring 的异步任务,默认提供几种异常处理方式,可以统一处理异步任务中的发生的异常。
带有返回值的异常处理方式
如果我们使用带有返回值的异步任务,处理方式就比较简单了,我们只需要捕获 Future#get
抛出的异常就好了。
Future<String> future = emailService.sendEmailAsyncWithResult(); try { String result = future.get(); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); }
如果我们使用 ListenableFuture
注册回调函数处理,那我们在方法内增加一个 FailureCallback
,在这个实现类处理相关异常即可。
ListenableFuture<String> listenableFuture = emailService.sendEmailAsyncWithListenableFuture(); // 异步回调处理 listenableFuture.addCallback(new SuccessCallback<String>() { @Override public void onSuccess(String result) { log.info("异步回调处理返回值"); } // 异常处理 }, new FailureCallback() { @Override public void onFailure(Throwable ex) { log.error("异步回调处理异常",ex); } });
统一异常处理方式
没有返回值的异步任务处理方式就比较复杂了,我们需要继承 AsyncConfigurerSupport
,实现 getAsyncUncaughtExceptionHandler
方法,示例代码如下:
@Slf4j @Configuration public class AsyncErrorHandler extends AsyncConfigurerSupport { @Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { AsyncUncaughtExceptionHandler handler = (throwable, method, objects) -> { log.error("全局异常捕获", throwable); }; return handler; } }
ps:这个异常处理方式只能处理未带返回值的异步任务。
异步任务使用注意点
异步线程池设置
Spring 异步任务默认使用 Spring 内部线程池 SimpleAsyncTaskExecutor
。
这个线程池比较坑爹,不会复用线程。也就是说来一个请求,将会新建一个线程。极端情况下,如果调用次数过多,将会创建大量线程。
Java 中的线程是会占用一定的内存空间 ,所以创建大量的线程将会导致 OOM 错误。
所以如果需要使用异步任务,我们需要一定要使用自定义线程池替换默认线程池。
XML 配置方式
如果当前使用 Spring XML 配置方式,我们可以使用如下配置设置线程池:
<task:annotation-driven/> <task:executor id="executor" pool-size="10" queue-capacity="200"/>
注解方式
如果注解方式配置,配置方式如下:
@Configuration public class AsyncConfiguration { @Bean public ThreadPoolTaskExecutor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setThreadNamePrefix("task-Executor-"); executor.setMaxPoolSize(10); executor.setCorePoolSize(5); executor.setQueueCapacity(200); // 还有其他参数可以设置 return executor; } }
只要我们配置了这个线程池Bean
,Spring 的异步任务都将会使用该线程池执行。
如果我们应用配置了多个线程池Bean
,异步任务需要指定使用某个线程池执行,我们只需要在 @Async
注解上设置相应 Bean
的名字即可。示例代码如下:
@Async("taskExecutor") public void sendEmailAsync() { log.info("使用 Spring 异步任务发送邮件示例"); TimeUnit.SECONDS.sleep(2l); }
Spring Boot 方式
如果是 SpringBoot 项目,从阿粉的测试情况来看,默认将会创建核心线程数为 8,最大线程数为 Integer.MAX_VALUE
,队列数也为 Integer.MAX_VALUE
线程池。
ps:以下代码基于 Spring-Boot 2.1.6-RELEASE,暂不确定 Spring-Boot 1.x 版本是否也是这种策略,熟悉的同学的,也可以留言指出一下。
虽然上面的线程池不用担心创建过多线程的问题,不是还是有可能队列任务过多,导致 OOM 的问题。所以还是建议使用自定义线程池吗,或者在配置文件修改默认配置,例如:
spring.task.execution.pool.core-size=10 spring.task.execution.pool.max-size=20 spring.task.execution.pool.queue-capacity=200
ps:如果我们使用注解方式自定义了一个线程池,那么 Spring 异步任务都将会使用这个线程池。通过 SpringBoot 配置文件创建的线程池将会失效。
异步方法失效
Spring 异步任务背后原理是使用 AOP ,而使用 Spring AOP 时我们需要注意,切勿在方法内部调用其他使用 AOP 的方法,可能有点拗口,我们来看下代码:
@Async @SneakyThrows public ListenableFuture<String> sendEmailAsyncWithListenableFuture() { // 这样调用,sendEmailAsync 不会异步执行 sendEmailAsync(); log.info("使用 Spring 异步任务发送邮件,并且获取任务返回结果示例"); TimeUnit.SECONDS.sleep(2l); return AsyncResult.forValue("success"); } /** * 异步发送任务 * * @throws InterruptedException */ @SneakyThrows @Async("taskExecutor") public void sendEmailAsync() { log.info("使用 Spring 异步任务发送邮件示例"); TimeUnit.SECONDS.sleep(2l); }
上面两个方法都处于同一个类中,这样调用将会导致 AOP 失效,无法起到 AOP 的效果。
其他类似的 @Transactional
,以及自定义的 AOP 注解都会有这个问题,大家使用过程,千万需要注意这一点。
总结
Spring 异步任务帮我们大大解决简化开发了流程,只要使用一个@Async
就可以轻松解决异步任务。
不过,虽然使用方式比较简单,大家使用过程一定要注意设置合理的线程池。
由于公号不能直接点击外链,关注「Java极客技术」,回复「20200701」获取源码~
最后说两句(求关注)
最近大家应该发现微信公众号信息流改版了吧,再也不是按照时间顺序展示了。这就对阿粉这样的坚持的原创小号主,可以说非常打击,阅读量直线下降,正反馈持续减弱。