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 的整体协作关系:

Kotlin + Spring Boot + JPA 协作流程 Kotlin 代码层 data class Entity | Repository 接口 | Service 类 Spring Boot 框架层 @Configuration | @Bean | @Autowired | 依赖注入 JPA 持久化层 EntityManager | 自动生成 SQL | 事务管理 数据库(H2 / MySQL / PostgreSQL)

避坑指南:我曾经踩过的几个坑

做这个集成的时候,有几个地方特别容易翻车。我一个个说:

  • Entity 类必须是 open 的:Kotlin 默认类是 final 的,JPA 的懒加载代理需要继承你的 Entity。不加 open 或者不用 all-open 插件,运行时会报错。用 kotlin("plugin.jpa") 可以自动解决。
  • data class 的 equals/hashCode 问题:data class 自动生成的 equalshashCode 会包含所有字段。如果 Entity 被代理了,这些方法可能出问题。我建议在 Entity 上手动重写 equalshashCode,只基于 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 就是天生一对,用习惯了真的回不去。


公众号:蓝海资料掘金营,微信deep3321