之前我们简要说过@Async和
首先肯定是有线程池的。Spring Boot已经帮你创建并配置好了,还配了两个,一个供@Async使用,一个供@Scheduled使用。
Spring将异步任务和定时任务的执行,抽象出了两个接口,TaskExecutor。
如果你对Java的线程池相关的API比较熟,那么在需要使用线程池的场景,你可能会用Executors来生成ExecutorService(继承于Executor),从而执行任务。Spring中的TaskExecutor其实和Java的Executor是一样的,只不过后者是在Java 5的时候被引入的,Spring为了兼容之前的Java版本,自己搞了一套。时至今日,Spring已经最低要求Java 8了,但是为了兼容之前的Spring版本,还是保持了自己的API,基础的TaskExecutor接口已经直接继承于Executor了(如上图)。
TaskExecutor有很多实现,比如Executor对象包装成TaskExecutor对象,这样将Java的Executor对象纳入到Spring管理,方便使用;再比如最常用的ThreadPoolTaskExecutor封装了Java的ThreadPoolExecutor,封装的同时也会对其进行一些配置。
Spring Boot会帮你自动生成一个ThreadPoolTaskExecutor,进行了默认配置,相关代码在TaskExecutionAutoConfiguration。使用如下属性,还可以对线程池进行自定义,比如核心线程数目、最多线程数目、线程名前缀等等:
如果你的代码里需要ThreadPoolTaskExecutor,直接通过@Autowired引入就行了。而@Async注解用的正是Spring Boot自动生成的这个对象,具体一点说,就是它会去容器里找TaskExecutor类型的Bean,如果有多个,他会再去找名为“taskExecutor”、类型为Executor的Bean。我在源码里扒拉半天才搞清楚的,如果也想搞清楚,可以查看getDefaultExecutor方法。
下面再来说说TaskScheduler。
TaskScheduler倒是没有什么历史遗留问题,你看看上图中的它的方法,会发现,这不跟@Scheduled注解的参数是大致对应的嘛,简直太好理解了:
我们上面说了,定时任务也有一个对应的线程池,具体实现由ThreadPoolTaskScheduler负责的,它其实是封装了Java里的ScheduledExecutorService。并且也有对应的属性方便你去自定义:
如果你研究透了之后会发现,以上其实就是典型的Spring Boot的特点,“自动配置好,不够你自己改”。你通过@EnableAsync和@EnableScheduling来开启功能,然后就可以通过@Async和@Scheduled来执行异步任务和定时任务,这些任务会分别扔给容器里的ThreadPoolTaskExecutor和ThreadPoolTaskScheduler,然后放到各自的线程池中运行,想要自定义可以在application.properties中修改属性,如果还觉得不够用,甚至可以给容器提供自己的TaskExecutor和TaskScheduler实现来覆盖Spring Boot默认给你的对象。如果你只想自定义@Async使用的线程池,可以通过Executor实现。这样一步步由浅入深,由简单场景过渡到复杂场景,各种需求都可以满足。如果有人还不理解Spring Boot的特点到底是啥,你可以拿本文的例子来讲。
再说说@Async的异常处理。如果你的@Async方法的返回值是Future类型或者ListenableFuture或者CompletableFuture等类型,那么你在调用Future.get()方法的时候,异常会被抛出。如果你的@Async方法的返回值是void,那么你可以通过AsyncConfigurer传递一个全局的
还有几个类,Spring文档并没有给出怎么用,但是源码告诉了大家怎么用。
比如TaskExecutorCustomizer,你的ThreadPoolTaskExecutor在创建完成并通过application.properties做了定制之后,还可以通过TaskExecutorCustomizer进行进一步的定制。对应的,TaskSchedulerCustomizer还可以对ThreadPoolTaskScheduler进行定制。比如TaskDecorator,可以对放到ThreadPoolTaskExecutor里执行的Runnable进行封装。你需要做的就是实现TaskExecutorCustomizer、TaskSchedulerCustomizer或者TaskDecorator,并通过@Bean方法或者@Component注解,将他们放到容器中去。
PS:我写完之后反复读了本文,感觉如果不跟着源码一起看的话,很可能不能很好理解。源码和概念相结合,使用效果更佳。