使用Kotlin
Kotlin 是一种非常流行的基于JVM的编程语言(在其他环境中)。在过去的几年里,Kotlin的受欢迎程度激增,使其成为最受欢迎的JVM语言,当然,除了Java之外。
Quarkus为使用Kotlin提供了一流的支持,本指南将对此进行讲解。
先决条件
完成这个指南,你需要:
-
大概15分钟
-
编辑器
-
JDK 17+ installed with
JAVA_HOME
configured appropriately -
Apache Maven 3.9.9
-
如果你愿意的话,还可以选择使用Quarkus CLI
-
如果你想构建原生可执行程序,可以选择安装Mandrel或者GraalVM,并正确配置(或者使用Docker在容器中进行构建)
注意:关于Gradle项目的设置,请见下文,如需进一步参考,请查阅 Gradle设置页面 的指南。
创建Maven项目
首先,我们需要一个新的Kotlin项目。这可以通过以下命令来完成:
For Windows users:
-
If using cmd, (don’t use backward slash
\
and put everything on the same line) -
If using Powershell, wrap
-D
parameters in double quotes e.g."-DprojectArtifactId=rest-kotlin-quickstart"
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
looks like this:
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"
}
更新代码
In order to show a more practical example of Kotlin usage we will add a simple data class called Greeting
like so:
package org.acme.rest
data class Greeting(val message: String = "")
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对象而不是一个简单的字符串来响应请求。
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
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 2.0.21. 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.
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.
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. |
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 | Enables the all-open annotation plugin (see discussion below) |
需要注意的是,这里使用了 all-open 的Kotlin编译器插件。为了理解为什么需要这个插件,首先我们需要注意:在默认情况下,所有从Kotlin编译器生成的类都被标记为 final
。
然而,拥有 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
.
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编译器插件。
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.0.21" (1)
id "org.jetbrains.kotlin.plugin.allopen" version "2.0.21" (1)
}
repositories {
mavenLocal()
mavenCentral()
}
dependencies {
implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.0.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_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 | 需要指定Kotlin插件的版本。 |
2 | 按照上面的Maven指南,需要配置all-open插件 |
或者,如果您使用Gradle Kotlin DSL:
plugins {
kotlin("jvm") version "2.0.21" (1)
kotlin("plugin.allopen") version "2.0.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_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 | 需要指定Kotlin插件的版本。 |
2 | 按照上面的Maven指南,需要配置all-open插件 |
Overriding the Quarkus BOM Kotlin version (Gradle)
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"
}
}
实时重载
Quarkus提供了对源代码的实时重载修改的支持。这种支持也适用于Kotlin,这意味着开发者可以更新他们的Kotlin源代码,并立即看到代码更改带来的反馈。
要体验到这个功能的作用,首先要执行:
quarkus dev
./mvnw quarkus:dev
./gradlew --console=plain quarkusDev
当执行一个对 http://localhost:8080/hello
的HTTP GET请求时,您会看到一个JSON消息,其 message
字段的值是 hello
。
现在使用您喜欢的编辑器或IDE,更新 ReactiveGreetingResource.kt
,并将 hello
方法改为如下:
fun hello() = Greeting("hi")
当您现在执行一个对 http://localhost:8080/hello
的HTTP GET请求时,您应该会看到一个JSON消息,其 message
字段的值是 hi
。
有一点需要注意的是,当对相互有依赖关系的Java和Kotlin源代码进行修改时,实时重载功能是不可用的。我们希望在未来能缓解这一限制。
配置实时重载编译器
如果您需要定制 kotlinc
在开发模式下使用的编译标志(compiler flags),您可以在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
来执行。
您也可以用以下方法构建原生(native)可执行文件:
quarkus build --native
./mvnw install -Dnative
./gradlew build -Dquarkus.native.enabled=true
Kotlin和Jackson
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).
当使用Kotlin数据类与 native-image
时,尽管Kotlin Jackson模块已被注册,但您可能会遇到JVM版本中不会出现的序列化错误。特别是如果您有一个更复杂的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)
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和Kubernetes客户端
当使用 quarkus-kubernetes
扩展并让Kotlin类绑定到CustomResource定义时(就像您在构建operator时一样),您需要注意底层的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)
请在编译到native-image时仔细测试,如果遇到问题,请退回到与Java兼容的Jackson binding。
Coroutines 支持
扩展
以下扩展通过允许在方法签名上使用Kotlin的 suspend
关键字,为Kotlin Coroutines提供支持。
扩展 | 备注 |
---|---|
|
Support is provided for Jakarta REST Resource Methods |
|
为REST客户端接口方法提供支持 |
|
提供对响应式信息传递方法的支持 |
|
为调度器方法提供支持 |
|
对基于声明性注解的API提供支持 |
|
Support is provided for |
|
Support is provided for server-side and client-side endpoint methods |
Kotlin coroutines和Mutiny
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.
使用Kotlin的CDI @Inject
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)
}
}