SpringBoot + Kotlin 中的坑

从 16 年 12 月我们在 SpringBoot 的后端使用 Kotlin 开发以来,遇到了各种各样的坑。尽管 Jetbrains 宣称 Kotlin 对 Java 的互操作性是语言设计的一大优势,但由于 SpringBoot 和 Spring 严重依赖了 JVM 平台的各种特性,有时 Kotlin 并不能编译出足够符合行为的字节码,在一些依赖 Spring 特性的地方会遇到各种奇怪问题。

本文总结了使用 Kotlin 开发 SpringBoot 后端项目的过程中遇到的各种坑,其中有些可能逐渐被 Kotlin 官方文档提醒或解决。

首先介绍一个可以方便查看 Kotlin 编译后代码具体行为的方式,以 Intellij 为例,假设以下代码:

@Entity
class User(
    @get:Column
    var name: String
) {
    @Transient
    @get:Id
    @get:GeneratedValue(strategy = GenerationType.AUTO)
    var id: Long = 0L
}

在 Intellij 中双击 Shift 键,选择 Show kotlin bytecode ,再选择 Decompile 可以查看编译后代码再逆回 Java 的样子,如上面这段 Data Class 会生成很多的方法,节选如下:

@Entity
@Metadata(/* xxx */)
public final class User {
   @Transient
   private long id;
   @NotNull
   private String name;
   @Id
   @GeneratedValue(
      strategy = GenerationType.AUTO
   )
   public final long getId() {
      return this.id;
   }

   public final void setId(long var1) {
      this.id = var1;
   }
   // xxxxxxx
}

可以看到每个字段都生成的 Getter 和 Setter,并且 @get:id 可以将 annotation 直接加在 Getter 上。 这种方式可以非常具体的查看 Kotlin 编译器到底为我们生成了怎样的代码,对于熟悉 Java 打算试试 Kotlin 的人来说非常方便,不会被内部复杂的细节困扰

No default constructor for entity (实体缺少默认构造)

从数据库中查询 entity 时,Hibernate 会首先调用默认构造(无参构造函数)初始化对象,之后将各个字段调用 Setter 设置进来。以开头的那段代码为例,使用 JpaRepository 查询出对象时,会报这个错误。查看一下 Java 代码可以发现构造函数只有一个,接受的是 name:string 字段。

开发时我们会希望将构造一个实体时需要的参数都放在构造函数中,增强静态检查能力,同时给每个对象都设置默认初值不够方便,因此无法手写一个无参构造给 Hibernate 调用。

此时可以使用 jpa-support 这个编译插件来为 @entity 注解的 class 生成无参构造。

此时再查看生成的代码,会看到在最下面多了个无参构造,没有做任何事情,但再次用 Hibernate 已经没问题了。

Could not locate setter method for property (找不到 Setter)

@Entity
data class User(
    @get:Column(updatable = false)
    val name: String
)

比如 name 字段我们只想在构造时设置,之后不能修改,在 kotlin 中自然的选择用 val,但运行时 hibernate 会提示找不到这个字段的 setter 方法。可以给 name 加上 annotation,在自己的代码中调用会报错,但 hibernate 反射调用却不会有问题。

@get:Column(updatable = false)
@set:Deprecated("deprecated", level = DeprecationLevel.HIDDEN)
var name: String

Transactional 不生效

Kotlin 默认的类是 final 的,不可继承,Spring 也无法代理其中的方法,可以手动将某些类变成 open class,方法变成 open fun,也可以使用 spring-support,会自动把一些 annotation 注解的类变成 open class。

Lazy Fetch 不生效

实体类默认全部被生成了 final class,且 spring-support 插件没有将 @entity 注解的类变为 open class,需要手动应用 kotlin-allopen 并在 gradle 中配置对 @entity 注解的 allopen。

apply plugin: "kotlin-allopen"
allOpen {
    annotation("javax.persistence.Entity")
}

类内方法不能被代理

@Configuration
class Runner(private val userRepo: UserRepo,
             private val postRepo: PostRepo) {
    @EventListener(ApplicationReadyEvent::class)
    fun test() {
        queryPost()
    }
    @Transactional
    fun queryPost() {
        val post = postRepo.findOne(1)
        println(post.user.name)
    }
}

在这个类中 queryPost 应该运行在事务中,但执行时会发生异常 could not initialize proxy - no Session ,没能开启事务。这是由于 spring-aop 是包裹你的方法,对于从 this 调用的方法不能代理掉。可以注入一个自己类的实例,调用该对象的方法。

@Configuration
class Runner(private val userRepo: UserRepo,
             private val postRepo: PostRepo) {

    @Autowired
    private lateinit var runner: Runner

    @EventListener(ApplicationReadyEvent::class)
    fun test() {
        runner.queryPost()
    }
}

原文:https://blog.hlyue.com/2018/05/03/Kotlin-and-springboot/
作者:Richard