1. 概述
本文将带你使用 Kotlin 和 Spring Boot 开发一个响应式微服务应用。
该应用会暴露 REST 接口,通过数据库持久化数据,并提供监控相关的管理接口。整体技术栈基于响应式编程模型(Reactive),适合高并发、低延迟的场景。
✅ 核心技术:Spring WebFlux + R2DBC + Kotlin Coroutines
❌ 不涉及:安全性、服务间调用、API 网关等进阶内容
2. 业务场景
现代人越来越关注健康问题,因此我们设计了一个“健康追踪器”应用作为示例。
用户可以:
- 创建个人健康档案(Profile)
- 记录每日症状,如体温、血压、心率等
- 查看历史记录及统计平均值
目标简单明了:帮助用户管理健康数据,顺便学习 Reactive 编程在真实项目中的落地方式。
3. 项目初始化
我们使用 Maven 进行依赖管理(当然 Gradle 也完全没问题)。
首先继承 spring-boot-starter-parent
:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
<relativePath/>
</parent>
必要依赖说明
以下是关键依赖项及其作用:
依赖 | 用途 |
---|---|
kotlin-reflect , kotlin-stdlib |
支持 Kotlin 反射与标准库 |
spring-boot-starter-webflux |
提供响应式 Web 接口支持 |
jackson-module-kotlin |
JSON 序列化/反序列化 Kotlin 数据类 |
spring-boot-starter-data-r2dbc |
响应式数据库访问(非阻塞 I/O) |
r2dbc-h2 |
内存数据库 H2 的 R2DBC 驱动 |
spring-boot-starter-actuator |
应用监控与管理接口 |
添加 H2 数据库驱动:
<dependency>
<groupId>io.r2dbc</groupId>
<artifactId>r2dbc-h2</artifactId>
<version>0.8.0.RELEASE</version>
</dependency>
⚠️ 注意:R2DBC 目前已有 Postgres、MySQL、SQL Server 等生产级驱动,H2 仅用于演示。
最后是主启动类:
@SpringBootApplication
class HealthTrackerApplication
fun main(args: Array<String>) {
runApplication<HealthTrackerApplication>(*args)
}
📌 小知识:runApplication<>()
是 Spring Boot for Kotlin 提供的 DSL 扩展,等价于传统的 SpringApplication.run()
调用。
4. 数据模型设计
Profile(用户档案)
@Table
data class Profile(
@Id var id: Long?,
var firstName: String,
var lastName: String,
var birthDate: LocalDateTime
)
@Table
:由 Spring Data 注解,自动映射为数据库表名(默认转下划线命名)@Id
:标识主键字段
HealthRecord(健康记录)
@Table
data class HealthRecord(
@Id var id: Long?,
var profileId: Long?,
var temperature: Double,
var bloodPressure: Double,
var heartRate: Double,
var date: LocalDate
)
⚠️ 踩坑提醒:Spring Data R2DBC 当前不支持实体关联(如 @OneToOne
, @OneToMany
),所以不能直接嵌套 Profile
对象。只能手动维护外键 profileId
。
这也是目前使用 R2DBC 时最常见的限制之一,需自行处理逻辑关联。
5. 数据库配置
R2DBC 不支持自动 DDL 生成(比如 Hibernate 的 hbm2ddl.auto
),所以我们需要手动创建表结构。
通过 DatabaseClient
在应用启动时执行建表语句:
@Configuration
class DBConfiguration(db: DatabaseClient) {
init {
val initDb = db.sql {
"""
CREATE TABLE IF NOT EXISTS profile (
id BIGSERIAL PRIMARY KEY,
first_name VARCHAR(255),
last_name VARCHAR(255),
birth_date TIMESTAMP
);
CREATE TABLE IF NOT EXISTS health_record (
id BIGSERIAL PRIMARY KEY,
profile_id BIGINT NOT NULL,
temperature DOUBLE PRECISION,
blood_pressure DOUBLE PRECISION,
heart_rate DOUBLE PRECISION,
date DATE
);
"""
}
initDb.then().subscribe()
}
}
📌 建议:复杂项目中可考虑结合 Flyway 或 Liquibase 管理 schema 版本,避免硬编码 SQL。
6. 仓库层(Repository)
Spring Data R2DBC 提供了响应式的 CRUD 操作接口。
ProfileRepository
@Repository
interface ProfileRepository : ReactiveCrudRepository<Profile, Long>
开箱即用的方法包括:
save(entity)
findById(id)
findAll()
deleteById(id)
HealthRecordRepository
@Repository
interface HealthRecordRepository : ReactiveCrudRepository<HealthRecord, Long> {
@Query("SELECT * FROM health_record WHERE profile_id = :profileId")
fun findByProfileId(profileId: Long): Flux<HealthRecord>
}
⚠️ 踩坑提醒:R2DBC 当前不支持方法名推导查询(query derivation),例如 findByProfileId()
这种命名无法自动生成 SQL,必须显式写 @Query
。
这是另一个常见的“意料之外”的限制,建议提前知晓。
7. 控制器层(Controller)
ProfileController
负责用户档案的创建:
@RestController
class ProfileController(val repository: ProfileRepository) {
@PostMapping("/profile")
fun save(@RequestBody profile: Profile): Mono<Profile> = repository.save(profile)
}
简洁明了,利用构造器注入 ProfileRepository
,返回 Mono<Profile>
表示单个异步结果。
HealthRecordController
包含两个核心接口:
@RestController
class HealthRecordController(val repository: HealthRecordRepository) {
// 存储健康记录
@PostMapping("/health/{profileId}/record")
fun storeHealthRecord(
@PathVariable("profileId") profileId: Long,
@RequestBody record: HealthRecord
): Mono<HealthRecord> {
record.profileId = profileId
return repository.save(record)
}
// 获取某用户的健康指标均值
@GetMapping("/health/{profileId}/avg")
fun fetchHealthRecordAverage(@PathVariable("profileId") profileId: Long): Mono<AverageHealthStatus> =
repository.findByProfileId(profileId)
.reduce(AverageAccumulator()) { acc, hr ->
acc.apply(hr)
}
.map { acc -> AverageHealthStatus(acc.count, acc.avgTemp(), acc.avgBP(), acc.avgHR()) }
}
其中 AverageHealthStatus
是封装返回值的数据类:
data class AverageHealthStatus(
var cnt: Int,
var temperature: Double,
var bloodPressure: Double,
var heartRate: Double
)
📌 提示:reduce()
是典型的响应式聚合操作,替代了传统的循环累加,避免中间状态被并发修改。
8. 监控接口(Actuator)
Spring Boot Actuator 提供了开箱即用的应用监控能力。
添加 spring-boot-starter-actuator
后,默认启用 /health
和 /info
接口。
启用更多监控端点
在 application.yml
中配置:
management:
endpoints:
web:
exposure:
include: health,metrics
也可以设为 *
暴露所有接口,但 ❌ 强烈不推荐在生产环境这样做,存在安全风险。
查看已启用接口
访问 http://localhost:8080/actuator 可查看当前开放的所有监控接口。
健康检查示例
请求 http://localhost:8080/actuator/health,返回:
{
"status": "UP"
}
表示应用运行正常。后续还可集成数据库、磁盘、外部服务等健康指标。
9. 接口测试
使用 WebTestClient
对控制器进行集成测试,无需启动完整服务器。
测试类定义
@SpringBootTest
class ProfileControllerTest {
@Autowired
lateinit var controller: ProfileController
private lateinit var client: WebTestClient
@BeforeEach
fun setup() {
client = WebTestClient.bindToController(controller).build()
}
@Test
fun whenRequestProfile_thenStatusShouldBeOk() {
val profile = Profile(null, "John", "Doe", LocalDateTime.now())
client.post()
.uri("/profile")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(profile)
.exchange()
.expectStatus().isOk
}
}
✅ 优点:
bindToController()
方式轻量,适合单元测试级别验证逻辑- 避免启动整个 Web 容器,提升测试速度
⚠️ 注意事项:
- 若需测试跨组件行为或全局拦截器,建议改用
.bindToServer().configureClient()
- 确保测试数据隔离,避免副作用
10. 总结
本文完整实现了一个基于 Kotlin + Spring Boot 的响应式微服务原型,涵盖以下核心能力:
✅ 响应式 REST 接口(WebFlux)
✅ 非阻塞数据库访问(R2DBC)
✅ 实体映射与手动 SQL 控制
✅ 监控管理接口(Actuator)
✅ 自动化测试方案(WebTestClient)
当前 R2DBC 的主要限制(务必注意)
限制项 | 解决方案建议 |
---|---|
不支持实体关联 | 手动维护外键,业务层处理关联逻辑 |
不支持 Query Derivation | 显式编写 @Query |
无自动 DDL 生成 | 使用 Flyway/Liquibase 或初始化脚本 |
虽然 R2DBC 仍在发展中,但在 IO 密集型场景下优势明显。随着生态完善,未来有望成为响应式系统的首选数据访问方式。
完整代码示例可在 GitHub 获取:https://github.com/Baeldung/kotlin-tutorials/tree/master/spring-reactive-kotlin