Spring Boot + Kotlin 项目通常需要 Kotlin JVM 插件、Spring 插件、kotlin-reflect。如果使用JPA,还需要kotlin-jpa插件。
plugins { kotlin("jvm") version "$kotlin_version" kotlin("plugin.spring") version "$kotlin_version" kotlin("plugin.jpa") version "$kotlin_version" id("org.springframework.boot") version "$spring_boot_version" id("io.spring.dependency-management") version "$dependency_management_version"}dependencies { implementation("org.springframework.boot:spring-boot-starter-web") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.jetbrains.kotlin:kotlin-reflect") testImplementation("org.springframework.boot:spring-boot-starter-test")}
可空类型:众所周知,基本数据类型不能为null,而包装类(对象)可以为空。当你声明一个变量属性为Int?时,就说明这是一个可以为空的 int 类型,它的值可能为null,这时编译器就会使用 Integer
泛型参数:当将 Int 放入到List<Int>中时,由于Java的泛型擦除机制,JVM层面需要的是对象,因此编译器会使用 Integer
在数据类中设计字段时,如何设置最合理?
不可变性原则优先:不可变性原则是数据类设计的黄金法则,即将变量设置为 val 而非 var
// ❌ 反例(可变数据类)data class User( // 使用var很不安全,这会破坏数据的一致性! var name: String, var age: Int)// ✅ 正例(不可变数据类)data class User( val name: String, val age: Int)
字段选择:谨慎使用可空类型,使用?明确表达可选性
可空字段使用场景:
字段确实可能不存在或未设置
有合理的默认值时使用默认参数
data class Product( val id: Long, // 明确非空 val price: Double, // 必须有价格 val inStock: Boolean // 明确的布尔状态)data class UserProfile( val name: String, val age: Int? = null, // 年龄可能为空 val avatarUrl: String? = null // 头像可选)
集合类型的选择:避免可变集合
// ❌ 反例data class Order( // 外部可能直接修改内部状态 val items: MutableList<OrderItem>)// ✅ 正例data class Order( // 外部只能读取不能修改 val items: List<OrderItem>){ // 如果需要修改,要提供明确的方法 fun addItem(item: OrderItem): Order = copy(items = items + item)}
open class Entity(val id: Long)// ✅ 可以继承普通类data class User(val name: String,val age: Int) : Entity(1L)// 也可以实现接口data class User(val name: String,val age: Int) : Serializable
不能为抽象:抽象数据类无意义
// 反例:抽象数据类abstract data class Response(val success: Boolean)// 正确实现:使用密封类处理不同响应类型sealed class ApiResponse<out T>{ data class Success<T>(val data: T) : ApiResponse<T>() data class Error(val message: String) : ApiResponse<Nothing>()}
数组和集合字段的陷阱
// 数组的equals基于引用比较data class Product(val tags: Array<String>)val p1 = Product(arrayOf("a","b"))val p2 = Product(arrayOf("a","b"))println(p1 == p2) // false!// 解决方案1:使用Listdata class Product(val tags: List<String>)// 解决方案2:如果必须使用数组,则需要自定义equals/logicdata class Product(private val tags: Array<String>){ // 自定义equals逻辑 override fun equals(other: Any?): Boolean{ // 自定义数组内容比较 } // 提供只读视图 fun getTags(): List<String> = tags.toList()}
可变字段破坏数据一致性:字段设置为 val 防止外部修改
深度拷贝问题
data class Company(val name: String,val employees: MutableList<Employee>)data class Employee(val name: String)val company = Company("Tech",mutableListOf(Employee("Alice")))val company1 = company.copy() // 浅拷贝!!company.employees.add(Employee("Bob"))println(company.employees.size) // 2println(company1.employees.size) // 2 因为共享了同一个List引用!!
sealed class BookDto { data class Request( val title: String, val author: String, val isbn: String, val publishedYear: Int ) : BookDto() data class Response( val id: Long?, val title: String, val author: String, val isbn: String, val publishedYear: Int ) : BookDto() data class Summary( val id: Long?, val title: String, val author: String ) : BookDto() data class Detailed( val id: Long?, val title: String, val author: String, val isbn: String, val publishedYear: Int, val formattedInfo: String ) : BookDto() }fun Book.toResponse(): BookDto.Response = BookDto.Response( id = id, title = title, author = author, isbn = isbn, publishedYear = publishedYear )
统一响应类 R
sealed class R<out T> { data class Success<T>(val data: T) : R<T>() data class Error(val message: String, val code: Int = 500) : R<Nothing>() object Loading : R<Nothing>() companion object { fun <T> success(data: T) = Success(data) fun error(message: String, code: Int = 500) = Error(message, code) val loading = Loading } }
数据持久层
@Repositoryinterface BookRepository : JpaRepository<Book, Long>{ fun findByAuthor(author: String): List<Book> fun findByTitleContaining(title: String): List<Book> }
fun square(num: Int): Int = num * numfun main(){ val n1: Int? = 10 // 两种等效输出 非空时执行lambda表达式中的代码,如果为空则不执行 n1?.let{n -> println(square(n))} n1?.let{println(square(it))}}
with 和 apply 函数
with函数:对一个对象执行多个操作,无需重复写对象名
data class Person(var name: String = "",var age: Int = 0,var city: String = "")fun main(){ val person = Person() with(person){ name = "张三" age = 18 city = "北京" } println(person)}
apply函数:配置对象属性并返回对象本身
fun main(){ val person = Person().apply{ name = "李四" age = 20 city = "上海" } println(person)}
区别:
with 函数用于对已有对象进行操作,不支持链式调用,返回lambda表达式的最后一行
apply 函数用于对象的初始化和配置,支持链式调用,返回对象本身
内联函数在Spring中的最佳实践
只内联小型高阶函数,避免让字节码膨胀
与事务、AOP、代理相关的代码不要为了“看起来优雅”强行内联
需要reified类型参数时再使用内联,例如封装通用反序列化或类型判断
业务服务方法通常不需要inline
高阶函数的最佳实践
高阶函数的存在是为了解决同一逻辑的高频复用的问题
必须遵循的原则
单一职责:每个高阶函数专注于一个横切关注点
合理使用内联函数:小型函数使用内联,大型函数避免
异常分层:不同层次处理不同的异常
命名清晰:使用业务领域语言命名
文档完备:为复杂的高阶函数提供使用示例
需要注意的坑
上下文丢失:尤其是在高协程环境中
循环依赖:避免在事务中频繁创建调用
过度抽象:没必要为不需要复用或复用性低的逻辑创建高阶函数
性能影响:监控高阶函数对性能的实际影响
3.6 协程—同步的方式写异步的代码
概述
Spring 对 Kotlin 协程提供支持,尤其是在 WebFlux、R2DBC 等响应式栈中可以直接使用suspend函数。传统 Spring MVC + JDBC/JPA 仍然是阻塞模型,写成suspend并不会自动变成非阻塞。
@RestControllerclass UserController( private val userService: UserService) { @GetMapping("/users/{id}") suspend fun getUser(@PathVariable id: Long): UserDto { return userService.findById(id) }}