Using Kotlin

Kotlin是一种非常流行的编程语言,而它以JVM为目标(也在其他环境中)。在过去的几年中,Kotlin 的流行度飙升,使其成为最流行的JVM 语言,当然除了 Java 之外。

Kotlin is a very popular programming language that targets the JVM (amongst other environments). Kotlin has experienced a surge in popularity the last few years making it the most popular JVM language, except for Java of course.

Quarkus 提供对使用 Kotlin 的一流支持,正如本指南中所解释的那样。

Quarkus provides first class support for using Kotlin as will be explained in this guide.

Prerequisites

Unresolved directive in kotlin.adoc - include::{includes}/prerequisites.adoc[]

注:如需了解 Gradle 项目设置,请参阅下方,若要进一步了解,请查阅 Gradle setup page中的指南。

NB: For Gradle project setup please see below, and for further reference consult the guide in the Gradle setup page.

Creating the Maven project

首先,我们需要一个新的 Kotlin 项目。这可以通过使用以下命令来完成:

First, we need a new Kotlin project. This can be done using the following command:

Unresolved directive in kotlin.adoc - include::{includes}/devtools/create-app.adoc[]

将 `kotlin`添加到扩展列表时,Maven 插件将生成一个配置正确的项目以配合 Kotlin 使用。更重要的是,`org.acme.ReactiveGreetingResource`类和所生成的测试实现了 Kotlin 源代码。在扩展列表中添加 `rest-jackson`会导致导入 Quarkus REST(以前称为 RESTEasy Reactive)和 Jackson 扩展。

When adding kotlin to the extensions list, the Maven plugin will generate a project that is properly configured to work with Kotlin. Furthermore, the org.acme.ReactiveGreetingResource class is implemented as Kotlin source code (as is the case with the generated tests). The addition of rest-jackson in the extension list results in importing the Quarkus REST (formerly RESTEasy Reactive) and Jackson extensions.

以下列样式显示 ReactiveGreetingResource

ReactiveGreetingResource looks like this:

ReactiveGreetingResource.kt
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"
}

Update code

为了显示更多关于 Kotlin 用法的实际范例,我们将添加一个简单的 data class,如下所示: Greeting

In order to show a more practical example of Kotlin usage we will add a simple data class called Greeting like so:

Greeting.kt
package org.acme.rest

data class Greeting(val message: String = "")

我们也以这样的方式更新 `ReactiveGreetingResource`类:

We also update the ReactiveGreetingResource class like so:

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 对象代替普通的 String 回复。

With these changes in place the /hello endpoint will reply with a JSON object instead of a simple String.

为使测试通过,我们也需要以这样的方式更新 ReactiveGreetingResourceTest

To make the test pass, we also need to update ReactiveGreetingResourceTest like so:

import org.hamcrest.Matchers.equalTo

@QuarkusTest
class ReactiveGreetingResourceTest {

    @Test
    fun testHelloEndpoint() {
        given()
          .`when`().get("/hello")
          .then()
             .statusCode(200)
             .body("message", equalTo("hello"))
    }

}

Kotlin version

Quarkus Kotlin 扩展已经声明对一些基础 Kotlin 库的依赖,例如 kotlin-stdlib-jdk8`和 `kotlin-reflect。这些依赖项的 Kotlin 版本是在 Quarkus BOM 中声明的,目前位于 {kotlin-version}。因此建议为其他 Kotlin 库使用相同的 Kotlin 版本。当向另一个基础 Kotlin 库(例如 kotlin-test-junit5)添加依赖项时,你不需要指定版本,因为 Quarkus BOM 包含了 Kotlin BOM

The Quarkus Kotlin extension already declares a dependency on some base Kotlin libraries like kotlin-stdlib-jdk8 and kotlin-reflect. The Kotlin version of these dependencies is declared in the Quarkus BOM and is currently at {kotlin-version}. It is therefore recommended to use the same Kotlin version for other Kotlin libraries. When adding a dependency to another base Kotlin library (e.g. kotlin-test-junit5) you don’t need to specify the version, since the Quarkus BOM includes the Kotlin BOM.

话虽如此,你仍然需要指定要使用的 Kotlin 编译器的版本。这里再次建议使用 Quarkus 为 Kotlin 库所使用的相同版本。

This being said, you still need to specify the version of the Kotlin compiler to use. Again, it is recommended to use the same version which Quarkus uses for the Kotlin libraries.

在 Quarkus 应用程序中通常不建议使用其他 Kotlin 版本。但为实现该目的,你必须导入 Kotlin BOM *before*以导入 Quarkus BOM。

Using a different Kotlin version in a Quarkus application is typically not recommended. But in order to do so, you must import the Kotlin BOM before the Quarkus BOM.

Important Maven configuration points

与当未选择 Kotlin 时的对应项相比,所生成的 `pom.xml`包含以下修改:

The generated pom.xml contains the following modifications compared to its counterpart when Kotlin is not selected:

  • The quarkus-kotlin artifact is added to the dependencies. This artifact provides support for Kotlin in the live reload mode (more about this later on)

  • The kotlin-stdlib-jdk8 is also added as a dependency.

  • Maven’s sourceDirectory and testSourceDirectory build properties are configured to point to Kotlin sources (src/main/kotlin and src/test/kotlin respectively)

  • The kotlin-maven-plugin is configured as follows:

pom.xml
<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 Enables the all-open annotation plugin (see discussion below)

值得注意的是使用 all-open Kotlin 编译器插件。为了了解为什么需要此插件,首先我们需要指出,默认情况下,从 Kotlin 编译器生成的所有类均标记为 final

The important thing to note is the use of the all-open Kotlin compiler plugin. In order to understand why this plugin is needed, first we need to note that by default all the classes generated from the Kotlin compiler are marked as final.

但是,拥有 final 类与需要创建 Dynamic Proxies 的各种框架配合得并不好。

Having final classes however does not work well with various frameworks that need to create Dynamic Proxies.

因此,all-open Kotlin 编译器插件允许我们配置编译器,以将拥有特定注释的类标记为 not 类。在上面的片段中,我们指定了用 jakarta.ws.rs.Path 注释的类不应该成为 final

Thus, the all-open Kotlin compiler plugin allows us to configure the compiler to not mark as final classes that have certain annotations. In the snippet above, we have specified that classes annotated with jakarta.ws.rs.Path should not be final.

例如,如果你的应用程序包含用 jakarta.enterprise.context.ApplicationScoped 注释的 Kotlin 类,那么也需要添加 <option>all-open:annotation=jakarta.enterprise.context.ApplicationScoped</option>。对于任何需要在运行时创建动态代理的类(如 JPA 实体类)也是如此。

If your application contains Kotlin classes annotated with jakarta.enterprise.context.ApplicationScoped for example, then <option>all-open:annotation=jakarta.enterprise.context.ApplicationScoped</option> needs to be added as well. Same goes for any class that needs to have a dynamic proxy created at runtime, like a JPA Entity class.

未来版本的 Quarkus 将以一种不需要更改此配置的方式配置 Kotlin 编译器插件。

Future versions of Quarkus will configure the Kotlin compiler plugin in a way that will make it unnecessary to alter this configuration.

Important Gradle configuration points

类似于 Maven 配置,当使用 Gradle 时,在选择 Kotlin 时需要进行以下修改:

Similar to the Maven configuration, when using Gradle, the following modifications are required when Kotlin is selected:

  • The quarkus-kotlin artifact is added to the dependencies. This artifact provides support for Kotlin in the live reload mode (more about this later on)

  • The kotlin-stdlib-jdk8 is also added as a dependency.

  • The Kotlin plugin is activated, which implicitly adds sourceDirectory and testSourceDirectory build properties to point to Kotlin sources (src/main/kotlin and src/test/kotlin respectively)

  • The all-open Kotlin plugin tells the compiler not to mark as final, those classes with the annotations highlighted (customize as required)

  • When using native-image, the use of http (or https) protocol(s) must be declared

  • An example configuration follows:

plugins {
    id 'java'
    id 'io.quarkus'

    id "org.jetbrains.kotlin.jvm" version "{kotlin-version}" (1)
    id "org.jetbrains.kotlin.plugin.allopen" version "{kotlin-version}" (1)
}

repositories {
    mavenLocal()
    mavenCentral()
}

dependencies {
    implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:{kotlin-version}'

   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_11
    targetCompatibility = JavaVersion.VERSION_11
}

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_11
    kotlinOptions.javaParameters = true
}

compileTestKotlin {
    kotlinOptions.jvmTarget = JavaVersion.VERSION_11
}
1 The Kotlin plugin version needs to be specified.
2 The all-open configuration required, as per Maven guide above

或者,如果你使用 Gradle Kotlin DSL:

or, if you use the Gradle Kotlin DSL:

plugins {
    kotlin("jvm") version "{kotlin-version}" (1)
    kotlin("plugin.allopen") version "{kotlin-version}"
    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_11
    targetCompatibility = JavaVersion.VERSION_11
}

allOpen { (2)
    annotation("jakarta.ws.rs.Path")
    annotation("jakarta.enterprise.context.ApplicationScoped")
    annotation("jakarta.persistence.Entity")
    annotation("io.quarkus.test.junit.QuarkusTest")
}

tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
    kotlinOptions.jvmTarget = JavaVersion.VERSION_11.toString()
    kotlinOptions.javaParameters = true
}
1 The Kotlin plugin version needs to be specified.
2 The all-open configuration required, as per Maven guide above

Overriding the Quarkus BOM Kotlin version (Gradle)

如果你想在应用程序中使用不同于 Quarkus BOM 指定的版本(例如,尝试预发布功能或出于兼容性原因),你可以通过在 Gradle 依赖关系中使用 strictly {} 版本修改器来执行此操作。例如:

If you want to use a different version than the one specified by Quarkus' BOM in your application (for example, to try pre-release features or for compatibility reasons), you can do so by using the strictly {} version modifier in your Gradle dependencies. For instance:

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"
    }
}

Live reload

Quarkus 提供对源代码进行实时重新加载更改的支持。Kotlin 还可以使用此支持,这意味着开发人员可以更新其 Kotlin 源代码并立即看到其更改反映。

Quarkus provides support for live reloading changes made to source code. This support is also available to Kotlin, meaning that developers can update their Kotlin source code and immediately see their changes reflected.

要了解此功能的操作,请首先执行:

To see this feature in action, first execute:

Unresolved directive in kotlin.adoc - include::{includes}/devtools/dev.adoc[]

针对 http://localhost:8080/hello 执行 HTTP GET 请求时,你将看到一个 JSON 消息,其 message 字段值为 hello

When executing an HTTP GET request against http://localhost:8080/hello, you see a JSON message with the value hello as its message field.

现在,使用你喜欢的编辑器或 IDE,更新 ReactiveGreetingResource.kt`并将 `hello 方法更改为以下内容:

Now using your favorite editor or IDE, update ReactiveGreetingResource.kt and change the hello method to the following:

fun hello() = Greeting("hi")

现在,当你针对 http://localhost:8080/hello 执行 HTTP GET 请求时,你将看到一个 JSON 消息,其 message 字段值为 hi

When you now execute an HTTP GET request against http://localhost:8080/hello, you should see a JSON message with the value hi as its message field.

需要注意的是,当更改相互依赖的 Java 和 Kotlin 源文件时,实时重新加载功能不可用。我们希望将来能解决此限制。

One thing to note is that the live reload feature is not available when making changes to both Java and Kotlin source that have dependencies on each other. We hope to alleviate this limitation in the future.

Configuring live reload compiler

如果需要在开发模式下自定义 kotlinc 使用的编译器标记,可以在 quarkus 插件中配置它们:

If you need to customize the compiler flags used by kotlinc in development mode, you can configure them in the quarkus plugin:

Maven
<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>
Gradle (Groovy DSL)
quarkusDev {
    compilerOptions {
        compiler("kotlin").args(['-Werror'])
    }
}
Gradle (Kotlin DSL)
tasks.quarkusDev {
     compilerOptions {
        compiler("kotlin").args(["-Werror"])
    }
}

Packaging the application

和往常一样,可以使用以下命令打包应用程序:

As usual, the application can be packaged using:

Unresolved directive in kotlin.adoc - include::{includes}/devtools/build.adoc[]

并使用 java -jar target/quarkus-app/quarkus-run.jar 执行。

and executed with java -jar target/quarkus-app/quarkus-run.jar.

您还可以使用以下命令构建本机可执行文件:

You can also build the native executable using:

Unresolved directive in kotlin.adoc - include::{includes}/devtools/build-native.adoc[]

Kotlin and Jackson

如果项目中添加了 com.fasterxml.jackson.module:jackson-module-kotlin 依赖项和 quarkus-jackson 扩展(或 quarkus-resteasy-jacksonquarkus-rest-jackson 扩展之一),则 Quarkus 会自动将 KotlinModule 注册到 ObjectMapper Bean(有关更多详细信息,请参阅 this 指南)。

If the com.fasterxml.jackson.module:jackson-module-kotlin dependency and the quarkus-jackson extension (or one of the quarkus-resteasy-jackson or quarkus-rest-jackson extensions) have been added to the project, then Quarkus automatically registers the KotlinModule to the ObjectMapper bean (see this guide for more details).

当使用带有 native-image 的 Kotlin 数据类时,您可能会遇到序列化错误,尽管已注册了 Kotlin Jackson 模块,但使用 JVM 版本不会出现此错误。尤其是在具有更复杂的 JSON 层次结构时,较低节点上的问题会导致序列化失败。显示的错误消息是包罗万象的,通常会显示根对象的问题,但这不一定属实。

When using Kotlin data classes with native-image you may experience serialization errors that do not occur with the JVM version, despite the Kotlin Jackson Module being registered. This is especially so if you have a more complex JSON hierarchy, where an issue on a lower node causes a serialization failure. The error message displayed is a catch-all and typically displays an issue with the root object, which may not necessarily be the case.

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 到 Kotlin 类)自动生成示例 JSON 的 Kotlin 数据类,并轻松启用 Jackson 注释并选择可为空的参数,作为自动代码生成的一部分。

To ensure full-compatibility with native-image, it is recommended to apply the Jackson @field:JsonProperty("fieldName") annotation, and set a nullable default, as illustrated below. You can automate the generation of Kotlin data classes for your sample JSON using IntelliJ IDEA plugins (such as JSON to Kotlin Class), and easily enable the Jackson annotation and select nullable parameters as part of the auto-code generation.

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 and the Kubernetes Client

在使用 quarkus-kubernetes 扩展以及绑定 Kotlin 类到 CustomResource 定义(例如在构建运算符时)时,您需要了解底层的 Fabric8 Kubernetes 客户端使用它自己的静态 Jackson ObjectMapper s,可以像下面这样使用 KotlinModule 来配置它:

When working with the quarkus-kubernetes extension and have Kotlin classes bound to CustomResource definitions (like you do for building operators), you need to be aware that the underlying Fabric8 Kubernetes Client uses its own static Jackson ObjectMapper s, which can be configured as follows with the 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)

Please test this carefully on compilation to native images and fallback to Java-compatible Jackson bindings if you experience problems.

Coroutines support

Extensions

以下扩展通过允许在方法签名中使用 Kotlin 的 suspend 关键字,为 Kotlin 协程提供支持。

The following extensions provide support for Kotlin Coroutines by allowing the use of Kotlin’s suspend keyword on method signatures.

Extension Comments

quarkus-rest

Support is provided for Jakarta REST Resource Methods

quarkus-rest-client

Support is provided for REST Client interface methods

quarkus-messaging

Support is provided for Reactive messaging methods

quarkus-scheduler

Support is provided for scheduler methods

quarkus-smallrye-fault-tolerance

Support is provided for the declarative annotation-based API

quarkus-vertx

Support is provided for @ConsumeEvent methods

quarkus-websockets-next

Support is provided for server-side and client-side endpoint methods

Kotlin coroutines and Mutiny

Kotlin 协程提供了一个命令式编程模型,该模型实际上是异步反应式方式执行的。为了简化 Mutiny 和 Kotlin 之间的互操作性,请使用 io.smallrye.reactive:mutiny-kotlin 模块, here 中对此模块进行了说明。

Kotlin coroutines provide an imperative programming model that actually gets executed in an asynchronous, reactive manner. To simplify the interoperability between Mutiny and Kotlin there is the module io.smallrye.reactive:mutiny-kotlin, described here.

CDI @Inject with Kotlin

Kotlin 反射注释处理与 Java 不同。当使用 CDI @Inject 时,您可能会遇到错误,例如:“kotlin.UninitializedPropertyAccessException:lateinit 属性 xxx 未初始化”

Kotlin reflection annotation processing differs from Java. You may experience an error when using CDI @Inject such as: "kotlin.UninitializedPropertyAccessException: lateinit property xxx has not been initialized"

在以下示例中,可以通过调整注释轻松解决此问题,添加 @field:Default,以处理 Kotlin 反射注释定义上 @Target 的缺失。

In the example below, this can be easily solved by adapting the annotation, adding @field: Default, to handle the lack of a @Target on the Kotlin reflection annotation definition.

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 requires a @field: xxx qualifier as it has no @Target on the annotation definition. Add @field: xxx in this example. @Default is used as the qualifier, explicitly specifying the use of the default bean.

或者,倾向于使用构造函数注入,它无需修改 Java 示例即可工作,提高可测试性,并且最符合 Kotlin 编程风格。

Alternatively, prefer the use of constructor injection which works without modification of the Java examples, increases testability and complies best to a Kotlin programming style.

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)
    }

}