上一篇我们讲了"线程池与异步"的原理,这篇直接上手:写一个最小可跑的 Spring Boot 异步 Demo 。
目标很明确:
调用接口立即返回,但后台任务继续跑(线程池执行)。
1. 项目目标与效果
实现接口:
-
GET /register
效果:
- 接口秒返回 :
注册成功 - 控制台稍后输出(模拟发邮件/写日志等慢任务):
开始发邮件...邮件发送完成...
2. 工程目录结构
async-demo
├── pom.xml
└── src
└── main
├── java
│ └── com/example/asyncdemo
│ ├── AsyncDemoApplication.java
│ ├── config
│ │ ├── AsyncPoolProperties.java
│ │ └── ThreadPoolConfig.java
│ ├── controller
│ │ └── UserController.java
│ └── service
│ └── AsyncService.java
└── resources
└── application.yml
3. pom.xml(最小依赖)
只要一个 Web 依赖就够了(因为我们就写个接口 + 异步)
XML
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>async-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>async-demo</name>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.6</version>
<relativePath/>
</parent>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- Web:提供 Controller / Tomcat 等 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 可选:仅用于开发热更新 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 打包运行 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
说明:我用的是 Spring Boot 3.x + Java 17(当前企业非常常见)。如果你是 2.x 也能跑,写法基本一致。
- application.yml(把线程池参数外置)
XML
server:
port: 8080
# 自定义线程池参数(我们自己定义配置前缀 async.pool)
async:
pool:
core-size: 5
max-size: 10
queue-capacity: 50
keep-alive-seconds: 60
thread-name-prefix: async-
为什么要放 yml?
- 线程池参数经常需要调(压测后调整)
- 不用改代码就能调参
- 工程习惯就是"参数外置"
5. 启动类:开启异步能力
AsyncDemoApplication.java
java
package com.example.asyncdemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
@EnableAsync
@SpringBootApplication
public class AsyncDemoApplication {
public static void main(String[] args) {
SpringApplication.run(AsyncDemoApplication.class, args);
}
}
关键点:
@EnableAsync:不加它,@Async不会生效
6. 配置属性类:把 yml 读成对象
AsyncPoolProperties.java
java
package com.example.asyncdemo.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "async.pool")
public class AsyncPoolProperties {
private int coreSize = 5;
private int maxSize = 10;
private int queueCapacity = 50;
private int keepAliveSeconds = 60;
private String threadNamePrefix = "async-";
public int getCoreSize() {
return coreSize;
}
public void setCoreSize(int coreSize) {
this.coreSize = coreSize;
}
public int getMaxSize() {
return maxSize;
}
public void setMaxSize(int maxSize) {
this.maxSize = maxSize;
}
public int getQueueCapacity() {
return queueCapacity;
}
public void setQueueCapacity(int queueCapacity) {
this.queueCapacity = queueCapacity;
}
public int getKeepAliveSeconds() {
return keepAliveSeconds;
}
public void setKeepAliveSeconds(int keepAliveSeconds) {
this.keepAliveSeconds = keepAliveSeconds;
}
public String getThreadNamePrefix() {
return threadNamePrefix;
}
public void setThreadNamePrefix(String threadNamePrefix) {
this.threadNamePrefix = threadNamePrefix;
}
}
7. 线程池配置类:真正创建线程池 Bean
ThreadPoolConfig.java
java
package com.example.asyncdemo.config;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
@Configuration
@EnableConfigurationProperties(AsyncPoolProperties.class)
public class ThreadPoolConfig {
/**
* 给 @Async 指定的线程池(名字要对应 @Async("taskExecutor"))
*/
@Bean(name = "taskExecutor")
public Executor taskExecutor(AsyncPoolProperties props) {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(props.getCoreSize());
executor.setMaxPoolSize(props.getMaxSize());
executor.setQueueCapacity(props.getQueueCapacity());
executor.setKeepAliveSeconds(props.getKeepAliveSeconds());
executor.setThreadNamePrefix(props.getThreadNamePrefix());
// 这个策略很重要:队列满了 + 线程也到上限时,让调用方线程自己执行(削峰,避免直接丢任务)
executor.setRejectedExecutionHandler(new java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
这里我额外加了一个"工程必备"的点:
CallerRunsPolicy():线程池打满时不直接丢任务,而是让请求线程自己执行,用来削峰(你后面学限流/降级会更懂它的价值)
8. 异步业务类:@Async 真正起作用的地方
AsyncService.java
java
package com.example.asyncdemo.service;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
public class AsyncService {
/**
* 注意:必须 public
* 注意:不要在同一个类里 self-invoke 调用(否则 @Async 失效)
*/
@Async("taskExecutor")
public void sendWelcomeEmail(String username) {
try {
System.out.println("【异步任务】开始发邮件,用户=" + username +
",线程=" + Thread.currentThread().getName());
// 模拟耗时 3 秒
Thread.sleep(3000);
System.out.println("【异步任务】邮件发送完成,用户=" + username +
",线程=" + Thread.currentThread().getName());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("【异步任务】被中断,用户=" + username);
}
}
@Async("taskExecutor")
public void writeAuditLog(String action) {
try {
System.out.println("【异步任务】开始写日志,action=" + action +
",线程=" + Thread.currentThread().getName());
Thread.sleep(1500);
System.out.println("【异步任务】日志写入完成,action=" + action +
",线程=" + Thread.currentThread().getName());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("【异步任务】日志写入被中断,action=" + action);
}
}
}
我这里放了两个异步任务,让你更直观看到:
- 一个接口触发多个后台任务
- 都跑在线程池线程里(
async-前缀)
9. Controller:接口秒返回,但触发异步任务
UserController.java
java
package com.example.asyncdemo.controller;
import com.example.asyncdemo.service.AsyncService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
@RestController
public class UserController {
private final AsyncService asyncService;
public UserController(AsyncService asyncService) {
this.asyncService = asyncService;
}
@GetMapping("/register")
public String register() {
String username = "lingpei";
String action = "register@" + LocalDateTime.now();
// 触发异步任务(不阻塞接口返回)
asyncService.sendWelcomeEmail(username);
asyncService.writeAuditLog(action);
// 立即返回
return "注册成功(接口秒返回),后台任务执行中...";
}
}
10. 运行验证(你照着做就能看到效果)
启动:
java
mvn spring-boot:run
请求:
java
curl "http://localhost:8080/register"
你会看到:
- curl 立刻返回:
注册成功... - 控制台输出类似:
java
【异步任务】开始发邮件... 线程=async-1
【异步任务】开始写日志... 线程=async-2
【异步任务】日志写入完成...
【异步任务】邮件发送完成...
这就证明:
- Controller 没等任务执行完就返回了
- 任务在你定义的线程池里跑
11. 常见坑(@Async 失效 99% 都是这几个)
坑 1:没加 @EnableAsync
✅ 解决:启动类加上。
坑 2:异步方法不是 public
✅ 解决:必须 public。
坑 3:同类内部调用(self-invoke)
例如:
java
this.sendWelcomeEmail();
✅ 解决:必须通过 Spring 注入的代理对象调用(也就是通过别的 Bean 调用它)。
坑 4:没指定线程池,默认线程池不可控
✅ 解决:自定义 ThreadPoolTaskExecutor,并用 @Async("taskExecutor") 指定。
12. 一句话总结
线程池:控制并发资源,避免线程爆炸
异步:把慢任务从主流程挪走,让接口秒返回
工程关键点:线程池参数外置 + 拒绝策略 + 避坑点
到这里,你已经具备后端并发的"最小实战闭环"。
下一篇: