Spring Boot 与 Kotlin

Kotlin 可以直接运行在 JVM 上,并且与 Java 库互操作。因此 Spring Framework 和 Spring Boot 都可以与 Kotlin 一起使用。Spring 官方文档称 Spring Framework 为 Kotlin 提供一等支持,Spring Boot 也提供专门的 Kotlin 支持。对很多 Java 后端团队来说,这通常是引入 Kotlin 的最低风险路径:保留 Spring 生态、部署方式和运维体系,把部分代码逐步改成 Kotlin。

为什么 Spring Kotlin 值得单独学习

把 Java Spring 项目文件后缀改成 .kt 并不等于写好了 Kotlin。真正的差异在这些地方:

  • Kotlin 类默认 final,而 Spring 代理经常需要类或方法可继承。
  • Kotlin 默认非空,Spring API 的 nullability 需要通过注解传给 Kotlin 编译器。
  • Kotlin 构造函数、属性、数据类会改变依赖注入和 JSON 绑定写法。
  • Kotlin 顶层函数、扩展函数、单表达式函数让入口代码和工具 API 更简洁。
  • Kotlin 的协程可以与 Spring WebFlux 等响应式场景结合,但不能简单等同于 Java CompletableFuture

Java 对比:Java 写 Spring 主要学习注解和容器模型;Kotlin 写 Spring 还要理解 Kotlin 编译器插件、空安全、反射、默认 final、数据类生成方法和 JVM 字节码互操作。

创建项目

官方 Kotlin Spring Boot 教程使用 IntelliJ IDEA 的 Spring Boot 项目向导,也可以使用 start.spring.io。常见选择:

  • Language:Kotlin。
  • Build:Gradle Kotlin 或 Maven。
  • Java:至少选择 Spring Boot 当前支持的 JDK 版本。官方教程示例选择 Java 17。
  • Dependencies:Spring Web、Spring Data JDBC、H2 Database 等。

生成后的目录仍符合 Maven 标准布局:

src/main/kotlin
src/main/resources
src/test/kotlin

也就是说,Kotlin 不改变 Spring Boot 的整体项目模型。你仍然会看到 application 配置、测试目录、Gradle/Maven 构建脚本和启动入口。

Gradle Kotlin 配置

一个 Spring Boot Kotlin 项目通常会包含:

plugins {
    kotlin("jvm") version "2.2.21"
    kotlin("plugin.spring") version "2.2.21"
    id("org.springframework.boot") version "4.0.2"
    id("io.spring.dependency-management") version "1.1.7"
}

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-webmvc")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("tools.jackson.module:jackson-module-kotlin")

    testImplementation("org.springframework.boot:spring-boot-starter-webmvc-test")
    testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

kotlin {
    compilerOptions {
        freeCompilerArgs.addAll(
            "-Xjsr305=strict",
            "-Xannotation-default-target=param-property",
        )
    }
}

tasks.withType<Test> {
    useJUnitPlatform()
}

版本号应以你创建项目时的 Spring Initializr 或官方文档为准,不要在老项目里机械复制。这里更重要的是理解每个组件的作用:

  • kotlin("jvm"):启用 Kotlin/JVM 编译。
  • kotlin("plugin.spring"):让 Spring 注解类自动 open,解决 Kotlin 默认 final 与 Spring 代理之间的冲突。
  • kotlin-reflect:Spring 对 Kotlin 构造函数、参数名、注解和反射信息的支持经常需要它。
  • jackson-module-kotlin:让 Jackson 正确处理 Kotlin 构造函数、非空类型、默认参数和数据类。
  • -Xjsr305=strict:让 Kotlin 更严格地理解 Java/Spring API 上的空性注解。
  • -Xannotation-default-target=param-property:配合 Kotlin 2.2 的注解默认目标变化,减少 Spring 注解落点歧义。

Java 对比:Java Spring 项目通常不需要 all-open 插件,因为 Java 类和方法默认可继承;Kotlin 正好相反,默认 final 是语言层面的安全选择,但与基于代理的框架结合时需要编译器插件协调。

应用入口

Kotlin Spring Boot 入口通常写成:

package com.example.demo

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class DemoApplication

fun main(args: Array<String>) {
    runApplication<DemoApplication>(*args)
}

这里有几个 Kotlin 点:

  • DemoApplication 没有成员,所以可以省略类体 {}
  • main 是顶层函数,不需要像 Java 那样放进类里。
  • runApplication<DemoApplication>(*args) 使用泛型和展开操作符。
  • *argsArray<String> 展开为可变参数。

Java 对比:

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

Kotlin 入口更短,但语义相同:启动 Spring Boot 应用上下文,执行自动配置和组件扫描。

Controller 写法

一个最小 REST Controller:

package com.example.demo

import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController

@RestController
class MessageController {
    @GetMapping("/")
    fun index(@RequestParam name: String): String =
        "Hello, $name!"
}

Kotlin 写法里值得注意:

  • 单表达式函数可以省略 { return ... }
  • 字符串模板替代 Java 的字符串拼接。
  • name: String 默认非空。如果请求缺少 name,Spring 绑定层会先处理,而不是把 null 塞进 Kotlin 非空参数。
  • 返回类型可以推断,但公共 API 中建议显式写出,尤其是库、Controller 契约、跨模块接口。

Java 对比:

@RestController
class MessageController {
    @GetMapping("/")
    String index(@RequestParam String name) {
        return "Hello, " + name + "!";
    }
}

Kotlin 代码少,但不要为了少写几行牺牲契约清晰度。Controller 返回类型、请求 DTO、响应 DTO 建议明确。

构造函数注入

Kotlin 与 Spring 最自然的依赖注入方式是构造函数注入:

import org.springframework.stereotype.Service
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController

@Service
class GreetingService {
    fun greeting(name: String): String = "Hello, $name!"
}

@RestController
class GreetingController(
    private val greetingService: GreetingService,
) {
    @GetMapping("/greeting")
    fun greeting(): String = greetingService.greeting("Kotlin")
}

优势:

  • 依赖不可变,使用 val
  • 没有字段注入导致的隐藏可变状态。
  • 单元测试时可以直接传入 mock 或 fake。
  • 不需要 @Autowired 标在唯一构造函数上。

Java 对比:现代 Java Spring 也推荐构造函数注入,但 Kotlin 的主构造函数让这种写法更自然。

数据类作为 DTO

Kotlin 数据类非常适合请求响应 DTO:

data class CreateUserRequest(
    val name: String,
    val email: String,
)

data class UserResponse(
    val id: Long,
    val name: String,
    val email: String,
)

优点:

  • 自动生成 equals()hashCode()toString()copy()
  • 构造函数参数就是属性。
  • 与 Jackson Kotlin 模块配合后,可以自然地从 JSON 绑定到构造函数。

但要注意:

  • 非空属性必须在 JSON 中提供,或者定义默认值。
  • 对外响应 DTO 不要直接复用 JPA Entity。
  • 对外发布的库 API 不建议随意暴露 data class,因为增删主构造函数属性会影响二进制兼容和调用方源码。

JPA Entity 的特殊性

Spring Data JPA 与 Kotlin 结合时要格外谨慎,因为 JPA 依赖代理、无参构造、延迟加载和可变属性,而 Kotlin 倾向不可变、final、主构造函数。

常见建议:

  • Entity 不要直接写成普通业务 DTO 风格的数据类。
  • 需要 JPA 时了解 kotlin-jpa 或 no-arg 插件。
  • 延迟加载字段不要轻易放进 data class 的主构造函数,否则 toString()equals() 可能触发意外加载。
  • DTO 与 Entity 分开,Controller 不直接返回 Entity。

Java 对比:Java Entity 通常天然满足可继承、无参构造和可变字段习惯;Kotlin 需要通过插件或更明确的建模方式适配 JPA。

空安全与 Spring API

Kotlin 的空安全在 Spring 项目中有两层:

  1. Kotlin 自己的类型系统:String 非空,String? 可空。
  2. Java/Spring API 的空性注解:通过 JSpecify、JSR-305 等注解让 Kotlin 编译器理解 Java API 是否可空。

建议:

  • 新项目启用严格空性检查,例如 -Xjsr305=strict
  • 对 Controller 入参,明确区分必填和可选:
@GetMapping("/search")
fun search(@RequestParam(required = false) keyword: String?): List<String> {
    if (keyword.isNullOrBlank()) return emptyList()
    return listOf("result for $keyword")
}
  • 不要用 !! 处理请求参数或外部输入。
  • Java API 返回的平台类型要尽快在边界转换成明确的 Kotlin 类型。

Java 对比:Java 常用 Optional 表达可能为空的返回值,但很多框架入口仍可能传入 null。Kotlin 把空性放进类型,但前提是 Java 注解信息足够准确。

Kotlin API:扩展函数与 reified 泛型

Spring Boot 和 Spring Framework 的 Kotlin 支持常通过扩展函数提供更自然的 API。例如 runApplication<T>() 就是典型入口。某些测试或客户端 API 也可以借助 reified 泛型减少 Class<T> 参数。

普通 Java 风格:

val body = restTemplate.getForObject(url, UserResponse::class.java)

Kotlin 风格 API 往往可以写得更接近:

val body = restTemplate.getForObject<UserResponse>(url)

这里的关键是 inline + reified,让泛型类型在调用处保留下来。Java 泛型类型擦除导致运行期通常要显式传 Class<T>;Kotlin 在内联函数中可以为一部分场景提供更好语法。

协程与 Spring

Spring 对 Kotlin 协程有支持,但要清楚边界:

  • suspend fun 表示挂起函数,不等于启动新线程。
  • 协程适合表达异步 I/O,不能自动消除阻塞数据库驱动带来的线程占用。
  • Spring MVC、WebFlux、R2DBC、阻塞 JDBC 的线程模型不同,不能混用时只看语法。
  • 如果底层库是阻塞的,仍然要考虑 dispatcher、连接池和限流。

Java 对比:

  • Java 常用 CompletableFuture、Project Reactor、虚拟线程或传统线程池。
  • Kotlin 协程提供顺序代码风格,但需要与框架的运行模型正确对接。

测试

Kotlin Spring Boot 测试可以使用 JUnit 5、kotlin.test、MockK 或 Spring 自带测试工具。Spring Boot 官方文档提到 MockK 是 mock Kotlin 类的常用选择。

一个简单的切片测试:

import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.get

@WebMvcTest(MessageController::class)
class MessageControllerTest(
    @Autowired private val mockMvc: MockMvc,
) {
    @Test
    fun `returns greeting`() {
        mockMvc.get("/") {
            param("name", "Ada")
        }.andExpect {
            status { isOk() }
            content { string("Hello, Ada!") }
        }
    }
}

Kotlin 测试命名可以使用反引号,让测试意图更像自然语言。但团队应统一风格,避免 CI 报告或测试筛选工具不兼容时产生额外成本。

迁移 Java Spring 项目的建议顺序

  1. 先引入 Kotlin 构建插件和测试依赖。
  2. 从测试代码开始写 Kotlin,降低生产风险。
  3. 新增 Controller/DTO/配置类用 Kotlin。
  4. Service 层逐步迁移,保持 Java/Kotlin 双向调用清晰。
  5. 最后再处理 Entity、AOP、复杂配置和底层基础设施。

迁移时不要一次性自动转换整个项目。IntelliJ IDEA 的 Java-to-Kotlin 转换器适合辅助,但转换后的代码经常还保留 Java 习惯,需要人工整理成真正的 Kotlin 风格。

常见误区

误区一:Kotlin Spring 不需要 kotlin-reflect

很多 Spring 场景依赖 Kotlin 反射信息,尤其是构造函数参数、默认值、注解读取、数据绑定等。缺少 kotlin-reflect 可能在运行期出现不直观的问题。

误区二:所有 Spring Bean 都应该写成 data class

data class 适合值对象和 DTO,不适合承载有生命周期、代理、事务或可变状态的 Spring Bean。Service、Controller、Repository 通常写普通类。

误区三:lateinit var 是依赖注入标准写法

lateinit 可以绕过初始化检查,但会引入运行期风险。业务 Bean 优先使用构造函数注入和 val

误区四:有了 Kotlin 空安全就不会 NPE

平台类型、反射、框架注入、JSON 反序列化、!!、未初始化 lateinit 都可能导致运行期错误。空安全能显著减少风险,但不能替代边界校验。

Spring 与 Ktor 的取舍

维度 Spring Boot Kotlin Ktor
生态 Spring 全家桶、企业集成强 Kotlin 原生、轻量
风格 注解、自动配置、DI 容器 DSL、显式插件、模块函数
迁移 Java 项目 非常适合渐进迁移 更像新架构选择
学习成本 Spring 背景团队低 Kotlin-first 团队低
配置复杂度 自动配置多,需要理解约定 显式配置多,需要自己组织

如果你已经在 Java Spring 体系内,Spring Kotlin 是最现实的第一步。如果你在做新 Kotlin 服务,且不需要大量 Spring 基础设施,Ktor 值得优先评估。

官方参考

  • Kotlin Backend development with Kotlin:https://kotlinlang.org/docs/server-overview.html
  • Kotlin Create a Spring Boot project with Kotlin:https://kotlinlang.org/docs/jvm-create-project-with-spring-boot.html
  • Spring Boot Kotlin Support:https://docs.spring.io/spring-boot/reference/features/kotlin.html
  • Spring Framework Kotlin:https://docs.spring.io/spring-framework/reference/languages/kotlin.html