Quarkus - Using Kotlin

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 provides first class support for using Kotlin as will be explained in this guide.

This technology is considered preview.

In preview, backward compatibility and presence in the ecosystem is not guaranteed. Specific improvements might require to change configuration or APIs and plans to become stable are under way. Feedback is welcome on our mailing list or as issues in our GitHub issue tracker.

For a full list of possible extension statuses, check our FAQ entry.

Prerequisites

To complete this guide, you need:

  • less than 10 minutes

  • an IDE

  • JDK 1.8+ installed with JAVA_HOME configured appropriately

  • Apache Maven 3.6.2+

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

Creating the Maven project

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

  1. mvn io.quarkus:quarkus-maven-plugin:1.7.6.Final:create \
  2. -DprojectGroupId=org.acme \
  3. -DprojectArtifactId=rest-kotlin-quickstart \
  4. -DclassName="org.acme.rest.GreetingResource" \
  5. -Dpath="/greeting" \
  6. -Dextensions="kotlin,resteasy-jsonb"
  7. cd 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.rest.GreetingResource class is implemented as Kotlin source code (as is the case with the generated tests). The addition of resteasy-jsonb in the extension list results in importing the RESTEasy/JAX-RS and JSON-B extensions.

GreetingResource.kt looks like this:

  1. package org.acme.rest
  2. import javax.ws.rs.GET
  3. import javax.ws.rs.Path
  4. import javax.ws.rs.Produces
  5. import javax.ws.rs.core.MediaType
  6. @Path("/greeting")
  7. class GreetingResource {
  8. @GET
  9. @Produces(MediaType.TEXT_PLAIN)
  10. fun hello() = "hello"
  11. }

Update code

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

  1. package org.acme.rest
  2. data class Greeting(val message: String = "")

We also update the GreetingResource.kt like so:

  1. import javax.ws.rs.GET
  2. import javax.ws.rs.Path
  3. import javax.ws.rs.Produces
  4. import javax.ws.rs.core.MediaType
  5. @Path("/greeting")
  6. class GreetingResource {
  7. @GET
  8. @Produces(MediaType.APPLICATION_JSON)
  9. fun hello() = Greeting("hello")
  10. }

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

To make the test pass, we also need to update GreetingResourceTest.kt like so:

  1. @QuarkusTest
  2. class GreetingResourceTest {
  3. @Test
  4. fun testHelloEndpoint() {
  5. given()
  6. .`when`().get("/greeting")
  7. .then()
  8. .statusCode(200)
  9. .body("message", equalTo("hello"))
  10. }
  11. }

Important Maven configuration points

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:

  1. <plugin>
  2. <artifactId>kotlin-maven-plugin</artifactId>
  3. <groupId>org.jetbrains.kotlin</groupId>
  4. <version>${kotlin.version}</version>
  5. <executions>
  6. <execution>
  7. <id>compile</id>
  8. <goals>
  9. <goal>compile</goal>
  10. </goals>
  11. </execution>
  12. <execution>
  13. <id>test-compile</id>
  14. <goals>
  15. <goal>test-compile</goal>
  16. </goals>
  17. </execution>
  18. </executions>
  19. <configuration>
  20. <compilerPlugins>
  21. <plugin>all-open</plugin>
  22. </compilerPlugins>
  23. <pluginOptions>
  24. <!-- Each annotation is placed on its own line -->
  25. <option>all-open:annotation=javax.ws.rs.Path</option>
  26. </pluginOptions>
  27. </configuration>
  28. <dependencies>
  29. <dependency>
  30. <groupId>org.jetbrains.kotlin</groupId>
  31. <artifactId>kotlin-maven-allopen</artifactId>
  32. <version>${kotlin.version}</version>
  33. </dependency>
  34. </dependencies>
  35. </plugin>

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.

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

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 javax.ws.rs.Path should not be final.

If your application contains classes annotated with javax.enterprise.context.ApplicationScoped for example, then <option>all-open:annotation=javax.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.

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

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:

  1. plugins {
  2. id 'java'
  3. id 'io.quarkus' version '1.7.6.Final' (1)
  4. id "org.jetbrains.kotlin.jvm" version "{kotlin-version}" (2)
  5. id "org.jetbrains.kotlin.plugin.allopen" version "{kotlin-version}" (2)
  6. }
  7. repositories {
  8. mavenCentral()
  9. }
  10. group = '...' // set your group
  11. version = '1.0.0-SNAPSHOT'
  12. dependencies {
  13. implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:{kotlin-version}'
  14. (3)
  15. implementation enforcedPlatform('io.quarkus:quarkus-bom:1.7.6.Final')
  16. implementation 'io.quarkus:quarkus-resteasy'
  17. implementation 'io.quarkus:quarkus-resteasy-jsonb'
  18. implementation 'io.quarkus:quarkus-kotlin'
  19. testImplementation 'io.quarkus:quarkus-junit5'
  20. testImplementation 'io.rest-assured:rest-assured'
  21. }
  22. sourceCompatibility = '1.8'
  23. targetCompatibility = '1.8'
  24. test {
  25. useJUnitPlatform()
  26. exclude '**/Native*'
  27. }
  28. buildNative {
  29. enableHttpUrlHandler = true
  30. }
  31. allOpen { (4)
  32. annotation("javax.ws.rs.Path")
  33. annotation("javax.enterprise.context.ApplicationScoped")
  34. annotation("io.quarkus.test.junit.QuarkusTest")
  35. }
1The Quarkus plugin needs to be applied.
2The Kotlin plugin version needs to be specified.
3We include the Quarkus BOM using Gradle’s relevant syntax
4The all-open configuration required, as per Maven guide above

or, if you use the Gradle Kotlin DSL:

  1. import io.quarkus.gradle.tasks.QuarkusNative
  2. import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
  3. group = "..."
  4. version = "1.0.0-SNAPSHOT"
  5. plugins {
  6. java
  7. id("io.quarkus") version "1.7.6.Final" (1)
  8. kotlin("jvm") version "{kotlin-version}" (2)
  9. id("org.jetbrains.kotlin.plugin.allopen") version "{kotlin-version}" (2)
  10. }
  11. repositories {
  12. mavenCentral()
  13. }
  14. dependencies {
  15. implementation(kotlin("stdlib-jdk8"))
  16. (3)
  17. implementation(enforcedPlatform("io.quarkus:quarkus-bom:${quarkusVersion}"))
  18. implementation("io.quarkus:quarkus-kotlin")
  19. implementation("io.quarkus:quarkus-resteasy")
  20. implementation("io.quarkus:quarkus-resteasy-jsonb")
  21. testImplementation("io.quarkus:quarkus-junit5")
  22. testImplementation("io.rest-assured:rest-assured")
  23. }
  24. tasks {
  25. named<QuarkusNative>("buildNative") {
  26. setEnableHttpUrlHandler(true)
  27. }
  28. }
  29. allOpen { (4)
  30. annotation("javax.ws.rs.Path")
  31. annotation("javax.enterprise.context.ApplicationScoped")
  32. annotation("io.quarkus.test.junit.QuarkusTest")
  33. }
  34. java {
  35. sourceCompatibility = JavaVersion.VERSION_1_8
  36. targetCompatibility = JavaVersion.VERSION_1_8
  37. }
  38. val compileKotlin: KotlinCompile by tasks
  39. compileKotlin.kotlinOptions {
  40. jvmTarget = "1.8"
  41. }
  42. val compileTestKotlin: KotlinCompile by tasks
  43. compileTestKotlin.kotlinOptions {
  44. jvmTarget = "1.8"
  45. }
1The Quarkus plugin needs to be applied.
2The Kotlin plugin version needs to be specified.
3We include the Quarkus BOM using Gradle’s relevant syntax
4The all-open configuration required, as per Maven guide above

CDI @Inject with Kotlin

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”

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.

Alternatively, prefer the use of constructor injection which works without modification of the Java examples.

  1. import javax.inject.Inject
  2. import javax.enterprise.inject.Default
  3. import javax.enterprise.context.ApplicationScoped
  4. import javax.ws.rs.GET
  5. import javax.ws.rs.Path
  6. import javax.ws.rs.PathParam
  7. import javax.ws.rs.Produces
  8. import javax.ws.rs.core.MediaType
  9. @ApplicationScoped
  10. class GreetingService {
  11. fun greeting(name: String): String {
  12. return "hello $name"
  13. }
  14. }
  15. @Path("/")
  16. class GreetingResource {
  17. @Inject
  18. @field: Default (1)
  19. lateinit var service: GreetingService
  20. @GET
  21. @Produces(MediaType.TEXT_PLAIN)
  22. @Path("/greeting/{name}")
  23. fun greeting(@PathParam("name") name: String): String {
  24. return service.greeting(name)
  25. }
  26. }
1Kotlin 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.

Live reload

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: ./mvnw compile quarkus:dev

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

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

  1. fun hello() = Greeting("hi")

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

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.

Packaging the application

As usual, the application can be packaged using ./mvnw clean package and executed using the -runner.jar file. You can also build the native executable using ./mvnw package -Pnative, or ./gradlew buildNative.

Kotlin and Jackson

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