[SpringBoot] SpringBoot3 + Kotlin 踩坑指南

Posted by xiuyuantech on 2024-01-22

Kotlin 是一个基于 JVM 的编程语言, 它的简洁、便利早已不言而喻;
Kotlin 能够胜任 Java 做的所有事;
SpringBoot3于2022年11月24号第一版正式发布后,
带来了许多令人兴奋的新特性和改进。

环境变化

  • JDK
    Spring Boot 3.0以上 需要Java 17,并且兼容 Java 20(包括 Java 20)。还需要Spring Framework 6.0.9或更高版本。
  • GraalVM支持
    Spring Native 也是升级的一个重大特性,支持使用 GraalVM 将 Spring 的应用程序编译成本地可执行的镜像文件,可以显著提升启动速度、峰值性能以及减少内存使用。

踩坑记录

1、创建项目,修改 POM 文件。

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
//添加 spring boot 依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.2.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
</dependencies>

//添加 mysql 依赖
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.35</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.14</version>
</dependency>

/添加Kotlin 依赖
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
</dependency>

在 Kotlin 中,data class 默认没有无参构造方法,并且 data class 默认为 final 类型,不可以被继承。如果我们使用 SpringBoot + Kotlin 的模式,那么使用 @autowired 就可能遇到这个问题。因此,我们可以添加 NoArg插件(kotlin-noarg) 为标注的类生成无参构造方法。使用 AllOpen插件(kotlin-allopen) 为被标注的类去掉 final,才允许被继承。

2、yml/properties配置文件

1
2
3
4
5
6
7
8
9
在kotlin和java混合开发时发现注解@ConfigurationProperties无效读取不到对应的属性,
需要使用注解@Value才有效。

两者区别:
两者都不能直接给static变量赋值。
@ConfigurationProperties是可以放在默认的类注解上,@Value可以读取单个配置项,
加到get/set方法或者属性名上。

注意: @Value注解中指定的系统属性名,必须存在,且必须跟配置文件中的相同。

3、redis/json配置
StringRedisTemplate 和 RedisTemplate 是有区别的,不过 StringRedisTemplate 实际是 RedisTemplate<String,String>; StringRedisTemplate 是经过字符串序列化可直接查看内容,而 RedisTemplate 是经过默认JDK序列化的无法直接查看。

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
//redis保存对象,Mapper序列化配置
val mapper = ObjectMapper()
// 反序列化的时候如果是无效子类型,不抛出异常
mapper.configure(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE, false)
// 序列化时候遇到空对象不抛出异常
mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)
//为空,为null时不显示
mapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY)
//忽略不识别的字段
mapper.configure(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES, false)
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
mapper.configure(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature(), false)
mapper.serializerProvider.setNullValueSerializer(NullStringJsonSerializer())
mapper.registerModule(Java8TimeModule())
// 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY)
// 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常,
/*
mapper.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.PROPERTY
)*/
//使用Jackson2JsonRedisSerializer序列化必须用下面的方法,否则值会被当作string处理
mapper.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.EVERYTHING,
JsonTypeInfo.As.PROPERTY
)

3、参数校验
在 Java 中各种注解的参数校验均有效,但是放在 Kotlin 中有些却无效。由于使用 Kotlin开发后台的资料比较少,同时经过我几天研究发现在参数校验的注解前面field:才有效果。
如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
data class MailDTO(
//发送者邮箱
@field:Valid
val from: MailSender? = null,
//接收者邮箱
@field:NotEmpty
val sendTo: List<String> = arrayListOf(),
//邮件主题
@field:NotBlank
val subject: String = "",
//邮件内容
val text: String = "",
)

4、优化相关
knife4j vs swagger,推荐knife4j,功能强大,页面友好。
减少扫描路径,手动注入重量级Bean或者使用 starter 机制。
SpringBoot 的 starter 机制,优化下缓存组件的实现,可以做到自动注入、开箱即用。
只要改造下缓存组件的代码,在 resources 文件中添加一个 META-INF/spring.factotries 文件,
在下面配置一个 EnableAutoConfiguration 即可,这样项目在启动时也会扫描到这个 jar 中的 spring.factotries 文件,将 XxxAdCacheConfiguration 配置类自动引入,而不需要扫描"com.xxx.demo"整个路径了:

1
2
3
# EnableAutoConfigurations
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.xxx.demo.XxxAdCacheConfiguration

5、反射相关
JDK17限制反射,对于包扫描和反射的权限控制更加的严格。
解决方案:
一个粗暴的解决办法是将没开放的module强制对外开放,即保持和Java9之前的版本一致。 –add-exports导出包,意味着其中的所有公共类型和成员都可以在编译和运行时访问。 –add-opens打开包,意味着其中的所有类型和成员(不仅是公共类型)都可以在运行时访问。 主要区别在于–add-opens允许深度反射,即非公共成员的访问,才可以调用setAccessible(true)。

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
//SGM需要加入
--add-opens java.management/java.lang.management=ALL-UNNAMED
--add-opens jdk.management/com.sun.management.internal=ALL-UNNAMED
--add-opens java.management/sun.management=ALL-UNNAMED

//R2M需要加入
--add-opens java.base/java.time=ALL-UNNAMED

//Ducc需要加入
--add-opens java.base/java.util.concurrent=ALL-UNNAMED
--add-opens java.base/java.util.concurrent.locks=ALL-UNNAMED
--add-opens java.base/java.security=ALL-UNNAMED
--add-opens java.base/jdk.internal.loader=ALL-UNNAMED
--add-opens java.management/com.sun.jmx.mbeanserver=ALL-UNNAMED
--add-opens java.base/java.net=ALL-UNNAMED
--add-opens java.base/sun.nio.ch=ALL-UNNAMED

//AKS需要加入
--add-exports java.base/sun.security.action=ALL-UNNAMED
--add-opens java.base/java.lang=ALL-UNNAMED
--add-opens java.base/java.math=ALL-UNNAMED
--add-opens java.base/java.util=ALL-UNNAMED
--add-opens java.base/sun.util.calendar=ALL-UNNAMED

异常:Causedby: java.lang.NoClassDefFoundError: javax/xml/bind/JAXBException
原因:Java11 删除了 Java EE modules,其中就包括 java.xml.bind (JAXB)。

<!-- API, java.xml.bind module -->
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
<version>2.3.2</version>
</dependency>
<!-- Runtime, com.sun.xml.bind module -->
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>2.3.2</version>
</dependency>

但是,这种解决方案可能存在潜在的安全风险,因为它打开了一个本来是受保护的模块,允许其他模块进行未经授权的访问。因此,建议在必要时使用此选项,同时确保您的应用程序没有潜在的漏洞和安全问题

总结建议

SpringBoot 整合 Kotlin 非常容易,并简化 SpringBoot 应用的初始搭建以及开发过程。JDK17性能也更强,最大的亮点是可以使用亚毫秒级停顿的GC性能(至少百倍的GC性能提升),所以强烈建议升级到JDK17。