使用Kotlin开发Spring Boot应用程序

使用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提示修改即可,主要有两点:

  1. assertThat(something).as(”Description”):IDE会提示在as上扩上反引号
  2. 有些地方比如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 = "123123")

如果Kotlin的函数比较简短,那么可以变成一个单表达式函数:

fun test(): Int = 42

Kotlin支持扩展函数,从而更加方便地为已有的类型添加功能:

// 定义方法:fun Type.funcName() { this.xxxxx },扩展函数同样支持参数列表
fun String.spaceToCamelCase() {
	...
  this.xxx()  // 使用this访问对象
  ...
}

// 使用扩展函数
"Convert this to camelcase".spaceToCamelCase()

参考链接

  1. Getting Started | Building web applications with Spring Boot and Kotlin
  2. Kotlin Idioms
  3. https://stackoverflow.com/a/34462577/17271495
  4. Is it possible to use Lombok with Kotlin? - Stack Overflow
  5. MockMvc Kotlin Dsl
  6. Kotlin Nothing Type
  7. Kotlin This
  8. Kotlin Collections
  9. Kotlin Docs | Kotlin Documentation (kotlinlang.org)
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

OriginCoding

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值