Kotlin 和 Spring 是好朋友

Spring Framework从5.0开始加入了完整的Kotlin语言支持~使用Kotlin DSL风格的构建语法,能切实感受到Java语言缺失的那份开发的快感。配合上免配置的Spring Boot框架,更是如虎添翼。

当然即便经过了从2016年到2018年的不断融合,Kotlin和Spring的结合依然还存在不少需要注意的地方。这篇文章主要列出果子开发中跳的坑,以及摸索(Google)出的解决方案。

Domain类

作为保存数据资源最基础的POJO对象,Kotlin中的data class可谓是满足了常见的那些根本不想写的功能。

一个常见的POJO需要getter、setter、equals、hashCode、toString方法,在没有IDE功能之前,get和set方法都是手写的;
然后各大IDE推出了快速创建getter、setter的功能,这项功能在宇宙级IDE IntelliJ IDEA中肯定提供,快捷键是Alt+Insert(Cmd+N on macOS)。但美中不足的是,如果修改了类的定义,比如添加或者删除了类的成员变量,这些方法需要手动相应修改;
现在Kotlin将这种操作简化成一个关键词data class,不仅不需要手写POJO方法,而且在类成员定义发生变化的时候,完全不需要更新代码!

比如这是一个找回密码的令牌类

1
2
3
4
5
6
7
8
9
10
11
typealias TokeType = String

@Entity
data class TokenDomain(
@Id
val token: TokenType,
val expirationDate: LocalDateTime,
var isUsed: Boolean
) {
val isExpired get() = expirationDate <= LocalDateTime.now()
}

这段代码不仅使用了data class来设置数据类,而且使用了typealias语句声明一个类型别名
类型别名能在IDE的类型提示中以注释的形式展示出来,更直观。

可以注意到Kotlin的类型定义十分适合需要大量DTO、VO的地方,简单几行便能创建一个类型安全还具有高度可读性的数据类。

反观Java选手,提供相同功能的Java类,则需要至少3倍的代码量。
对比之下 可读性下降到无法直视的地步。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
@Entity
class TokenDomain {
@Id
private String token;
private LocalDateTime expirationDate;
boolean isUsed;

boolean isExpired() {
return LocalDateTime.now().isAfter(expirationDate);
}

public TokenDomain(){}

public TokenDomain(final String token, final LocalDateTime expirationDate, final boolean isUsed) {
this.token = token;
this.expirationDate = expirationDate;
this.isUsed = isUsed;
}

public String getToken() {
return token;
}

public void setToken(final String token) {
this.token = token;
}

public LocalDateTime getExpirationDate() {
return expirationDate;
}

public void setExpirationDate(final LocalDateTime expirationDate) {
this.expirationDate = expirationDate;
}

public boolean isUsed() {
return isUsed;
}

public void setUsed(final boolean used) {
isUsed = used;
}

@Override
public String toString() {
return "TokenDomain{token='" + token + '\'' + ", expirationDate=" + expirationDate + ", isUsed=" + isUsed + '}';
}

@Override
public boolean equals(final Object object) {
if (this == object)
return true;
if (object == null || getClass() != object.getClass())
return false;
if (!super.equals(object))
return false;

final TokenDomain that = (TokenDomain) object;

if (isUsed != that.isUsed)
return false;
if (token != null ? !token.equals(that.token) : that.token != null)
return false;
if (expirationDate != null ? !expirationDate.equals(that.expirationDate) : that.expirationDate != null)
return false;

return true;
}

@Override
public int hashCode() {
int result = super.hashCode();
result = 31 * result + (token != null ? token.hashCode() : 0);
result = 31 * result + (expirationDate != null ? expirationDate.hashCode() : 0);
result = 31 * result + (isUsed ? 1 : 0);
return result;
}
}

如果还需要加上@Nullable @NotNull等null类型标记,更体现Kotlin的优越性。

所以Kotlin语言对于大型项目的开发和维护可是一副五星装备!

而且作为用在Kotlin上的代码注释KDoc相比JavaDoc来讲,编写舒适度也有十足提升。
它支持Markdown语法,顺带的也能使用[]()语法来引用函数参数、函数名的超链接。
@return @param标签也有相同的写法,完全没有学习成本。

构造器和依赖注入

Kotlin类支持主从构造器,主构造器可以放在类名后的括号里直接申明,就如同上面data class里class后面的括号。
申明继承也是使用 : 完成的。
使用Spring 依赖注入框架,配合上Kotlin可以有很漂亮的代码

比如这是一个Controller,需要注入一个Service。可以使用的方式有

  • 设置一个类型为Service?的域,然后设置 @Autowired 注解

    1
    2
    3
    4
    class AccountController{
    @Autowired
    private var accountService: AccountService?
    }

    这样操作以后每次调用都需要判断null

  • 使用lateinit var声明一个类型为Service的域,然后设置@Autowired 注解

    1
    2
    3
    4
    class AccountController{
    @Autowired
    private lateinit var accountService: AccountService
    }

    这样使用至少不用判断null了,可是它还是个var在代码高亮的时候可能会不好看

  • 在类的构造器上直接声明val servie: Service,不需要注解!

    1
    2
    3
    class AccountController(
    private val accountService: AccountService
    )

    这样既没有可变性,又没有可空性,简直完美😝

单例和Object Expression

Java中的单例模式比较废代码,在Kotlin中只需要object一个标注就行。

比如下面是一个关于密码的帮助类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**密码相关的帮助类*/
object PasswordUtil {

private val passwordEncoder = BCryptPasswordEncoder()

/**
* 对比纯文本未加密形式的[rawPassword]和数据库中已编码的[encodedPassword]
* @param rawPassword 密码的原始格式
* @param encodedPassword 数据库中加密过的密码
* @exception BadCredentialsException 如果未通过验证则抛出异常
*/
fun checkEquality(rawPassword: UserPassword, encodedPassword: UserPassword) {
if (!passwordEncoder.matches(rawPassword, encodedPassword))
throw BadCredentialsException()
}

fun encodePassword(password: UserPassword): String {
return passwordEncoder.encode(password)
}
}

在Kotlin中调用这个类的方法只需要使用类名即可,不需要获取到INSTANCE实例类似的方法。
可是在在Java中调用Kotlin代码,则需要先通过获取INSTANCE实例,再进行操作。

Gradle

如果你使用Gradle,请往下阅读
根据Gradle新版本(4.0+)的文档,有几个比较大的变化。

  1. Gradle plugins DSL
    还记得build.gradle默认生成的代码头上会有一段

    1
    2
    3
    4
    5
    6
    7
    8
    9
    buildscript {
    repositories {
    xxx
    }
    dependencies {
    classpath 'xxxxxx'
    }
    }
    apply plugin: xxx

    这段代码是应用Gradle plugin老旧的方法。

    如果使用plugins DSL,可以变成短短的样子。
    比如这是Kotlin + Spring Boot常用的插件组合

    1
    2
    3
    4
    5
    6
    7
    8
    plugins {
    id "org.springframework.boot" version "2.0.1.RELEASE"
    id "io.spring.dependency-management" version "1.0.5.RELEASE"
    id "org.jetbrains.kotlin.jvm" version "1.2.40"
    id "org.jetbrains.kotlin.plugin.spring" version "1.2.40"
    id "org.jetbrains.kotlin.plugin.jpa" version "1.2.40"
    id "org.jetbrains.dokka" version "0.9.16"
    }

    DSL写法的一个不足是,它的版本号是字面量,以字符串的形式卸载代码里。并不能使用之前常见的作法,将版本号作为变量使用。

  2. compile -> implementation

    在新版本的Gradle上,如果你曾经运行过 Tasks > help > dependencies ,能看到compile部分被加上了Deprecated的标记

    compile - Dependencies for source set ‘main’ (deprecated, use ‘implementation ‘ instead).

    那他们为什么要这么做呢。

    原因是在你打包的项目中,compile会把你的文件和你引用的包一起暴露给引用者。而implementation就不会。

    不过作为开发者的你确实想要暴露给外一个组件呢,比如编写的项目包含有一个名叫project_api的组件,或者它是个project_library的库组件。
    这时候只需要使用api标记。

    1
    2
    3
    4
    5
    6
    plugins {
    id 'java-library'
    }
    dependencies {
    api 'xxxxx'
    }

    注意这里使用了一个叫 java-library 的插件,有了它才能使用api标记,不然代码提示里的 apiElements 并不是你需要的。


    那么如果跟上时代呢

    其实很简单,只需把compile一键替换成implentation即可!

    其实还有,将runtime替换成runtimeOnlytestCompile替换成testImplementation
    将像暴露给外的组件使用api标记。

  3. Fat jar,打包可执行的Kotlin项目

    我们分发项目成果的时候都希望简单一些,如果能直接打包一个jar丢过去,对方就能运行了那便是坠好的。
    Spring Boot 的插件提供了一个叫 bootJar 的任务,可以很好的完成目标任务。它在Tasks > build > bootJar下面,
    会将生成的jar文件放到builds/libs下面,直接使用Java -jar运行就好。因为所有的依赖项都在一个jar包里,所以体积会有点大。

    如果没有使用Spring Boot呢,那就只能自己手动操作。
    为jar任务添加一个打包操作即可。参考来源

    1
    2
    3
    4
    5
    jar {
    from {
    configurations.compileClasspath.collect { it.isDirectory() ? it : zipTree(it) }
    }
    }

常见问题

写了Controller方法,却不能在Spring MVC Panel看到接口定义

如果是Java,请注意自己写的类和方法是不是public的。
如果是Kotlin,似乎现在的版本暂时还看不到。

设置了301转发,第二次修改之后永远看不到更改变化

可能你是浏览器301缓存的受害者,果子曾经在nginx上为一个域名设置了301转发,可是第一次设置错误。
使用Chrome浏览器打开发现不对,再次修改nginx配置文件却一直等不到生效。多次debug也失效

鸡汁的我选择用 隐身模式 打开,验证设置是正确的。
然后搜索啊搜索,最后发现是Chrome浏览器的301缓存的锅,而且这个缓存是近乎永久的🌚
参见大佬们的讨论得出解决方案如下

  1. 打开 chrome://net-internals/ (或者 about://net-internals/ )
  2. 点击右上角黑色的向下的小箭头▼
  3. 选择清除缓存 Clear cache
  4. 大功告成

为什么我在Kotlin下面没有一些代码提示

IntelliJ IDEA的Spring插件,主要是面向Java平台的,
在Kotlin语境下就看不到比如Spring Data Repository的语法提示;
也没有JPA的图标和语法检查。

这都是以后期望厂家跟进修复的功能

0%