从 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