使用Kotlin开发Spring Boot应用程序
最近把KeyOA从Java前移到了Kotlin进行开发,下面说一下需要注意的事项以及一些Kotlin的语法。
在Spring Boot中引入Kotlin
// build.gradle.kts import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins {
id("org.springframework.boot") version "3.0.1" id("io.spring.dependency-management") version "1.1.0" kotlin("jvm") version "1.8.0" kotlin("plugin.spring") version "1.8.0" kotlin("plugin.jpa") version "1.8.0" } // 让Spring支持Kotlin的Null Safety tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs += "-Xjsr305=strict" } }
// build.gradle.kts,负责引入依赖 dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("org.springframework.boot:spring-boot-starter-mustache") implementation("org.springframework.boot:spring-boot-starter-web") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") // Jackson Kotlin支持 implementation("org.jetbrains.kotlin:kotlin-reflect") // Kotlin反射 runtimeOnly("org.springframework.boot:spring-boot-devtools") testImplementation("org.springframework.boot:spring-boot-starter-test") }
<!-- pom.xml --> <build> <sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory> <testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> <plugin> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-maven-plugin</artifactId> <configuration> <compilerPlugins> <plugin>jpa</plugin> <plugin>spring</plugin> </compilerPlugins> <args> <arg>-Xjsr305=strict</arg> </args> </configuration> <dependencies> <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-maven-noarg</artifactId> <version>${kotlin.version}</version> </dependency> <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-maven-allopen</artifactId> <version>${kotlin.version}</version> </dependency> </dependencies> </plugin> </plugins> </build>
<!-- pom.xml,依赖部分 --> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mustache</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.fasterxml.jackson.module</groupId> <artifactId>jackson-module-kotlin</artifactId> </dependency> <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-reflect</artifactId> </dependency> <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-stdlib</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
应用的Main Class:
package com.jydjal.oa import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication @SpringBootApplication class OaApplication fun main(args: Array<String>) {
runApplication<OaApplication>(*args) {
setBannerMode(Banner.Mode.OFF) // 关闭Spring Boot Banner,可选 } }
OOP&依赖注入
Kotlin中的类,成员变量和构造函数:
class Person( val name: String, val age: Int, // 前两个是构造函数参数,同时也是类的成员变量 height: Int, // 构造函数的普通参数,后面逗号可以不省略 ) {
val height: Int = height // 成员变量,但是未在构造函数中 }
在Kotlin中使用@Autowired注解实现依赖注入,这个注解可以放在:Constructor、Method、Parameter、Field、AnnotationType
@Service class EmployeeService{
// 逻辑代码略 } @Controller class EmployeeController(@Autowired val service: EmployeeService) {
// 逻辑代码略 } // 如果需要注入的成员变量过多,则可以把注解放在构造方法上,这是需要在构造方法前添加constructor关键字 @Controller class EmployeeController @Autowired constructor(val service: EmployeeService) {
// 逻辑代码略 }
日志(以Slf4j为例)
在Java中,如果需要使用日志可以使用Lombok的@Slf4j注解,但是在Kotlin中,这个注解失效了,因此需要手动获取logger。
在这里我使用扩展方法来实现,然后解释原因:
// 获取logger的函数,这里使用Slf4j fun <T : Any> T.logger(): Lazy<Logger> {
// 使logger的名字始终和最外层的类一致,即使是在companion object中初始化属性 val ofClass = this.javaClass val clazz = ofClass.enclosingClass?.takeIf {
ofClass.enclosingClass.kotlin.companionObject?.java == ofClass } ?: ofClass return lazy {
LoggerFactory.getLogger(clazz) } } // 使用logger函数 class TestClass{
val logger1 by logger() // 或者使用companion object companion object {
val logger2 by logger() } fun testLog() {
logger1.info("Name: logger1, Level: info.") logger2.debug("Name: logger2, Level: debug.") } }
现在给出原因,首先是把logger作为类的成员变量获取:
class TestClass{
val logger = LoggerFactory.getLogger(TestClass::class.java) fun testLog() = logger.info("This is a test log.") // 这是Kotlin中的单表达式函数,参见附录的Kotlin Idioms }
但是这样会有两个弊端:1:每个logger都需要手动编写代码实现,比较麻烦;2:每个类的实例都会有一个logger变量(虽然日志框架会有logger缓存机制,影响不大)。
针对第一个问题,我们可以使用扩展方法实现:
fun <T: Any> T.logger(): Logger {
return LoggerFactory.getLogger(T::class.java) }
针对第二个问题,可以考虑使用companion object:
class TestClass{
companion object {
val logger = LoggerFactory.getLogger(TestClass::class.java) } fun testLog() = logger.info("This is a test log.") }
但是在companion object中的logger获取的类名不正确,变成了TestClass.Companion,因此需要让companion中的logger也能获取外部类的名称,于是就有了上面获取外部类的操作。
// 使logger的名字始终和最外层的类一致,即使是在companion object中初始化属性 val ofClass = this.javaClass val clazz = ofClass.enclosingClass?.takeIf {
ofClass.enclosingClass.kotlin.companionObject?.java == ofClass } ?: ofClass
最后使用Lazy,让logger在使用时才进行计算,就得到了上面的方法。
其实还有其他的方法可以获取logger的实例,详见参考链接第三条StackOverflow上的回答。
测试
我们将使用Spring Boot Starter Test完成教学,包含JUnit5、MockMvc和AssertJ三部分。
JUnit5
@SpringBootTest class TestEmployee {
@Test fun `Test employee setter`() {
// 在Kotlin中,测试方法名称可以用反引号(数字1左边那个)括起来作为测试的名称 val employee: Employee? = Employee("张三", "男") assertThat(employee).isNotNull() } }
关于JUnit5的测试实例生命周期:在JUnit5中支持使用@BeforeAll和@AfterAll注解在测试开始前和结束后做一些额外的工作,这需要被标记上注解的方法要是静态方法,在Kotlin中这需要通过companion object实现,这样做非常麻烦。但是JUnit5可以让测试在每个类上实例化,这就需要我们手动修改JUnit5的测试生命周期:
# src/test/resources/junit-platform.properties junit.jupiter.testinstance.lifecycle.default = per_class
这样@BeforeAll和@AfterAll注解就可以加在普通方法上,实现起来也就容易了许多。
MockMvc
这里直接给出实例:
@SpringBootTest @AutoConfigureMockMvc class LoginTest @Autowired constructor( private val mockMvc: MockMvc, private val mapper: ObjectMapper) {
private lateinit var token: String fun testLoginSuccess() {
val loginDto = mapOf("account" to "admin", "password" to "1234qwer") mockMvc.post("/login") {
contentType = MediaType.APPLICATION_JSON content = mapper.writeValueToString(loginDto) header("isAdmin", true) }.andExpect{
jsonPath("$.message") {
value("OK") } jsonPath("$.data") {
exists() } }.andDo{
print() handle{
val content = it.response.contentAsString token = mapper.readValue(content, object: TypeReference<JsonResponse<String>>() {
}).data } } } }
具体用法见参考链接中的MockMvc Kotlin Dsl。
AssertJ
这部分使用Kotlin和使用Java没有太大区别,有区别的地方按照IDE提示修改即可,主要有两点:
- assertThat(something).as(”Description”):IDE会提示在as上扩上反引号
- 有些地方比如assertThat(”string”).isNotEmpty():IDE会提示将isNotEmpty的括号去掉,改成属性访问的形式
数据库实体定义
首先引入依赖
// build.gradle.kts plugins {
... kotlin("plugin.allopen") version "1.8.0" } allOpen {
annotation("jakarta.persistence.Entity") annotation("jakarta.persistence.Embeddable") annotation("jakarta.persistence.MappedSuperclass") }
<!-- pom.xml --> <plugin> <artifactId>kotlin-maven-plugin</artifactId> <groupId>org.jetbrains.kotlin</groupId> <configuration> ... <compilerPlugins> ... <plugin>all-open</plugin> </compilerPlugins> <pluginOptions> <option>all-open:annotation=jakarta.persistence.Entity</option> <option>all-open:annotation=jakarta.persistence.Embeddable</option> <option>all-open:annotation=jakarta.persistence.MappedSuperclass</option> </pluginOptions> </configuration> </plugin>
实体类定义:
@Entity class Article( var title: String, var headline: String, var content: String, @ManyToOne var author: User, var slug: String = title.toSlug(), var addedAt: LocalDateTime = LocalDateTime.now(), @Id @GeneratedValue var id: Long? = null) @Entity class User( var login: String, var firstname: String, var lastname: String, var description: String? = null, @Id @GeneratedValue var id: Long? = null)
注意:因为Spring Data Jpa无法处理不可变的类型,因此在这里成员变量需要是可变的;如果你使用其他的框架比如Spring Data Jdbc、Spring Data MongoDB等,则可以把var改成val。
同时建议在每个实体类上使用Generated Id(@Id @GeneratedValue),原因如下:Kotlin properties do not override Java-style getters and setters。
开发杂项
变量的相等判断
在Kotlin中,“ == ”相当于Java中的equals()方法,只判断值是否相等;“ === ”相当于Java中的“ == ”,同时判断地址是否一致。
异常处理
Kotlin支持Java异常的大部分写法,但有部分不一致:
// 抛出一个异常 throw Exception("This is an exception.") // 捕获一个异常 try {
... } catch (e: Exception) {
... } finally {
... } // try表达式 val a: Int? = try {
input.toInt() } catch (e: NumberFormatException) {
null } // throws的Kotlin写法,等价于 void function() throws Exception {} @Throws(Exception::class) fun function() {
}
同时Kotlin还有一个叫Nothing的类型,用来表示永远不可能有返回结果,即总会返回异常,详见参考链接中的Kotlin Nothing Type。
匿名类和内部类
// 匿名类的使用,摘自Kotlin Documentation window.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
/*...*/ } override fun mouseEntered(e: MouseEvent) {
/*...*/ } }) // 内部类的使用,同Java一样,内部类也可以访问外部类的数据 class OuterClass{
var num: Int = 1 inner class InnerClass{
fun foo() = num } }
内部类还有this的作用域问题,详见参考链接中的Kotlin This。
常量定义
在Java中定义常量需要放在类中进行:
// Constant.java class Constant {
public static final String str = "This is a constant string."; }
在Kotlin中的常量可以直接写在文件中,例如:
// Constant.kt const val str: String = "This is a constant string" // 使用常量 // UseConstant.kt import Constant.str println(str)
常用数据结构及相关方法
Kotlin中支持的数据结构如下图所示:
其中MutableList、MutableSet、MutableMap支持向其中添加或删除元素,List、Set、Map则不支持。
Kotlin的List支持的常用操作:
val numbers = listOf(1, 2, 3, 4, 5) println(numbers.indexOf(2)) println(numbers.lastIndexOf(4)) println(numbers.indexOfFirst{
it > 3 }) println(numbers.indexOfLast{
it < 5 }) println(numbers.binarySearch(3)) // 使用二分查找
Kotlin的Set支持交并补的操作:
val numbers = setOf("one", "two", "three") println(numbers intersect setOf("two", "one")) // 交 println(numbers union setOf("four", "five")) // 并 println(numbers subtract setOf("three", "four")) // 补
值得注意的是,Kotlin的Map支持使用方括号的形式访问元素:
val map: Map<String, int> = mutableMapOf("one" to 1, "two" to 2) // 此方法创建LinkedHashMap,保存记录顺序,此外还有不保存记录顺序的HashMap map["three"] = 3 // 等价于map.push("three", 3),Java和Kotlin同理 println(map["one"]) // 1 map -= "one" // 等价于map.remove("one")
在Kotlin中,filter、map、groupBy等操作也有相应的支持,详见参考链接中的Kotlin Idioms和Kotlin Collections。
类型判断、类型转换和空值判断
Kotlin支持可空类型,举例如下:
var str: String? = null // 使用Type?来定义一个可空类型 val str2: String = null // 报错 str = "123" val str3 = str!! // 使用!!将可空类型转成不可空类型,如果此变量值为null那么就会报错 val len = str?.length // 如果str不为null,则返回它的长度,否则返回null str?.let{
println(str) } // 如果str不为null,则进行函数调用,否则不执行
Kotlin的类型判断和类型转换:
val b: Int? = a as? Int // 尝试类型转换,如果不成功就返回null val b: Int? = a as Int // 尝试类型转换,如果不成功就报错 // Kotlin类型判断 if (obj is String) {
println(obj.length) } // 这里会自动转换类型 if (obj !is String) {
} // 等价于if (!(obj is String))
Kotlin中有一个名为Any的类型,表示任何类型,但是Java中的Object对应Kotlin中的Any?,因为Java中的Object可以是null。
函数和扩展方法
Kotlin的函数支持默认参数和按名传参:
fun test(a: Int? = null, str: String = "") {
... } test(str = "")
如果Kotlin的函数比较简短,那么可以变成一个单表达式函数:
fun test(): Int = 42
Kotlin支持扩展函数,从而更加方便地为已有的类型添加功能:
// 定义方法:fun Type.funcName() { this.xxxxx },扩展函数同样支持参数列表 fun String.spaceToCamelCase() {
... this.xxx() // 使用this访问对象 ... } // 使用扩展函数 "Convert this to camelcase".spaceToCamelCase()
参考链接
- Getting Started | Building web applications with Spring Boot and Kotlin
- Kotlin Idioms
- https://stackoverflow.com/a//
- Is it possible to use Lombok with Kotlin? - Stack Overflow
- MockMvc Kotlin Dsl
- Kotlin Nothing Type
- Kotlin This
- Kotlin Collections
- Kotlin Docs | Kotlin Documentation (kotlinlang.org)
版权声明:
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若内容造成侵权、违法违规、事实不符,请将相关资料发送至xkadmin@xkablog.com进行投诉反馈,一经查实,立即处理!
转载请注明出处,原文链接:https://www.xkablog.com/kotlinkf/1109.html