阿芬最近遇到一个场景,用户注册后需要发邮件到自己的邮箱。在原来的设计中,这是一个同步的过程,注册方法需要等待邮件发送成功后才返回。由于邮件发送过程不是注册的关键节点,我们可以异步执行邮件发送,以减少注册方法的执行时间。我们可以自己创建一个线程池,然后执行异步任务。示例代码如下://生产中使用线程池的最佳实践,一定要自定义线程池,不嫌麻烦,使用Executors创建线程池privateThreadPoolExecutorthreadPool=newThreadPoolExecutor(5,10,60l,TimeUnit.SECONDS,newLinkedBlockingDeque<>(200),newThreadFactoryBuilder().setNameFormat("register-%d").build());/***使用线程池执行发送邮件的任务*/privatevoidsendEmailByThreadPool(){threadPool.submit(()->emailService.sendEmail());}ps:生产中使用线程池的最佳实践,一定要自定义线程池,根据业务场景设置合理的线程池参数,设置线程with一个含义明确的前缀使得故障排除变得非常简单。为了方便,不要使用Executors相关方法创建线程池。上面代码中,线程池用于完成发送邮件的异步任务。可以看到这个例子还是有点麻烦。我们不仅需要自定义线程池,还需要创建相关的任务执行类。Spring提供了执行异步任务的功能,我们可以通过一个注解轻松完成上述功能。今天阿芬就给大家讲解一下Spring异步任务的使用方法以及Spring异步任务使用中的一些注意事项。如何使用异步任务Spring异步任务需要在相关方法上设置@Async注解。在这里,例如,我们创建一个专门用于完整电子邮件服务的EmailService类。代码如下:@Slf4j@ServicepublicclassEmailService{/***异步发送任务**@throwsInterruptedException*/@SneakyThrows@AsyncpublicvoidsendEmailAsync(){log.info("使用Spring异步任务发送邮件示例");//模拟emailsending耗时TimeUnit.SECONDS.sleep(2l);}}这里需要注意的是Spring异步任务默认是关闭的,我们需要使用@EnableAsync开启异步任务。如果还是使用SpringXML配置,我们需要配置如下配置:然后直接调用这个方法。方法将在异步线程中执行。@Slf4j@RestControllerpublicclassRegisterController{@AutowiredEmailServiceemailService;@RequestMapping("register")publicStringregister(){log.info("Registrationprocessstarted");emailService.sendEmailAsync();return"success";}}输出日志如下:从日志中可以看出,这两个方法的执行线程是不同的,也就是说EmailService#sendEmailAsync被异步线程成功执行了。有返回值的异步任务上面的异步任务比较简单,但是有时候我们需要获取异步任务的返回值。如果使用线程池来执行异步任务,我们可以使用threadPool#submit获取返回对象Future,然后调用其内部的get方法获取返回结果。在Spring异步任务中,我们也可以使用Future来获取返回结果,示例代码如下:@Async@SneakyThrowspublicFuturesendEmailAsyncWithResult(){log.info("使用Spring异步任务发送邮件,并获取任务返回结果示例");TimeUnit.SECONDS.sleep(2l);returnAsyncResult.forValue("success");}这里需要注意的是这里需要使用Spring内部类AsyncResult来返回对象。Controller层调用代码如下:ExecutionExceptione){e.printStackTrace();}}}我们知道Future#get方法会被阻塞,直到异步任务执行成功。有时候我们获取异步任务的返回值做后续业务,但是主流程方法不需要返回异步任务的返回值。如果我们使用Future#get方法,主进程将一直被阻塞。对于这种场景,我们可以使用org.springframework.util.concurrent.ListenableFuture稍微修改一下上面的方法。ListenableFuture类允许我们注册一个回调函数。一旦异步任务执行成功,或者执行异常,都会立即执行回调函数。这样,执行的主线程就不会被阻塞。示例代码如下:@Async@SneakyThrowspublicListenableFuturesendEmailAsyncWithListenableFuture(){log.info("使用Spring异步任务发送邮件,并获取任务返回结果示例");TimeUnit.SECONDS.sleep(2l);returnAsyncResult.forValue("success");}controller层代码如下:ListenableFuturelistenableFuture=emailService.sendEmailAsyncWithListenableFuture();//异步回调处理listenableFuture.addCallback(newSuccessCallback(){@OverridepublicvoidonSuccess(Stringresult){log.info("异步回调处理返回值");}},newFailureCallback(){@OverridepublicvoidonFailure(Throwableex){log.error("异步回调处理异常",ex);}});看到这里,如果同学们有疑惑,我们返回的对象是AsyncResult。为什么方法的返回类可以是Future或者ListenableFuture?看完这个类的继承关系,你应该就知道答案了。异常处理方法异步任务中的异常处理方法并不是很难,我们只需要try...catch方法中的整个代码块即可。try{//othercode}catch(Exceptione){e.printStackTrace();}一般来说,我们只需要捕获Exception就可以应对大部分情况,但是在极端情况下,比如方法中OOM,就会抛出OutOfMemoryError.如果出现Error错误,上面的捕获代码就会失败。Spring的异步任务默认提供了几种异常处理方法,可以统一处理异步任务中出现的异常。有返回值的异常处理如果我们使用有返回值的异步任务,处理方式比较简单,只需要捕获Future#get抛出的异常即可。如果我们使用ListenableFuture注册回调函数处理,那么我们在方法中添加一个FailureCallback,在这个实现类中处理相关的异常。ListenableFuturelistenableFuture=emailService.sendEmailAsyncWithListenableFuture();//异步回调处理listenableFuture.addCallback(newSuccessCallback(){@OverridepublicvoidonSuccess(Stringresult){log.info("异步回调处理返回值");}//异常处理},newFailureCallback(){@OverridepublicvoidonFailure(Throwableex){log.error("异步回调处理异常",ex);}});UnifiedexceptionhandlingmethodTheasynchronoustaskprocessingmethodwithoutreturnvalueismorecomplicated,we需要继承AsyncConfigurerSupport,实现getAsyncUncaughtExceptionHandler方法,示例代码如下:@Slf4j@ConfigurationpublicclassAsyncErrorHandlerextendsAsyncConfigurerSupport{@OverridepublicAsyncUncaughtExceptionHandlergetAsyncUncaughtExceptionHandler(){AsyncUncaughtExceptionHandlerhandler=(throwable,method,objects)->{log.error("全局异常捕获",throwable);};returnhandler;}}ps:这种异常处理方式只能处理没有返回值的异步任务。使用异步任务注意事项异步线程池设置Spring异步任务默认使用Spring内部的线程池SimpleAsyncTaskExecutor。这个线程池比较坑爹,不会复用线程。也就是说,一个请求会创建一个新的线程。在极端情况下,如果调用次数过多,则会创建大量线程。Java中的线程会占用一定的内存空间,因此创建大量的线程会导致OOM错误。所以如果我们需要使用异步任务,就需要将默认的线程池换成自定义的线程池。XML配置方式如果使用目前Spring的XML配置方式,我们可以使用如下配置来设置线程池:注解方式如果配置了注解方式,配置方式如下:@ConfigurationpublicclassAsyncConfiguration{@BeanpublicThreadPoolTask??ExecutortaskExecutor(){ThreadPoolTask??Executorexecutor=newThreadPoolTask??Executor();executor.setThreadNamePrefix("task-Executor-");executor.setMaxPoolSize(CoreolSize(10set);executor5);executor.setQueueCapacity(200);//还有其他参数可以设置returnexecutor;}}只要我们配置好这个线程池Bean,Spring的异步任务就会使用这个线程池。如果我们的应用配置了多个线程池bean,异步任务需要使用某个线程池来执行,我们只需要在@Async注解上设置对应bean的名字即可。示例代码如下:@Async("taskExecutor")publicvoidsendEmailAsync(){log.info("使用Spring异步任务发送邮件的例子");TimeUnit.SECONDS.sleep(2l);}SpringBoot方法如果它是一个SpringBoot项目,从A开始根据fans的测试情况,会默认创建核心线程数为8个,最大线程数为Integer.MAX_VALUE,队列数也是Integer.MAX_VALUE线程水池。上面的线程池虽然不用担心创建太多线程,但是还是有可能排队太多任务,导致OOM问题。所以还是建议使用自定义线程池,或者修改配置文件中的默认配置,例如:spring.task.execution.pool.core-size=10spring.task.execution.pool.max-size=20spring.task.执行。pool.queue-capacity=200ps:如果我们使用注解自定义一个线程池,那么Spring异步任务就会使用这个线程池。通过SpringBoot配置文件创建的线程池会失效。异步方法失败Spring异步任务背后的原理就是使用AOP,而我们在使用SpringAOP的时候需要注意,不要在方法内部调用其他使用AOP的方法,可能会有点啰嗦,我们看下code:@Async@SneakyThrowspublicListenableFuturesendEmailAsyncWithListenableFuture(){//这样调用,sendEmailAsync不会异步执行sendEmailAsync();log.info("使用Spring异步任务发送邮件,获取任务返回结果示例");TimeUnit.SECONDS.sleep(2l);returnAsyncResult。forValue("success");}/***异步发送任务**@throwsInterruptedException*/@SneakyThrows@Async("taskExecutor")publicvoidsendEmailAsync(){log.info("使用Spring异步任务发送邮件的例子");TimeUnit.SECONDS.sleep(2l);}以上两个方法在同一个类中,调用它们会导致AOP失效,无法达到AOP的效果。其他类似的@Transactional和自定义AOP注解都会有这个问题,使用时一定要注意这一点。总结Spring异步任务帮助我们大大简化了开发流程,只要使用一个@Async,就可以轻松解决异步任务。不过虽然使用方法比较简单,但是在使用过程中一定要注意设置合理的线程池。