Spring Boot - 异步任务
# 1. 异步任务
有时候,前端可能提交了一个耗时任务,如果后端接收到请求后,直接执行该耗时任务,那么前端需要等待很久一段时间才能接受到响应。如果该耗时任务是通过浏览器直接进行请求,那么浏览器页面会一直处于转圈等待状态。
事实上,当后端要处理一个耗时任务时,通常都会将耗时任务提交到一个异步任务中进行执行,此时前端提交耗时任务后,就可直接返回,进行其他操作。
# 什么叫异步
异步:如果方法中有休眠任务,不用等任务执行完,直接执行下一个任务
简单来说:客户端发送请求,可以跳过方法,执行下一个方法,如果其中一个A方法有休眠任务,不需要等待,直接执行下一个方法,异步任务(A 方法)会在后台得到执行,等 A 方法的休眠时间到了再去执行 A 方法。
同步:一定要等任务执行完了,得到结果,才执行下一个任务。
# 2. Java 线程处理异步
在 Java 中,开启异步任务最常用的方式就是开辟线程执行异步任务,如下所示:
@RestController
@RequestMapping("async")
public class AsyncController {
@GetMapping("/")
public String index() {
new Thread(new Runnable() {
@Override
public void run() {
try {
// 模拟耗时操作
Thread.sleep(TimeUnit.SECONDS.toMillis(5));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
return "consuming time behavior processing!";
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
这时浏览器请求 localhost:8080/async/
,就可以很快得到响应,并且耗时任务会在后台得到执行。
一般来说,前端不会关注耗时任务结果,因此前端只需负责提交该任务给到后端即可。但是如果前端需要获取耗时任务结果,则可通过 Future 等方式将结果返回,详细内容如下
public class MyReturnableTask implements Callable<String> {
@Override
public String call() throws Exception {
long startTime = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName()+"线程运行开始");
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName()+"线程运行结束");
return "result";
}
}
2
3
4
5
6
7
8
9
10
@GetMapping("/task")
public void task() throws ExecutionException, InterruptedException {
MyReturnableTask myReturnableTask = new MyReturnableTask();
FutureTask<String> futureTask = new FutureTask<String>(myReturnableTask);
Thread thread = new Thread(futureTask, "returnableThread");
thread.start();
String s = futureTask.get();
System.out.println(s);
}
2
3
4
5
6
7
8
9
事实上,在 Spring Boot 中,我们不需要手动创建线程异步执行耗时任务,因为 Spring 框架已提供了相关异步任务执行解决方案,本文主要介绍下在 Spring Boot 中执行异步任务的相关内容。
# 3. SpringBoot 异步任务
在Spring Boot应用中,你可以通过使用@EnableAsync
和@Async
注解轻松实现异步任务。这使得在不阻塞调用线程的情况下执行长时间运行的任务成为可能。下面是关于如何使用这些特性的详细总结:
# 开启异步任务支持
首先,在你的Spring Boot主程序类或配置类上使用@EnableAsync
注解。这会告诉Spring框架为那些标记有@Async
注解的方法创建代理,使得这些方法可以异步执行。
@SpringBootApplication
@EnableAsync // 开启异步任务支持
public class ApplicationStarter {
public static void main(String[] args) {
SpringApplication.run(ApplicationStarter.class, args);
}
}
2
3
4
5
6
7
# 定义异步方法
在你的服务类中,使用@Async
注解标记任何你希望异步执行的方法。这些方法可以有任意的参数类型,但它们必须返回void
或者Future
类型的对象。
@Service
public class AsyncServiceImpl {
/**
* 异步执行的无返回值方法。
* 通过 @Async 注解标记,Spring会在执行此方法时将其放入异步任务中执行。
* 这意味着主线程调用此方法后不会等待其执行完成,而是立即返回继续执行后续代码。
*/
@Async
public void t1() {
// 模拟耗时任务,例如:数据库操作、文件读写、网络请求等
try {
// 模拟任务执行耗时5秒
Thread.sleep(TimeUnit.SECONDS.toMillis(5));
} catch (InterruptedException e) {
// 处理中断异常
e.printStackTrace();
}
// 任务完成后的操作
System.out.println("异步方法中:耗时任务完成");
}
/**
* 异步执行的有返回值方法。
* 方法返回一个 Future<String> 类型的对象,允许调用者获取异步执行的结果。
* AsyncResult<String> 是一个 Future 的实现,用于包装异步操作的结果。
*/
@Async
public Future<String> t2() {
// 模拟耗时任务
try {
// 模拟任务执行耗时5秒
Thread.sleep(TimeUnit.SECONDS.toMillis(5));
} catch (InterruptedException e) {
// 处理中断异常
e.printStackTrace();
}
// 返回异步任务的执行结果
return new AsyncResult<>("异步任务完成!");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
- t1方法:此方法没有返回值。它演示了如何执行一个耗时操作(比如休眠5秒)而不阻塞调用线程。方法执行完成后,会在控制台输出一条消息表示异步任务已完成。
- t2方法:此方法返回一个
Future<String>
对象,允许调用者在未来某个时间点获取异步操作的结果。它同样执行一个耗时操作,然后使用AsyncResult
对象返回一个字符串结果。AsyncResult
是Spring提供的Future
接口的实现,用于从异步方法中返回结果。
# 获取异步方法的结果
当异步方法需要返回结果时,你应该使用Future
、ListenableFuture
或CompletableFuture
类型的返回值。这些返回类型提供了方法来检查异步操作是否完成、等待异步操作的完成,并获取异步操作的结果。
如果你需要在你的控制器或其他服务中获取这些异步方法的结果,你可以调用Future
的get()
方法。请记住,get()
方法是阻塞的,它会等待异步操作完成并返回结果。
@Autowired
private AsyncServiceImpl asyncService;
public void useAsyncMethod() throws Exception {
Future<String> futureResult = asyncService.t2();
// 执行其他非阻塞操作...
String result = futureResult.get(); // 阻塞直到异步方法完成
System.out.println(result);
}
2
3
4
5
6
7
8
9
注意事项
使用@Async
标记的方法会在Spring管理的线程池中异步执行。默认情况下,Spring会使用一个简单的线程池,但你可以通过实现AsyncConfigurer
接口自定义线程池。
- 方法的异步执行:使用
@Async
注解的方法,其调用会立即返回,而方法体内的代码会在另一个线程中执行。 - 异常处理:异步方法中抛出的未捕获异常默认情况下会被忽略。你可以通过实现
AsyncUncaughtExceptionHandler
接口来自定义异常处理逻辑。 - 使用场景:异步方法非常适合处理那些耗时的任务,如发送邮件、调用远程服务等操作,它可以提高程序的响应速度和吞吐量。
# @Async注解注意的问题
异步任务在提高应用性能和响应速度方面非常有用,但它们的使用确实存在一些限制和注意事项。正确理解和遵循这些限制可以帮助您有效地使用@Async
注解,避免常见的错误。
Public方法:被
@Async
注解的方法必须是public的。这是因为Spring通过代理模式来实现方法的异步调用,而只有public方法才可以被代理拦截。如果方法不是public的,那么它将不会异步执行,而是像普通方法一样同步执行。避免同类内调用:在同一个类内部直接调用
@Async
方法会绕过Spring代理机制,导致异步方法同步执行。这是因为代理是基于类的外部调用实现的。如果需要在同一个类中调用异步方法并保持异步行为,可以考虑将异步方法放在另一个被Spring管理的bean中。@Service public class MyService { @Async public void asyncMethod() { // 异步执行的操作 } // syncMethod方法中对asyncMethod的调用实际上是同步的,因为它直接调用了方法,而没有经过Spring创建的代理。 public void syncMethod() { asyncMethod(); // 这个调用实际上是同步的 } } // 通过注入自己的bean实例,然后用该实例Bean内部调用@Async注解方法可以解决不走代理的问题,从而使得异步执行生效。 // 这样操作会产生循环依赖问题,可以使用@Lazy注解可以延迟Bean的初始化,从而避免启动时的循环依赖问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15非Static方法:被
@Async
注解的方法不能是static的。静态方法不属于任何实例,而Spring的代理模式是基于bean实例的,所以无法代理静态方法。不能用于生命周期回调:
@Async
注解不能用于Bean对象的生命周期回调方法(例如,使用@PostConstruct
注解的方法)。生命周期回调方法是由容器在特定时点调用的,不适合作为异步任务。Spring管理的Bean:要使用
@Async
功能,异步方法所在的类必须被Spring管理,即通过@Component
、@Service
等注解声明为Spring的组件。这样Spring才能创建相应的代理并管理其生命周期。依赖注入:其他类中使用异步类对象时,必须通过Spring的依赖注入机制(如
@Autowired
)进行注入,而不能通过手动new
的方式创建对象。手动创建的对象不会被Spring上下文管理,因此不会被代理,@Async
注解的方法也不会异步执行。
# @Async注解引发的循环依赖问题
当你在Spring中使用@Async
注解时,Spring会为这个Bean创建一个代理对象,这样当调用该Bean的方法时,Spring可以捕获这个调用,并在另一个线程中异步执行它。这是通过在Bean初始化的最后阶段添加一个代理来实现的。
循环依赖问题
假设我们有两个Bean,A和B,它们相互依赖。也就是说,A需要B来完成它的初始化,B也需要A来完成它的初始化。
- 没有使用
@Async
时:Spring的三级缓存机制可以解决这种循环依赖,即通过提前暴露一个Bean的创建工厂来允许早期引用,确保A和B都能正确地初始化。但Spring的三级缓存机制本身并不涉及到延迟初始化。 - 使用了
@Async
后:假设A使用了@Async
注解。这意味着Spring会在A完全初始化之后,为A创建一个代理对象。但是,如果B在A的代理对象创建之前就尝试注入A,B实际上注入的将是A的原始对象,而不是A的代理对象。因为代理对象是在A初始化的最后阶段创建的。
这里的关键点是:代理对象的创建改变了A的引用。也就是说,一开始B尝试注入的A对象(A的原始对象),和后来Spring管理的A对象(A的代理对象)不一致。这种不一致导致了循环依赖问题的出现。
解决方法
- 使用
@Lazy
注解:在依赖@Async
注解的Bean上使用@Lazy
注解,可以延迟该Bean的实例化和初始化,这样有助于确保其他依赖项先于该Bean初始化,从而避免循环依赖。 - 自定义异步工具类:避免直接使用
@Async
注解,而是通过自定义异步工具类(使用线程池等)来实现异步逻辑。这种方式不会创建代理对象,因此不会干扰Spring的依赖注入流程。 - 避免循环依赖:重新设计Bean的依赖关系,尽量避免让使用
@Async
注解的Bean参与循环依赖。这可能涉及到重构应用的架构和Bean之间的交互方式。
@Lazy的作用
@Lazy
注解使得Bean的初始化被延迟到首次访问时。对于@Async
注解的Bean,这意味着其代理对象的创建也被相应地延迟了。如果@Lazy
能够确保所有相关的依赖注入在代理对象创建之前完成,那么在代理对象创建之后,相互依赖的Bean之间应该就不会有问题,因为此时它们都将引用正确的代理对象。
# 默认的异步执行行为
当你在方法上使用@Async
注解而没有指定任何特定的执行器(Executor)时,Spring会按照以下规则来决定使用哪个线程池执行这些异步任务:
- 自动搜索线程池Bean:Spring首先会检查上下文中是否存在唯一的TaskExecutor类型的Bean。如果存在,就使用这个Bean作为异步方法的执行器。
- 检查命名为
taskExecutor
的Bean:如果没有找到唯一的TaskExecutor
,Spring接着会尝试查找一个名为taskExecutor
的Executor
类型的Bean,并使用它。 - 回退到
SimpleAsyncTaskExecutor
:如果以上两个条件都不满足,即没有找到任何合适的TaskExecutor
或名为taskExecutor
的Executor
Bean,Spring默认会使用SimpleAsyncTaskExecutor
执行异步方法。SimpleAsyncTaskExecutor并不重用线程,每次调用都会创建一个新的线程,这在许多业务场景下可能不是最优选择,因为频繁地创建和销毁线程可能会影响性能。
Spring提供了多种线程池:
SimpleAsyncTaskExecutor:不是真的线程池,这个类不重用线程,每次调用都会创建一个新的线程。
SyncTaskExecutor:这个类没有实现异步调用,只是一个同步操作。只适用于不需要多线程的地方。
ConcurrentTaskExecutor:Executor的适配类,不推荐使用。如果ThreadPoolTaskExecutor不满足要求时,才用考虑使用这个类。
ThreadPoolTaskScheduler:可以使用cron表达式。
ThreadPoolTaskExecutor:最常使用,推荐。其实质是对java.util.concurrent.ThreadPoolExecutor的包装。
下面我们来看SimpleAsyncTaskExecutor
提交任务的源码,验证一下这个类是不是真的不重用线程:
@Override
public void execute(Runnable task, long startTimeout) {
// 确保传入的任务不为空
Assert.notNull(task, "Runnable must not be null");
// 如果存在任务装饰器,则对任务进行装饰
Runnable taskToUse = (this.taskDecorator != null ? this.taskDecorator.decorate(task) : task);
// 检查是否激活了并发节流,并且启动超时大于立即执行的阈值
if (isThrottleActive() && startTimeout > TIMEOUT_IMMEDIATE) {
// 在允许执行任务之前进行并发控制
this.concurrencyThrottle.beforeAccess();
// 执行经过并发控制处理的任务
doExecute(new ConcurrencyThrottlingRunnable(taskToUse));
} else {
// 如果没有并发节流激活,直接执行任务
doExecute(taskToUse);
}
}
protected void doExecute(Runnable task) {
// 使用线程工厂创建新线程,如果线程工厂不存在,则直接创建新线程
Thread thread = (this.threadFactory != null ? this.threadFactory.newThread(task) : createThread(task));
// 启动线程,执行任务
thread.start();
}
public Thread createThread(Runnable runnable) {
// 创建一个新线程,指定线程组、任务和线程名
Thread thread = new Thread(getThreadGroup(), runnable, nextThreadName());
// 设置新线程的优先级
thread.setPriority(getThreadPriority());
// 设置新线程是否为守护线程(后台线程)
thread.setDaemon(isDaemon());
// 返回创建的线程对象
return thread;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
doExecute
方法:它检查是否有线程工厂threadFactory
被设置,如果有,则使用线程工厂创建新线程执行任务;如果没有,则调用createThread(task)
方法创建新线程。无论哪种情况,最后都通过调用thread.start()
来启动新线程执行传入的任务。
# 为什么高版本没有使用SimpleAsyncTaskExecutor?
写一个简单的demo去验证一下我们的猜想,看看底层是不是真的调用的SimpleAsyncTaskExecutor
处理器:
@Service
public class AsyncServiceImpl {
// 使用 @Async 注解标记的方法 会提交到一个异步任务中进行执行,第一次不会执行该方法
// 如果不添加该注解,controller 中调用该方法会等待 5 秒在响应
@Async
public void t1() {
// 模拟耗时任务
try {
Thread.sleep(TimeUnit.SECONDS.toMillis(5)); // 在这里打上断点
} catch (InterruptedException e) {
e.printStackTrace();
}
// 因为该异步方法中使用了休眠,所以过 5 秒才会执行下面代码
System.out.println("异步方法中:耗时时间已走完");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Component
public class MyApplicationRunner implements ApplicationRunner {
@Resource
private AsyncServiceImpl asyncService;
@Override
public void run(ApplicationArguments args) throws Exception {
asyncService.t1(); // 在SpringBoot应用准备就绪后调用这个异步方法
System.out.println("ApplicationRunner: 应用就绪,执行自定义逻辑");
}
}
2
3
4
5
6
7
8
9
10
11
12
我们在想要观察的异步方法内设置一个断点,在AsyncServiceImpl
类的t1
方法内部,在Thread.sleep
语句或之后设置断点,这样做是为了在异步方法执行时暂停程序,我们有机会查看调用栈。使用ApplicationRunner在项目启动完成后去调用我们的异步方法。当程序执行到断点时,IDE会自动暂停执行,此时我们打开调试视图去查看调用栈:
此时我惊奇的发现底层调用的居然不是我们想要的SimpleAsyncTaskExecutor
,而是ThreadPoolTaskExecutor
。
为什么容器中会有 ThreadPoolTaskExecutor 的 Bean 对象,我们并没有去自定义自己的线程池呀?
翻阅了springboot的源码,在spring-boot-autoconfigure 这个包下面的spring.factories中找到了自动配置TaskExecutionAutoConfiguration
,在该类中我们看到了如下代码:
@ConditionalOnClass({ThreadPoolTaskExecutor.class})
@AutoConfiguration
@EnableConfigurationProperties({TaskExecutionProperties.class})
public class TaskExecutionAutoConfiguration {
public static final String APPLICATION_TASK_EXECUTOR_BEAN_NAME = "applicationTaskExecutor";
// 默认构造函数
public TaskExecutionAutoConfiguration() {
}
// 定义一个Bean来构建TaskExecutor。如果已经存在一个Bean,则不创建新的Bean
@Bean
@ConditionalOnMissingBean
public TaskExecutorBuilder taskExecutorBuilder(TaskExecutionProperties properties, ObjectProvider<TaskExecutorCustomizer> taskExecutorCustomizers, ObjectProvider<TaskDecorator> taskDecorator) {
Pool pool = properties.getPool(); // 获取线程池配置属性
TaskExecutorBuilder builder = new TaskExecutorBuilder(); // 创建TaskExecutor构建器
// 以下是配置线程池属性的代码,包括队列容量、核心线程数、最大线程数等
builder = builder.queueCapacity(pool.getQueueCapacity());
builder = builder.corePoolSize(pool.getCoreSize());
builder = builder.maxPoolSize(pool.getMaxSize());
builder = builder.allowCoreThreadTimeOut(pool.isAllowCoreThreadTimeout());
builder = builder.keepAlive(pool.getKeepAlive());
Shutdown shutdown = properties.getShutdown(); // 获取关闭线程池的配置
builder = builder.awaitTermination(shutdown.isAwaitTermination()); // 是否等待所有任务完成后再关闭线程池
builder = builder.awaitTerminationPeriod(shutdown.getAwaitTerminationPeriod()); // 等待终止的时间
builder = builder.threadNamePrefix(properties.getThreadNamePrefix()); // 设置线程名的前缀
builder = builder.customizers(taskExecutorCustomizers.orderedStream()::iterator); // 应用自定义的TaskExecutor定制器
builder = builder.taskDecorator(taskDecorator.getIfUnique()); // 设置任务装饰器
return builder;
}
// 定义一个线程池TaskExecutor的Bean,名为"applicationTaskExecutor"和"taskExecutor"
@Lazy // 延迟初始化该Bean,直到首次被引用
@Bean(
name = {"applicationTaskExecutor", "taskExecutor"}
)
@ConditionalOnMissingBean({Executor.class}) // 仅当不存在Executor类型的Bean时才创建
public ThreadPoolTaskExecutor applicationTaskExecutor(TaskExecutorBuilder builder) {
return builder.build(); // 使用TaskExecutorBuilder构建ThreadPoolTaskExecutor
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
此配置类通过TaskExecutionProperties
提供的配置来自定义任务执行器,然后点击applicationTaskExecutor
方法中的build()
方法进入到了TaskExecutorBuilder
类,发现默认使用的并不是SimpleAsyncTaskExecutor而是ThreadPoolTaskExecutor。
public ThreadPoolTaskExecutor build() {
return configure(new ThreadPoolTaskExecutor());
}
2
3
由于 TaskExecutionAutoConfiguration
是SpringBoot内置的自动配置类,所以SpringBoot 一启动就会扫描它,由此可以断定 ThreadPoolTaskExecutor 是 SpringBoot 项目中 Executor 的默认 Bean 对象。而 @Async 在选择执行器的时候会先去 IOC 容器中先找是否有 TaskExecutor 的 Bean对象,所以在当前版本 SpringBoot 中,@Async 的默认 TaskExecutor 是 ThreadPoolTaskExecutor。
最后查询了各种资料才找到了原因,在spring boot2.1.0.RELEASE版本的时候,新增了TaskExecutionAutoConfiguration配置类,
也就是说新版本,其实spring boot默认使用的已经是ThreadPoolTaskExecutor线程池了,大家不用再去手动更改默认的线程池,我们可以在yml配置文件去配置ThreadPoolTaskExecutor的默认参数了,这样SpringBoot启动的时候就会加载这些参数去实例化ThreadPoolTaskExecutor线程池了。
总结
在 SpringBoot 2.0.9 版本及以前,@Async 默认使用的是 SimpleAsyncTaskExecutor;从 2.1.0 开始,@Async 默认使用的是 ThreadPoolTaskExecutor。这也就是大家的结论里面spring线程池默认不是ThreadPoolTaskExecutor的原因。
那此时我们更改SpringBoot的版本后,再进行一波测试,看看现在是不是调用的SimpleAsyncTaskExecutor
处理器:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.9.RELEASE</version>
</parent>
2
3
4
5
最后也成功得到我需要的结果了,在 SpringBoot 2.0.9 版本及以前使用的确实是 SimpleAsyncTaskExecutor
处理器;
# 自定义异步线程池
由上我们知道默认情况下,Spring 使用的 Executor 是 SimpleAsyncTaskExecutor
,SimpleAsyncTaskExecutor
每次执行任务时启动一个新的线程,它并不是真正的线程池,因为它不重用线程。很多时候,这种实现方式不符合我们的业务场景,因此通常我们都会自定义一个 Executor 来替换 SimpleAsyncTaskExecutor
。
对于自定义 Executor(自定义线程池),可以分为如下两个层级:
- 应用层级:即全局生效的 Executor。依据 Spring 默认搜索机制,其实就是配置一个全局唯一的
TaskExecutor
实例或者一个名称为taskExecutor
的Executor实例即可。 - 方法层级:即为单独一个或多个方法指定运行线程池,其他未指定的异步方法运行在默认线程池。
# 应用层级自定义线程池
在应用层级自定义线程池,意味着所有未显式指定执行器的@Async
注解的方法都会使用这个自定义的线程池执行。你可以通过实现AsyncConfigurer
接口并重写getAsyncExecutor
方法来实现这一点。
@Configuration
@EnableAsync
public class ExecutorConfig implements AsyncConfigurer {
/**
* 定义全局异步线程池。
* @return 自定义的Executor
*/
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 设置核心线程数为CPU核心数
executor.setCorePoolSize(Runtime.getRuntime().availableProcessors());
// 设置最大线程数
executor.setMaxPoolSize(20);
// 设置队列容量
executor.setQueueCapacity(10);
// 设置线程名的前缀
executor.setThreadNamePrefix("App-Level-Async-");
// 设置拒绝策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
// 初始化Executor
executor.initialize();
return executor;
}
/**
* 自定义异常处理机制。
* @return 自定义的异常处理器
*/
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) -> System.err.println("异常处理:" + ex.getMessage());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 方法层级自定义线程池
如果你想对特定的@Async
方法使用不同的线程池,可以通过定义多个执行器(Executor)Bean来实现。然后在@Async
注解中通过指定执行器的Bean名称来选择不同的线程池。
@Configuration
public class ExecutorConfig {
/**
* 定义特定方法的异步线程池。
* @return 配置好的TaskExecutor
*/
@Bean("methodLevelExecutor")
public TaskExecutor methodLevelExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 设置核心线程数
executor.setCorePoolSize(4);
// 设置最大线程数
executor.setMaxPoolSize(20);
// 设置队列容量
executor.setQueueCapacity(100);
// 设置线程名的前缀
executor.setThreadNamePrefix("Method-Level-Async-");
// 初始化Executor
executor.initialize();
return executor;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
在@Async
方法上使用自定义线程池:
@Service
public class AsyncService {
/**
* 使用指定的线程池执行异步任务。
* @throws InterruptedException 如果执行被中断
*/
@Async("methodLevelExecutor")
public void asyncMethodWithCustomExecutor() throws InterruptedException {
// 模拟耗时任务
System.out.println("执行异步任务:" + Thread.currentThread().getName());
Thread.sleep(5000);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
在这个案例中,asyncMethodWithCustomExecutor
方法会使用名为methodLevelExecutor
的线程池执行。如果你在@Async
注解中不指定线程池名称,则会使用全局的自定义线程池(如果配置了的话)或Spring的默认线程池(SimpleAsyncTaskExecutor
)执行。
通过这种方式,你可以根据不同的业务需求灵活地控制异步任务的执行环境,优化应用的性能和资源利用率。
# 自定义异步异常处理器
在Spring的异步执行框架中,处理异步方法中发生的未捕获异常是一个重要的考虑点。默认情况下,如果异步方法抛出异常,它们可能不会被调用者捕获,因为异步方法的调用立即返回而不是等待异步方法的执行结果。因此,Spring提供了一个机制来全局处理这些未捕获的异步异常,通过实现AsyncConfigurer
接口并重写getAsyncUncaughtExceptionHandler
方法。
@Configuration
@EnableAsync
public class ExecutorConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
// 省略自定义线程池配置...
}
/**
* 自定义异常处理机制。
* 这个方法返回一个 AsyncUncaughtExceptionHandler 实例,用于处理异步方法中未捕获的异常。
* @return 自定义的异常处理器
*/
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
// 返回一个自定义的 AsyncUncaughtExceptionHandler
return (throwable, method, params) -> {
// 这里可以自定义异常处理逻辑
System.err.println("异步任务异常处理:");
System.err.println("Exception message - " + throwable.getMessage());
System.err.println("Method name - " + method.getName());
for (Object param : params) {
System.err.println("Parameter value - " + param);
}
};
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
在这个自定义的异常处理器中,getAsyncUncaughtExceptionHandler
方法返回了一个AsyncUncaughtExceptionHandler
匿名类的实例。这个实例覆盖了handleUncaughtException
方法,该方法在异步方法抛出未捕获异常时被调用。
方法参数说明:
throwable
:抛出的异常对象。method
:抛出异常的异步方法。params
:异步方法调用时的参数列表。
通过自定义异常处理器,你可以对异步方法中的所有未捕获异常进行集中处理,比如记录日志、发送报警通知等。