48、Spring Boot集成:Kotlin + Spring Boot项目搭建、Bean定义、JPA集成
说实话,Kotlin 和 Spring Boot 的搭配,是我这几年用得最顺手的组合之一。Java 那套东西当然成熟,但写久了总觉得啰嗦。Kotlin 一来,代码量直接砍半,可读性还上去了。今天我们就聊聊怎么把这两者捏到一起,从项目搭建到 JPA 集成,一步到位。
项目搭建:别用 Maven 了,试试 Gradle Kotlin DSL
我个人习惯用 Gradle Kotlin DSL 来搭项目。为什么?因为 build.gradle.kts 本身就是 Kotlin 代码,写起来跟业务代码一样舒服。你想想看,不用再切到 XML 或者 Groovy 里去配依赖,多省心。
先看一个典型的 build.gradle.kts:
plugins {
id("org.springframework.boot") version "3.2.0"
id("io.spring.dependency-management") version "1.1.4"
kotlin("jvm") version "1.9.21"
kotlin("plugin.spring") version "1.9.21"
kotlin("plugin.jpa") version "1.9.21"
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("com.h2database:h2")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
}
这里有个细节:kotlin("plugin.jpa") 这个插件一定要加。不加的话,JPA 的 @Entity 注解在 Kotlin 类上会出问题。我之前踩过这个坑,折腾了半天才发现是插件没配。
Bean 定义:Kotlin 的方式更简洁
Spring 的 Bean 定义,在 Kotlin 里有两种主流写法。一种是传统的 @Configuration + @Bean,另一种是用 Kotlin 的 bean { } DSL。我个人更推荐后者,代码更紧凑。
看个例子:
@Configuration
class AppConfig {
@Bean
fun myService(): MyService {
return MyService()
}
}
用 DSL 写就是:
@Configuration
class AppConfig {
@Bean
fun myService() = MyService()
}
嗯,这里要注意:Kotlin 的 = 表达式可以直接返回对象,省掉了 return 和花括号。但如果你需要做复杂的初始化,还是老老实实用传统写法吧。
@ConfigurationProperties 绑定配置,记得在类上加上 @ConstructorBinding,配合 Kotlin 的 data class 使用,代码会非常干净。
JPA 集成:Entity 怎么写才优雅?
JPA 在 Kotlin 里有个老生常谈的问题:@Entity 类必须有一个无参构造函数。但 Kotlin 默认所有类都是 final 的,而且 data class 的构造函数是带参的。怎么办?
解决方案就是前面提到的 kotlin("plugin.jpa") 插件。它会自动为 @Entity 类生成无参构造函数,同时把类改成 open 的。你只管写 data class 就行:
@Entity
@Table(name = "users")
data class User(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0,
@Column(nullable = false)
val name: String = "",
@Column(unique = true)
val email: String = ""
)
这里我用了默认值 = 0 和 = ""。为什么?因为 JPA 在创建实体对象时,会先调用无参构造函数,然后通过 setter 赋值。有了默认值,这个流程就顺畅了。
Repository 的写法就更简单了:
interface UserRepository : JpaRepository<User, Long> {
fun findByName(name: String): List<User>
@Query("SELECT u FROM User u WHERE u.email LIKE %:domain%")
fun findByEmailDomain(@Param("domain") domain: String): List<User>
}
Kotlin 的 ? 可空类型在这里特别有用。比如你查一个可能不存在的用户:
fun findByIdOrNull(id: Long): User? {
return userRepository.findById(id).orElse(null)
}
返回 User? 类型,调用方就必须处理 null 的情况,不会出现空指针。我在项目中遇到过好几次因为忘记判空导致的线上问题,自从用了 Kotlin 的可空类型,这类 bug 几乎绝迹了。
核心逻辑流程图
下面这张图展示了 Kotlin + Spring Boot + JPA 的整体协作关系:
避坑指南:我曾经踩过的几个坑
做这个集成的时候,有几个地方特别容易翻车。我一个个说:
- Entity 类必须是 open 的:Kotlin 默认类是 final 的,JPA 的懒加载代理需要继承你的 Entity。不加
open或者不用all-open插件,运行时会报错。用kotlin("plugin.jpa")可以自动解决。 - data class 的 equals/hashCode 问题:data class 自动生成的
equals和hashCode会包含所有字段。如果 Entity 被代理了,这些方法可能出问题。我建议在 Entity 上手动重写equals和hashCode,只基于 ID 字段。 - @Query 里的 Kotlin 字符串模板:写 JPQL 时,别用
$做字符串插值,那会被 Kotlin 编译器当成模板变量。用:param配合@Param注解最安全。
spring.jpa.open-in-view=true(默认就是 true),在 Controller 层直接访问懒加载的关联对象时,可能会触发 N+1 查询。我建议在 Service 层就用 @Transactional 把查询包起来,或者用 fetch = FetchType.JOIN 显式指定。
实战:一个完整的 Service 示例
最后,我们写一个完整的 Service 类,把上面这些知识点串起来:
@Service
@Transactional
class UserService(private val userRepository: UserRepository) {
fun createUser(name: String, email: String): User {
require(name.isNotBlank()) { "用户名不能为空" }
require(email.contains("@")) { "邮箱格式不正确" }
val user = User(name = name, email = email)
return userRepository.save(user)
}
fun findByName(name: String): List<User> {
return userRepository.findByName(name)
}
fun findByEmailDomain(domain: String): List<User> {
return userRepository.findByEmailDomain(domain)
}
}
你看,Kotlin 的 require 函数直接做参数校验,比 Java 的 if 判断简洁多了。而且构造函数注入用 val 声明,连 @Autowired 都省了。说白了,Kotlin + Spring Boot 就是天生一对,用习惯了真的回不去。