第51篇-Kotlin协程在Spring-Boot中的应用
【Kotlin + Spring Boot 4 从零到架构师】第 51 篇:Kotlin 协程在 Spring Boot 中的应用
本系列定位:零基础入门,从 Kotlin 语法一路到 Spring Boot 4 高级架构(DDD + Modulith),适合 Java 开发者转型,也适合纯新手系统学习。
本篇你将学到
- Spring Boot 4 对 Kotlin 协程的支持
suspendController 函数- 协程与 JDBC / JPA 的桥接
- JDK 21 虚拟线程 vs Kotlin 协程的对比
学完本篇,你将能在 mini-shop 中运用协程处理并发请求,提升吞吐量。
一、Spring Boot 协程支持
1.1 suspend Controller
Spring MVC 从 5.3 开始支持 Kotlin 协程。Controller 方法可以直接声明为 suspend:
@RestController
@RequestMapping("/api/products")
class ProductController(
private val productService: ProductService
) {
// suspend 函数——Spring 会自动在协程线程池中执行
@GetMapping("/{id}")
suspend fun getById(@PathVariable id: Long): ApiResponse<ProductResponse> {
val result = productService.findByIdAsync(id)
return ApiResponse.success(result)
}
// 多个 suspend 调用可以并发
@GetMapping("/{id}/detail")
suspend fun getDetail(@PathVariable id: Long): ApiResponse<ProductDetail> {
// 并发获取商品信息和评价
val productDeferred = async { productService.findByIdAsync(id) }
val reviewsDeferred = async { reviewService.findByProductIdAsync(id) }
val product = productDeferred.await()
val reviews = reviewsDeferred.await()
return ApiResponse.success(ProductDetail(product, reviews))
}
}
1.2 为什么用 suspend Controller
传统(阻塞):
请求线程 → 查数据库(阻塞等待)→ 返回
100 个并发请求 = 100 个线程被阻塞
协程(非阻塞):
请求线程 → 查数据库(挂起,释放线程)→ 数据返回后恢复
100 个并发请求可以用少量线程处理
Spring Boot 在底层把 suspend 函数转换为响应式处理,但你写的是同步风格的代码——这就是协程的魅力。
下面是传统阻塞模型与协程非阻塞模型的流程对比图:
二、协程 Service 层
2.1 suspend Service 方法
@Service
class ProductService(
private val productRepository: ProductRepository
) {
// suspend 方法
suspend fun findByIdAsync(id: Long): ProductResponse {
// JPA 是阻塞的,需要用 withContext 切换到 IO 线程
return withContext(Dispatchers.IO) {
val product = productRepository.findById(id)
.orElseThrow { ResourceNotFoundException("商品", id) }
product.toResponse()
}
}
// 并发查询多个商品
suspend fun findByIdsAsync(ids: List<Long>): List<ProductResponse> {
return ids.map { id ->
async { findByIdAsync(id) }
}.awaitAll()
}
}
2.2 为什么需要 withContext(Dispatchers.IO)
JDBC 是阻塞式 API——调用时会占住线程等待数据库响应。如果在协程中直接调用,会阻塞协程线程。
withContext(Dispatchers.IO) 把阻塞操作切换到专门的 IO 线程池,不阻塞协程线程:
suspend fun query(id: Long): Product = withContext(Dispatchers.IO) {
// 这里的阻塞代码在 IO 线程池执行
productRepository.findById(id).orElseThrow()
}
三、R2DBC 响应式数据库(进阶)
3.1 JDBC vs R2DBC
| 维度 | JDBC | R2DBC |
|---|---|---|
| 模型 | 阻塞(一个连接占一个线程) | 响应式(非阻塞) |
| 协程支持 | 需要 withContext 桥接 |
原生支持 suspend |
| Spring Data | Spring Data JPA | Spring Data R2DBC |
| 成熟度 | 非常成熟 | 较新 |
3.2 R2DBC Repository
dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-r2dbc")
runtimeOnly("org.postgresql:r2dbc-postgresql")
}
// R2DBC Repository —— 原生 suspend 支持
interface ProductRepository : R2dbcRepository<Product, Long> {
// 所有方法自动支持协程
suspend fun findById(id: Long): Product?
suspend fun findByCategory(category: String): List<Product>
}
// Service 直接用,不需要 withContext
@Service
class ProductService(
private val productRepository: ProductRepository
) {
suspend fun findById(id: Long): ProductResponse? {
val product = productRepository.findById(id) // ← 原生 suspend
return product?.toResponse()
}
}
选择建议:
- 新项目、追求极致性能:R2DBC
- 已有 JPA 项目、功能丰富:JPA +
withContext桥接- mini-shop 系列:用 JPA(前面全部代码),协程部分作为可选方案
四、JDK 21 虚拟线程
4.1 什么是虚拟线程
JDK 21 引入了虚拟线程(Virtual Thread / Project Loom)——JVM 级别的轻量级线程:
传统线程(平台线程):1 个线程 = 1 个 OS 线程(~1MB 内存)
虚拟线程:N 个虚拟线程 → 1 个载体线程(~KB 级内存)
4.2 启用虚拟线程
spring:
threads:
virtual:
enabled: true # Spring Boot 自动用虚拟线程处理请求
一行配置!Spring Boot 会把 Tomcat 的请求处理线程换成虚拟线程。
4.3 虚拟线程 vs 协程
| 维度 | 虚拟线程 | Kotlin 协程 |
|---|---|---|
| 语言级别 | JVM(Java/Kotlin 都能用) | Kotlin 语言特性 |
| 编程模型 | 看起来和普通线程一样 | suspend 函数 |
| 学习成本 | 极低(几乎零改动) | 需要理解协程概念 |
| 生态 | 兼容所有阻塞 API(JDBC/JPA) | 需要协程友好的库 |
| 性能 | 优秀 | 优秀 |
| 调试 | 堆栈追踪可能很长 | 结构化并发,调试友好 |
4.4 选择建议
| 场景 | 推荐 |
|---|---|
| 已有 Spring Boot + JPA 项目 | 虚拟线程(零代码改动) |
| 纯 Kotlin 新项目 | 协程(suspend 风格更优雅) |
| 需要复杂异步编排 | 协程(async/await + 结构化并发) |
| 想要最简单的方案 | 虚拟线程 |
mini-shop 的建议:如果你不想改动已有代码,直接启用虚拟线程(一行配置),就能获得大部分协程的吞吐量提升。
下面是虚拟线程与 Kotlin 协程的选择决策流程图:
五、性能对比
5.1 模拟高并发场景
// 阻塞式 Controller(传统)
@GetMapping("/blocking/{id}")
fun getByIdBlocking(@PathVariable id: Long): ProductResponse {
Thread.sleep(100) // 模拟 IO 延迟
return productService.findById(id)
}
// 协程式 Controller
@GetMapping("/coroutine/{id}")
suspend fun getByIdCoroutine(@PathVariable id: Long): ProductResponse {
delay(100) // 挂起 100ms(不阻塞线程)
return withContext(Dispatchers.IO) {
productService.findById(id)
}
}
100 个并发请求的性能对比(概念值):
| 方式 | 线程数 | 总耗时 | 说明 |
|---|---|---|---|
| 阻塞 | 200 线程 | ~100ms | 每个请求占一个线程 |
| 协程 | 少量线程 | ~100ms | 挂起不占线程 |
| 虚拟线程 | 100 虚拟线程 | ~100ms | JVM 调度 |
实际提升取决于 IO 操作的占比。IO 密集型应用(大量数据库查询/HTTP 调用)受益最大。
下面是 100 个并发请求下,阻塞、协程、虚拟线程三种方式的处理时序图:
本篇小结
| 知识点 | 核心内容 |
|---|---|
suspend Controller |
Spring MVC 原生支持协程 |
withContext(Dispatchers.IO) |
桥接阻塞式 JDBC |
| R2DBC | 响应式数据库,原生 suspend 支持 |
| 虚拟线程 | JDK 21,一行配置启用 |
| 虚拟线程 vs 协程 | 虚拟线程零改动,协程更灵活 |
async { }.awaitAll() |
协程并发执行多个查询 |
| 适用场景 | IO 密集型应用受益最大 |
下篇预告
全系列的收官篇!下一篇总结 Spring Boot 性能调优的方方面面,并给出一份完整的生产上线检查清单。
如果本篇内容对你有帮助,欢迎点赞收藏!有任何疑问,欢迎在评论区交流。
更多推荐



所有评论(0)