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


原始标题:Kotlin Reactive Microservice With Spring Boot