SpringBoot中@Async方法未使用自动配置的ThreadPoolTaskExecutor线程池
问题描述
在项目中由于业务需要,使用了Spring自带的 @Async 注解来异步方法。查看了相关的文档,使用方法和其它自动配置的功能相同。
使用步骤
- 启动类或配置类上标注@EnableAsync
- proxyTargetClass:是否代理实现类而非接口。默认为false,即为JDK中默认的动态代理实现,只能代理目标类实现的接口。若为true则是直接代理实现类,应该是使用了不同的底层实现。
- 在想要被异步调用的方法上,标注 @Async (确保方法在Bean中)
- value:指定异步线程池的bean名称,若为空则使用默认线程池。(默认线程池待会要考)
- 通过Bean调用被 @Async 标注的异步方法。
异常现象
按照步骤配置后,异步方法就已经可以使用了。它底层使用的是默认的线程池,直接上结论,使用的是实现了TaskExecutor接口的ThreadPoolTaskExecutor。在Spring Boot 2.0.9之前使用的是SimpleAsyncTaskExecutor。关于这个就不再展开说明了,SimpleAsyncTaskExecutor的线程实际上不具备复用性,用完即弃。所以不适合应用于生产环境。
在Spring Boot 2.1.0之后,启动时会自动装配一个线程池ThreadPoolTaskExecutor,也就是这次问题的主角。它实现了TaskExecutor(实际上继承关系更加复杂,这里只是简单概括),它的具体参数可以在 yml 中修改,是一个标准的 AutoConfiguration 开发。
于是我想对这个线程池进行一点自定义配置,如下:
1 | spring: |
按照预期,它会作为ThreadPoolTaskExecutor自动配置的参数。
于是我启动项目,准备验收成果。发现出现了预料外的问题。线程名称的前缀是“cTaskExecutor-”(后来才知道其实是SimpleAsyncTaskExecutor)。这和配置中的不同,我的配置似乎没有生效?
与此同时遇到了另一个问题,当我尝试为CompletableFuture指定线程池为ThreadPoolTaskExecutor,让它们共用同一个线程池时。发现无法注入ThreadPoolTaskExecutor类型的Bean。
1 | org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor' available |
解决方案
这说明容器中根本没有ThreadPoolTaskExecutor类型的Bean。也就是说,不仅仅是我的配置没有生效,默认配置的ThreadPoolTaskExecutor都没有被装配。
我猜测是自动配置出了问题(我装的,其实中间排查了很久原因,到最后才怀疑到自动配置上)。
于是翻阅ThreadPoolTaskExecutor自动配置的源码,找到了原因:
1 |
|
其中重点在于 @ConditionalOnMissingBean(Executor.class)
,也就是说只有不存在Executor.class类型的Bean,才会自动装配。
看到这里时我大概就知道原因了。因为在我的项目中,为了实现定时任务,在容器中注入了ScheduledExecutorService类型的Bean。而它也是间接实现了Executor.class接口!导致没有通过 @ConditionalOnMissingBean(Executor.class)
校验,从而导致自动配置失效。
1 |
|
找到原因之后,修复就很简单了,既然Spring Boot本身的校验它不通过,我们直接在自己定义配置类中手动配置这个Bean即可:
1 |
|
配置完成后,重新启动,问题解决。
思考
在这之后我又翻阅了源码,找到了指定处理 @Async 方法线程池的相关代码:
1 |
|
可以看到,它会先调用父类的 getDefaultExecutor 方法,若返回为null,则使用SimpleAsyncTaskExecutor。
1 |
|
父类的getDefaultExecutor会寻找TaskExecutor类型或者名称为‘taskExecutor’的Bean。如果失败则会返回null。正常的情况下ThreadPoolTaskExecutor会被自动配置,并在这里被配置为DefaultExecutor。
于是就引发了本次问题:
在自动配置阶段由于我自定义了ScheduledExecutorService类型的Bean,
@ConditionalOnMissingBean(Executor.class)
校验没有通过,ThreadPoolTaskExecutor没有被自动配置。这个校验条件的设计思想大概就是:如果IOC容器中已经定义的有线程池,那就不自动配置了(毕竟线程很贵)。在指定处理 @Async 方法线程池时,查询条件却不是 Executor.class,而是 TaskExecutor.class,就导致了我自定义的ScheduledExecutorService也没有被注入。甚至注释都说了是防止匹配ScheduledExecutorService。(那ThreadPoolTaskExecutor自动配置时为什么是检测 Executor.class 啊喂!!)
Search for TaskExecutor bean… not plain Executor because that would match ScheduledExecutorService as well, which is unusable for our purposes here. TaskExecutor is more clearly designed for it.
搜索 TaskExecutor Bean… 不是普通的 Executor,因为普通的 Executor 也会匹配 ScheduledExecutorService,而这对我们的目的来说是无用的。TaskExecutor 更适合此用途,它的设计更明确。
最终生效的是SimpleAsyncTaskExecutor。
总感觉 @ConditionalOnMissingBean(Executor.class)
这个限制过于宽泛了,修改成 @ConditionalOnMissingBean(TaskExecutor.class)
会更好一点。一定是Spring的问题!