这可能是阿粉见过最详细的一份 Spring 异步任务教程(下)

简介: 阿粉最近碰到一个场景,用户注册之后需要发送邮件给其邮箱。原先设计中,这是一个同步过程,注册方法需要等待邮件发送成功才能返回。 由于邮件发送流程对于注册来说并不是一个关键节点,我们可以将邮件发送异步执行,减少注册方法执行时间。

异常处理方式

异步任务中异常处理方式,不是很难,我们只要在方法中将整个代码块 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

97.jpg

这个线程池比较坑爹,不会复用线程。也就是说来一个请求,将会新建一个线程。极端情况下,如果调用次数过多,将会创建大量线程。

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 版本是否也是这种策略,熟悉的同学的,也可以留言指出一下。

98.jpg

虽然上面的线程池不用担心创建过多线程的问题,不是还是有可能队列任务过多,导致 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」获取源码~

最后说两句(求关注)

最近大家应该发现微信公众号信息流改版了吧,再也不是按照时间顺序展示了。这就对阿粉这样的坚持的原创小号主,可以说非常打击,阅读量直线下降,正反馈持续减弱。

相关文章
|
1天前
|
Java 索引 Spring
教程:Spring Boot中集成Elasticsearch的步骤
教程:Spring Boot中集成Elasticsearch的步骤
|
1天前
|
Java API Spring
教程:Spring Boot中如何集成GraphQL
教程:Spring Boot中如何集成GraphQL
|
1天前
|
缓存 Java Spring
教程:Spring Boot中集成Memcached的详细步骤
教程:Spring Boot中集成Memcached的详细步骤
|
1天前
|
安全 Java 数据库连接
Spring Boot 优雅关机时异步线程安全优化
Spring Boot 优雅关机时异步线程安全优化
7 1
|
4天前
|
Java API UED
Spring Boot中如何处理异步任务
Spring Boot中如何处理异步任务
|
4天前
|
存储 NoSQL Java
教程:Spring Boot与RocksDB本地存储的整合方法
教程:Spring Boot与RocksDB本地存储的整合方法
|
4天前
|
Java API Spring
Spring Boot中如何处理异步任务
Spring Boot中如何处理异步任务
|
5天前
|
搜索推荐 Java 机器人
教程:Spring Boot中集成Elasticsearch的步骤
教程:Spring Boot中集成Elasticsearch的步骤
|
5天前
|
Java 机器人 程序员
教程:Spring Boot中如何集成GraphQL
教程:Spring Boot中如何集成GraphQL
|
5天前
|
缓存 Java 机器人
教程:Spring Boot中集成Memcached的详细步骤
教程:Spring Boot中集成Memcached的详细步骤