Spring异步任务

Spring异步任务

Spring 异步任务机制非常的有用,特别是在那些记录日志、发端短信、发送邮件等等非核心的业务上面,或者用在一些系统内部任务上,可以优化代码结构,加快程序响应速度,提升用户体验。

这里使用 Springboot 框架来演示

Spring 异步任务默认关闭的,我们需要使用 @EnableAsync开启异步任务

@Configuration
@EnableAsync
public class SpringAsyncConfig {
}

Spring 异步任务需要在相关的方法上设置 @Async 注解,这里为了举例,我们创建一个 EmailService 类,专用完成邮件服务。

@Slf4j
@Service
public class EmailService {

   Logger logger = Logger.getGlobal();
    /**
     * 不带返回值的异步任务
     */
    @SneakyThrows
    @Async
    public void sendEmailAsync() {
        logger.info("使用 Spring 异步任务发送邮件示例");
        // 模拟邮件发送耗时
        TimeUnit.SECONDS.sleep(10L);
    }
}

上述配置完成之后,我们只需要在调用方,比如上一层 Controller 注入这个 EmailService ,然后直接调用这个方法,该方法将会在异步线程中执行。

@Slf4j
@RestController
public class RegisterController {

    Logger logger = Logger.getGlobal();

    @Autowired
    EmailService emailService;

    @RequestMapping("register")
    public String register() {
     logger.info("注册流程开始");
     emailService.sendEmailAsync();
        return "success";
    }
}

输出日志如下:

从日志上可以看到,两个方法执行线程不一样,这就说明了EmailService.sendEmailAsync 被异步线程成功执行。

带有返回值的异步任务

上面的异步任务比较简单,但是有时我们有需要获取异步任务返回值。

如果使用线程池执行异步任务,我们可以使用 threadPool.submit 获取返回对象Future,接着我们就可以调用其内 get 方法,获取返回结果。

在 Spring 异步任务中,我们也可以使用 Future 获取返回结果,示例代码如下:

@Async
@SneakyThrows
public Future<String> sendEmailAsyncWithResult() {
    logger.info("使用 Spring 异步任务发送邮件,并且获取任务返回结果示例");
    TimeUnit.SECONDS.sleep(10L);
    return AsyncResult.forValue("success");
}

这里需要注意,这里返回对象我们需要使用 Spring 内部类 AsyncResult

Controller 层调用代码如下所示:

@RequestMapping("registers")
private void sendEmailWithResult() {
    Future<String> future = emailService.sendEmailAsyncWithResult();
    try {
        String result = future.get();
    } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
    }
}

我们知道 Future.get 方法将会一直阻塞,直到异步任务执行成功。

通过 java - How to use Spring AsyncResult and Future Return - Stack Overflow 得知 future 对象并不是结果,而是实际结果的占位符。现在调用者和工作者并行,可以做不同的事情。当调用者调用 future.get() 而实际结果尚未计算时,这个线程将被阻塞,直到工作者提供结果为止,如果真正的结果已经计算过了,那么调用者会立即得到结果。

有时候我们获取异步任务的返回值是为了做一下后续业务,但是主流程方法是无需返回异步任务的返回值。如果我们使用了 Future.get()方法,主流程就会一直被阻塞。

对于这种场景,我们可以使用org.springframework.util.concurrent.ListenableFuture稍微改造一下上面的方法。

ListenableFuture 这个类允许我们注册回调函数,一旦异步任务执行成功,或者执行异常,将会立刻执行回调函数。通过这种方式就可以不用阻塞执行的主线程。

示例代码如下:

@Async
@SneakyThrows
public ListenableFuture<String> sendEmailAsyncWithListenableFuture() {
    logger.info("使用 Spring 异步任务发送邮件,并且获取任务返回结果示例");
    TimeUnit.SECONDS.sleep(10L);
    return AsyncResult.forValue("success");
}

Controller 层代码如下所示:

@RequestMapping("registerss")
private void sendEmailWithListenableFuture() {

    ListenableFuture<String> listenableFuture = emailService.sendEmailAsyncWithListenableFuture();
    // 异步回调处理
    listenableFuture.addCallback(new SuccessCallback<String>() {
        @Override
        public void onSuccess(String result) {
            logger.info("异步回调处理返回值");

        }
    }, new FailureCallback() {
        @Override
        public void onFailure(Throwable ex) {
            logger.warning("异步回调处理异常"+ ex);
        }
    });
}

看到这里,如果有同学有疑惑,我们返回对象是 AsyncResult,为什么方法返回类可以是Future,又可以是 ListenableFuture?

看完这张类继承关系,大家应该就知道答案了。

异常处理方式

异步任务中异常处理方式,不是很难,我们只要在方法中将整个代码块 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) {
        logger.info("异步回调处理返回值");

    }
    // 异常处理
}, new FailureCallback() {
    @Override
    public void onFailure(Throwable ex) {
        logger.warning("异步回调处理异常",ex);
    }
});

统一异常处理方式

没有返回值的异步任务处理方式就比较复杂了,我们需要继承AsyncConfigurerSupport,实现 getAsyncUncaughtExceptionHandler 方法,示例代码如下:

@Slf4j
@Configuration
public class AsyncErrorHandler extends AsyncConfigurerSupport {

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        AsyncUncaughtExceptionHandler handler = (throwable, method, objects) -> {
            logger.warning("全局异常捕获", throwable);
        };
        return handler;
    }
}

这个异常处理方式只能处理未带返回值的异步任务。

异步任务使用注意点

异步线程池设置

Spring 异步任务默认使用 Spring 内部线程池 SimpleAsyncTaskExecutor

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

Java 中的线程是会占用一定的内存空间 ,所以创建大量的线程将会导致 OOM 错误。

所以如果需要使用异步任务,我们需要一定要使用自定义线程池替换默认线程池

注解方式配置:

@Configuration
@EnableAsync
public class SpringAsyncConfig {
    @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() {
    logger.info("使用 Spring 异步任务发送邮件示例");
    TimeUnit.SECONDS.sleep(10L);
}

异步方法失效

Spring 异步任务背后原理是使用 AOP ,而使用 Spring AOP 时我们需要注意,切勿在方法内部调用其他使用 AOP 的方法,可能有点拗口,我们来看下代码:

@Async
@SneakyThrows
public ListenableFuture<String> sendEmailAsyncWithListenableFuture() {
    // 这样调用,sendEmailAsync 不会异步执行
    sendEmailAsync();
    logger.info("使用 Spring 异步任务发送邮件,并且获取任务返回结果示例");
    TimeUnit.SECONDS.sleep(10L);
    return AsyncResult.forValue("success");
}

/**
 * 异步发送任务
 */
@SneakyThrows
@Async("taskExecutor")
public void sendEmailAsync() {
    logger.info("使用 Spring 异步任务发送邮件示例");
    TimeUnit.SECONDS.sleep(10L);
}

上面两个方法都处于同一个类中,这样调用将会导致 AOP 失效,无法起到 AOP 的效果。

其他类似的 @Transactional,以及自定义的 AOP 注解都会有这个问题,大家使用过程,千万需要注意这一点。


参考来源:

点赞!超详细的一份 Spring 异步任务教程_牛客博客 (nowcoder.net) 有删改

浅析如何使用Spring的@Async异步任务、自定义线程池及异常处理 - 古兰精 - 博客园 (cnblogs.com)

13.ThreadPoolExecutor线程池之submit方法 - OKevin - 博客园 (cnblogs.com)

java - How to use Spring AsyncResult and Future Return - Stack Overflow

Spring Async不得不知的用法_cmlbeliever的博客-CSDN博客

使用JDK Logging - 廖雪峰的官方网站 (liaoxuefeng.com)