使用 Kotlin
Kotlin 是一种非常流行的编程语言,它以 JVM(以及其他环境)为目标。Kotlin 在过去几年中经历了人气的飙升,使其成为最流行的 JVM 语言,当然 Java 除外。
Quarkus 提供了对使用 Kotlin 的一流支持,本指南将对此进行解释。
先决条件
要完成本指南,您需要
-
大约 15 分钟
-
一个 IDE
-
已安装 JDK 17+ 并正确配置了
JAVA_HOME
-
Apache Maven 3.9.9
-
如果您想使用它,可以选择 Quarkus CLI
-
如果您想构建本机可执行文件(或者如果您使用本机容器构建,则为 Docker),可以选择安装 Mandrel 或 GraalVM 并进行适当的配置
注意:对于 Gradle 项目设置,请参见下文,有关更多参考,请查阅 Gradle 设置页面中的指南。
创建 Maven 项目
首先,我们需要一个新的 Kotlin 项目。这可以使用以下命令完成
对于 Windows 用户
-
如果使用 cmd,(不要使用反斜杠
\
并将所有内容放在同一行上) -
如果使用 Powershell,请将
-D
参数括在双引号中,例如"-DprojectArtifactId=rest-kotlin-quickstart"
当将 kotlin
添加到扩展列表时,Maven 插件将生成一个配置正确的项目,以便与 Kotlin 一起使用。此外,org.acme.ReactiveGreetingResource
类被实现为 Kotlin 源代码(生成的测试也是如此)。在扩展列表中添加 rest-jackson
会导致导入 Quarkus REST(以前的 RESTEasy Reactive)和 Jackson 扩展。
ReactiveGreetingResource
看起来像这样
package org.acme
import jakarta.ws.rs.GET
import jakarta.ws.rs.Path
import jakarta.ws.rs.Produces
import jakarta.ws.rs.core.MediaType
@Path("/hello")
class ReactiveGreetingResource {
@GET
@Produces(MediaType.TEXT_PLAIN)
fun hello() = "Hello from Quarkus REST"
}
更新代码
为了展示 Kotlin 用法的更实际的示例,我们将添加一个简单的 数据类,名为 Greeting
,如下所示
package org.acme.rest
data class Greeting(val message: String = "")
我们还更新 ReactiveGreetingResource
类,如下所示
import jakarta.ws.rs.GET
import jakarta.ws.rs.Path
import jakarta.ws.rs.core.MediaType
@Path("/hello")
class ReactiveGreetingResource {
@GET
fun hello() = Greeting("hello")
}
通过这些更改,/hello
端点将回复 JSON 对象,而不是简单的字符串。
为了使测试通过,我们还需要更新 ReactiveGreetingResourceTest
,如下所示
import org.hamcrest.Matchers.equalTo
@QuarkusTest
class ReactiveGreetingResourceTest {
@Test
fun testHelloEndpoint() {
given()
.`when`().get("/hello")
.then()
.statusCode(200)
.body("message", equalTo("hello"))
}
}
Kotlin 版本
Quarkus Kotlin 扩展已经声明了对一些基本 Kotlin 库(如 kotlin-stdlib-jdk8
和 kotlin-reflect
)的依赖。这些依赖的 Kotlin 版本在 Quarkus BOM 中声明,目前为 2.1.21。因此,建议对其他 Kotlin 库使用相同的 Kotlin 版本。当向另一个基本 Kotlin 库(例如 kotlin-test-junit5
)添加依赖时,您不需要指定版本,因为 Quarkus BOM 包括 Kotlin BOM。
话虽如此,您仍然需要指定要使用的 Kotlin 编译器版本。同样,建议使用与 Quarkus 用于 Kotlin 库的版本相同的版本。
通常不建议在 Quarkus 应用程序中使用不同的 Kotlin 版本。但为了这样做,您必须在 Quarkus BOM 之前导入 Kotlin BOM。 |
重要的 Maven 配置点
与未选择 Kotlin 时的对应版本相比,生成的 pom.xml
包含以下修改
-
quarkus-kotlin
工件已添加到依赖项中。此工件为实时重新加载模式下的 Kotlin 提供支持(稍后将详细介绍) -
kotlin-stdlib-jdk8
也作为依赖项添加。 -
Maven 的
sourceDirectory
和testSourceDirectory
构建属性配置为指向 Kotlin 源代码(分别为src/main/kotlin
和src/test/kotlin
) -
kotlin-maven-plugin
配置如下
<plugin>
<artifactId>kotlin-maven-plugin</artifactId>
<groupId>org.jetbrains.kotlin</groupId>
<version>${kotlin.version}</version>
<executions>
<execution>
<id>compile</id>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
<configuration>
<compilerPlugins>
<plugin>all-open</plugin> (1)
</compilerPlugins>
<pluginOptions>
<!-- Each annotation is placed on its own line -->
<option>all-open:annotation=jakarta.ws.rs.Path</option>
</pluginOptions>
</configuration>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-allopen</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
</plugin>
1 | 启用 all-open 注释插件(请参阅下面的讨论) |
需要注意的重要一点是使用 all-open Kotlin 编译器插件。为了理解为什么需要此插件,首先我们需要注意的是,默认情况下,从 Kotlin 编译器生成的所有类都标记为 final
。
但是,具有 final
类对于需要创建 动态代理的各种框架来说效果不佳。
因此,all-open
Kotlin 编译器插件允许我们配置编译器不将具有某些注释的类标记为 final
。在上面的代码片段中,我们指定用 jakarta.ws.rs.Path
注释的类不应是 final
。
例如,如果您的应用程序包含用 jakarta.enterprise.context.ApplicationScoped
注释的 Kotlin 类,则还需要添加 <option>all-open:annotation=jakarta.enterprise.context.ApplicationScoped</option>
。对于需要在运行时创建动态代理的任何类(如 JPA Entity 类)也是如此。
Quarkus 的未来版本将以一种方式配置 Kotlin 编译器插件,使其无需更改此配置。
重要的 Gradle 配置点
与 Maven 配置类似,当使用 Gradle 时,选择 Kotlin 时需要进行以下修改
-
quarkus-kotlin
工件已添加到依赖项中。此工件为实时重新加载模式下的 Kotlin 提供支持(稍后将详细介绍) -
kotlin-stdlib-jdk8
也作为依赖项添加。 -
Kotlin 插件已激活,这隐式地添加了
sourceDirectory
和testSourceDirectory
构建属性,以指向 Kotlin 源代码(分别为src/main/kotlin
和src/test/kotlin
) -
all-open Kotlin 插件告诉编译器不要将带有突出显示的注释的类标记为 final(根据需要进行自定义)
-
当使用 native-image 时,必须声明 http(或 https)协议的使用
-
以下是一个示例配置
plugins {
id 'java'
id 'io.quarkus'
id "org.jetbrains.kotlin.jvm" version "2.1.21" (1)
id "org.jetbrains.kotlin.plugin.allopen" version "2.1.21" (1)
}
repositories {
mavenLocal()
mavenCentral()
}
dependencies {
implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.1.21'
implementation enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}")
implementation 'io.quarkus:quarkus-rest'
implementation 'io.quarkus:quarkus-rest-jackson'
implementation 'io.quarkus:quarkus-kotlin'
testImplementation 'io.quarkus:quarkus-junit5'
testImplementation 'io.rest-assured:rest-assured'
}
group = '...' // set your group
version = '1.0.0-SNAPSHOT'
java {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
allOpen { (2)
annotation("jakarta.ws.rs.Path")
annotation("jakarta.enterprise.context.ApplicationScoped")
annotation("jakarta.persistence.Entity")
annotation("io.quarkus.test.junit.QuarkusTest")
}
compileKotlin {
kotlinOptions.jvmTarget = JavaVersion.VERSION_21
kotlinOptions.javaParameters = true
}
compileTestKotlin {
kotlinOptions.jvmTarget = JavaVersion.VERSION_21
}
1 | 需要指定 Kotlin 插件版本。 |
2 | 所需的 all-open 配置,如上面的 Maven 指南所示 |
或者,如果您使用 Gradle Kotlin DSL
plugins {
kotlin("jvm") version "2.1.21" (1)
kotlin("plugin.allopen") version "2.1.21"
id("io.quarkus")
}
repositories {
mavenLocal()
mavenCentral()
}
val quarkusPlatformGroupId: String by project
val quarkusPlatformArtifactId: String by project
val quarkusPlatformVersion: String by project
group = "..."
version = "1.0.0-SNAPSHOT"
repositories {
mavenLocal()
mavenCentral()
}
dependencies {
implementation(kotlin("stdlib-jdk8"))
implementation(enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}"))
implementation("io.quarkus:quarkus-kotlin")
implementation("io.quarkus:quarkus-rest")
implementation("io.quarkus:quarkus-rest-jackson")
testImplementation("io.quarkus:quarkus-junit5")
testImplementation("io.rest-assured:rest-assured")
}
group = '...' // set your group
version = "1.0.0-SNAPSHOT"
java {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
allOpen { (2)
annotation("jakarta.ws.rs.Path")
annotation("jakarta.enterprise.context.ApplicationScoped")
annotation("jakarta.persistence.Entity")
annotation("io.quarkus.test.junit.QuarkusTest")
}
kotlin {
compilerOptions {
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21
javaParameters = true
}
}
1 | 需要指定 Kotlin 插件版本。 |
2 | 所需的 all-open 配置,如上面的 Maven 指南所示 |
覆盖 Quarkus BOM Kotlin 版本 (Gradle)
如果您想在应用程序中使用与 Quarkus 的 BOM 中指定的版本不同的版本(例如,尝试预发布功能或出于兼容性原因),您可以使用 Gradle 依赖项中的 strictly {}
版本修饰符来做到这一点。例如
plugins {
id("io.quarkus")
kotlin("jvm") version "1.7.0-Beta"
kotlin("plugin.allopen") version "1.7.0-Beta"
}
configurations.all {
resolutionStrategy {
force "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.0-Beta"
force "org.jetbrains.kotlin:kotlin-reflect:1.7.0-Beta"
}
}
实时重新加载
Quarkus 提供了对实时重新加载源代码更改的支持。此支持也可用于 Kotlin,这意味着开发人员可以更新他们的 Kotlin 源代码并立即看到他们的更改反映出来。
要查看此功能的实际效果,请首先执行
quarkus dev
./mvnw quarkus:dev
./gradlew --console=plain quarkusDev
当对 https://:8080/hello
执行 HTTP GET 请求时,您会看到一个 JSON 消息,其中 message
字段的值为 hello
。
现在使用您最喜欢的编辑器或 IDE,更新 ReactiveGreetingResource.kt
并将 hello
方法更改为以下内容
fun hello() = Greeting("hi")
当您现在对 https://:8080/hello
执行 HTTP GET 请求时,您应该看到一个 JSON 消息,其中 message
字段的值为 hi
。
需要注意的一点是,当对彼此具有依赖关系的 Java 和 Kotlin 源代码进行更改时,实时重新加载功能不可用。我们希望在将来减轻此限制。
配置实时重新加载编译器
如果您需要自定义开发模式下 kotlinc
使用的编译器标志,您可以在 quarkus 插件中配置它们
<plugin>
<groupId>${quarkus.platform.group-id}</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<version>${quarkus.platform.version}</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
<compilerOptions>
<compiler>
<name>kotlin</name>
<args>
<arg>-Werror</arg>
</args>
</compiler>
</compilerOptions>
</configuration>
...
</plugin>
quarkusDev {
compilerOptions {
compiler("kotlin").args(['-Werror'])
}
}
tasks.quarkusDev {
compilerOptions {
compiler("kotlin").args(["-Werror"])
}
}
打包应用程序
与往常一样,可以使用以下命令打包应用程序
quarkus build
./mvnw install
./gradlew build
并使用 java -jar target/quarkus-app/quarkus-run.jar
执行。
您也可以使用以下命令构建本机可执行文件
quarkus build --native
./mvnw install -Dnative
./gradlew build -Dquarkus.native.enabled=true
Kotlin 和 Jackson
如果已将 com.fasterxml.jackson.module:jackson-module-kotlin
依赖项和 quarkus-jackson
扩展(或 quarkus-resteasy-jackson
或 quarkus-rest-jackson
扩展之一)添加到项目中,则 Quarkus 会自动将 KotlinModule
注册到 ObjectMapper
bean(有关更多详细信息,请参见此指南)。
当将 Kotlin 数据类与 native-image
一起使用时,您可能会遇到序列化错误,这些错误不会在 JVM
版本中发生,即使已注册 Kotlin Jackson 模块也是如此。如果您的 JSON 层次结构更复杂,则尤其如此,其中较低节点上的问题会导致序列化失败。显示错误消息是一个包罗万象的消息,通常会显示根对象的问题,但情况可能并非如此。
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `Address` (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
为了确保与 native-image
的完全兼容性,建议应用 Jackson @field:JsonProperty("fieldName")
注释,并设置一个可为空的默认值,如下所示。您可以使用 IntelliJ IDEA 插件(例如 JSON to Kotlin Class)自动生成示例 JSON 的 Kotlin 数据类,并轻松启用 Jackson 注释并选择可为空的参数作为自动代码生成的一部分。
import com.fasterxml.jackson.annotation.JsonProperty
data class Response(
@field:JsonProperty("chart")
val chart: ChartData? = null
)
data class ChartData(
@field:JsonProperty("result")
val result: List<ResultItem?>? = null,
@field:JsonProperty("error")
val error: Any? = null
)
data class ResultItem(
@field:JsonProperty("meta")
val meta: Meta? = null,
@field:JsonProperty("indicators")
val indicators: IndicatorItems? = null,
@field:JsonProperty("timestamp")
val timestamp: List<Int?>? = null
)
...
将 Kotlin 与 Quarkus REST 一起使用时,值得考虑使用 |
Kotlin 和 Kubernetes 客户端
当使用 quarkus-kubernetes
扩展并且具有绑定到 CustomResource 定义的 Kotlin 类(就像您构建运算符一样)时,您需要知道底层的 Fabric8 Kubernetes 客户端使用其自己的静态 Jackson ObjectMapper
,可以使用 KotlinModule
按如下方式配置它
import io.fabric8.kubernetes.client.utils.Serialization
import com.fasterxml.jackson.module.kotlin.KotlinModule
...
val kotlinModule = KotlinModule.Builder().build()
Serialization.jsonMapper().registerModule(kotlinModule)
Serialization.yamlMapper().registerModule(kotlinModule)
请仔细测试此编译到本机映像,如果遇到问题,则回退到与 Java 兼容的 Jackson 绑定。
协程支持
扩展
以下扩展通过允许在方法签名上使用 Kotlin 的 suspend
关键字来提供对 Kotlin 协程的支持。
扩展 | 注释 |
---|---|
|
为 Jakarta REST 资源方法提供支持 |
|
为 REST 客户端接口方法提供支持 |
|
为 Reactive 消息传递方法提供支持 |
|
为调度器方法提供支持 |
|
为声明式基于注释的 API 提供支持 |
|
为 |
|
为服务器端和客户端端点方法提供支持 |
Kotlin 协程和 Mutiny
Kotlin 协程提供了一种命令式编程模型,实际上是以异步、响应式的方式执行的。为了简化 Mutiny 和 Kotlin 之间的互操作性,有一个模块 io.smallrye.reactive:mutiny-kotlin
,此处 描述了该模块。
CDI @Inject 与 Kotlin
Kotlin 反射注释处理与 Java 不同。使用 CDI @Inject 时,您可能会遇到错误,例如:“kotlin.UninitializedPropertyAccessException: lateinit property xxx has not been initialized”
在下面的示例中,可以通过调整注释,添加 @field: Default,来轻松解决此问题,以处理 Kotlin 反射注释定义中缺少 @Target 的情况。
import jakarta.inject.Inject
import jakarta.enterprise.inject.Default
import jakarta.enterprise.context.ApplicationScoped
import jakarta.ws.rs.GET
import jakarta.ws.rs.Path
import jakarta.ws.rs.Produces
import jakarta.ws.rs.core.MediaType
@ApplicationScoped
class GreetingService {
fun greeting(name: String): String {
return "hello $name"
}
}
@Path("/")
class ReactiveGreetingResource {
@Inject
@field: Default (1)
lateinit var service: GreetingService
@GET
@Produces(MediaType.TEXT_PLAIN)
@Path("/hello/{name}")
fun greeting(name: String): String {
return service.greeting(name)
}
}
1 | Kotlin 需要 @field: xxx 限定符,因为它在注释定义中没有 @Target。在此示例中添加 @field: xxx。@Default 用作限定符,显式指定使用默认 bean。 |
或者,首选使用构造函数注入,它无需修改 Java 示例即可工作,提高了可测试性,并且最符合 Kotlin 编程风格。
import jakarta.enterprise.context.ApplicationScoped
import jakarta.ws.rs.GET
import jakarta.ws.rs.Path
import jakarta.ws.rs.Produces
import jakarta.ws.rs.core.MediaType
@ApplicationScoped
class GreetingService {
fun greeting(name: String): String {
return "hello $name"
}
}
@Path("/")
class ReactiveGreetingResource(
private val service: GreetingService
) {
@GET
@Produces(MediaType.TEXT_PLAIN)
@Path("/hello/{name}")
fun greeting(name: String): String {
return service.greeting(name)
}
}