Kotlin 和 Java EE 系列之—— 如何让 Kotlin 类对 Java EE 友好
Kotlin 和 Java 都是 JVM 语言,所以它们之间相互转换很容易,是这样吗?不完全是,让 Kotlin 的类对 JEE 友好还需要一点工作。
Kotlin 的主要优势之一就是能很好地集成 Java。事实上 Java 很容易转换为 Kotlin,看起来用 Kotlin 写 Java EE 应用似乎不需要动什么脑筋。然而,两者之间存在一些微妙的差别,使得转换并不那么顺畅:
大多数框架要求非 final 的类,而 Kotlin 的类是 final 的。
注入会引入大量不必要的空检查。
上述两点以及强制的无参数构造函数会妨碍编写函数式风格的代码。
Java EE 和 Kotlin 并不是真正的朋友,除非你撮合它们。幸好所有这些问题都可以避免。
我们的转换目标是一个简单的 Java WAR,它可以通过 REST 接口从数据库中存储和检索记录。从 GitHub 拉取 fables-kotlin 仓库开始吧。这个项目在 jee/java 目录下。如果想运行它并进行测试,请查看 GitHub 上的说明。
让 Kotlin 类对 Java EE 友好
Java EE 服务器对类的构造方式非常挑剔。它们大多数必须是非 final,而且拥有一个无参数的构造函数,以及公有方法。无参数构造函数用于实例化,而另外两个需求用于生成代理。代理会拦截对象的调用并丰富它们的附加功能。如果写的是 Java 代码,不需要考虑太多,但写 Kotlin 代码会有点不一样。
在 Build 脚本中加入 Kotlin
在开始转换之前,将 Kotlin 编译器添加到 build 脚本中。也就是从这个:
改为这个:
我们必须注册 Kotlin 编译器插件,将其应用到模块,并添加一些依赖项。Kotlin 标准库并不是强制要求使用的,它虽然小型,却提供了大量有用的功能。
简单的开始:RestApplication 类
IntelliJ 内置支持将 Java 类转换为 Kotlin。这很容易使用:你可以打开一个 Java 类然后按下 [CTRL]+[ALT]+[SHIFT]+K,或者拷贝一段 Java 文件中的代码并在 Kotlin 文件中粘贴。两种方式都会自动转换代码。下面把这个组合键称为 [Kotlin]。
打开 RestApplication.java 类,按下 [Kotlin]。完成。
转换后的代码正常工作,但它仍然是 Java 风格的代码,只是用了不同的语言。通过不可变的 Kotlin set 并把 getClasses 变成一个真正的函数来代替复杂的 Java HashSet 初始化过程 —— 这样做:
转换接口
按下 [Kotlin] 然后继续。
转换简单的不可变类
下一步,我们将转换 fables.kotlin.jee.java.rest.KittenRest.java 类。注意这个类只有一个构造函数,其中包含所有属性的赋值,没有 setters。这个类用于在 REST 中传输数据,所以它被写成不可变的。如果这个类拥有一个无参数构造函数并含有 setters,框架就能对它进行实例化并用 setters 填充数据。这种情况下,框架必须使用无参数构造函数。因为 Java 构造函数不提供参数名称,因此自动绑定是不可能实现的。这就是为什么需要 @JsonProperty 注解。参数名元数据在 Java 8 中可以通过一个特殊的编译参数提供,但默认情况下没有这个特性。
按下 [Kotlin],IDEA 会帮你把类转换成 Kotlin 代码。很神奇,类的内容不见了。
休息一下,做点练习:
创建一个类型为 Person 且具有 person 属性的 JavaBean。为其添加 getter, setter, equals, hasCode, 和 toString 方法,以及接收 person 的构造函数。现在统计一下你写“person”的次数,不区分大小写。
希望你对结果不要太震惊。拥有与这个 Java 类同样功能的 Kotlin 类在代码格式化良好的情况下只需要编写 4 行代码。
JPA 实体类
热身过后,我们来尝试更有趣的事情:JPA 实体类。这是很典型的,拥有很长的 setters 和 getters、巨大的equals, hashCode, 和toString的类。下面会用一个简短的摘要来提醒你它有多臃肿。
这里我们有第一个进行真正改进的机会。再使用一次 [Kotlin]。现在声明它是一个数据类(data class)。将主构造函数声明为私有的,放入所有字段的声明,删除默认的东西并添加 override。删除所有的方法(但要保留构造函数)。让次构造函数调用主构造函数。来看看“瘦身”的效果:
不过我们仍然停留在无参数构造函数的阶段。来看看强制的 name 属性。该值将通过公共构造函数或 JPA 提供。然而,JPA 会先构造对象,然后再设置值,这意味着我们必须为构造提供一些默认值。空字符串是个简单但成本小的方案。
业务服务
KittenBusinessService.java 是一个简单的服务,它能插入和读取数据。在将其转换为 Kotlin 之后,我们必须使用一点技巧来让它发挥作用。
首先,entityManager 的声明完全错了:
作为在类构造之后注入的值,它必须声明为可空,并使用 null 作为初始值,所有调用都需要进行空值检查。幸好 Kotlin 有办法在变量第一次使用之前设置为非空值:lateinit (延迟初始化)。也就是说现在没有值,但晚一点会有,这正是注入所做的事情。这允许我们将其声明为非空的(non-nullable),并抛出所有空值检查(null-checks):
类和所有非私有方法必须声明为 open —— 否则就无法创建代理。我们还要对返回的 id 类型做一个小小的修复,因为实体在被持久化之后,id 不再是 null。!! 意味着“值不能是 null;如果是,则抛出异常”。find 方法只是个函数,所以我们按这种方式声明它。
这个方法的返回值由编译器推导。不过,我更愿意声明它。如果你弄错了,返回了一个错误的类型, 声明将会捕获到这一点,你也将得到一个编译错误,而不是一些奇怪的运行时行为。
REST 服务
再次从自动转换开始。为类和所有非私有成员声明 open。现在尝试使用服务,不会成功:
记住每个类属性会自动创建 getter 和 setter,而且 Kotlin 中所有东西都是 final 的。受保护(protected)的变量 kittenBusinessService 创建了一个 final 的受保护的 setter,我们并不喜欢这样,因为它不能被代理。这里我们可以将其声明为 open 或 private,两种方式都可以。因为它是私下里使用的,我们声明它为 private,同时声明它为 lateinit 来处理空值检查:
小结
Kotlin 最酷的地方在于它与 Java 能很好的互动。我们可以一个个的去转换类,期间不会出现问题。
Kotlin 和 Java 在默认情况下有两点差异;Kotlin 的类和方法是 final 的,但 Java 的是 open 的;Kotlin 的成员是公共的,而 Java 的是受保护的。在写独立程序的时这并没有太大的差别,因为编译器会对所有问题发出警告,但是在 Java EE 框架中这会产生问题。在应用程序部署和使用之前你不会收到任何对问题的警告。通过谨慎的编码可以避免问题,但小心的对待每个类的时候难免出错。只要忘了一个 open 就会造成应用程序出错。
在下一部分中,我会告诉你如何利用 Kotlin 编辑器插件来处理 Kotlin 的细节,让你的 Java EE 应用更健壮。这些插件也会让代码更清晰,更有效,最终比对应的 Java 代码更好。
TIOBE 6 月编程语言排行榜:Kotlin 突围进入 50 强
Node.js 发布 v8.0.0 正式版;Qt 5.9 正式发布,长期支持版本