DEV Community

Vinicius Carvalho
Vinicius Carvalho

Posted on

Building a native ktor application with GraalVM

The source code for this post can be found on this github repo

Introduction

On my last post: Building a native cli with kotlin and graalvm, I've demonstrated how to build native cli application using graalvm. On this post we will build a web application using ktor.io and the GraalVM.

Requirements

  • GraalVM installed and $GRAALVM_BIN pointing to the binary folder of your install

Building it

Ktor supports several server options (Netty, Jetty, CIO). Netty is my favorite option for java applications, but it can be quite painful trying to get netty to work with GraalVM as explained here

So I tried the CIO server (leverages only kotlin code, and coroutines). I then found out that GraalVM does not support the bytecode generated by coroutines.

Fortunately after some googling I bumped into kraal which seems to solve the problem.

The example will use the Gradle Kotlin DSL, just create a new Gradle project on intellij and mark "use Kotlin DSL"

From there modify your build.gradle.kts to look like this:

import org.gradle.jvm.tasks.Jar
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    kotlin("jvm") version "1.3.21"
    id("com.hpe.kraal") version "0.0.15" // kraal version - for makeRelease.sh
}

group = "io.igx.kotlin"
version = "1.0-SNAPSHOT"

repositories {
    mavenCentral()
    jcenter()
}

dependencies {
    implementation(kotlin("stdlib-jdk8"))
    implementation("org.slf4j:slf4j-simple:1.7.26")
    implementation("io.ktor:ktor-server-cio:1.1.3")
    implementation("io.ktor:ktor-gson:1.1.3")
}

tasks.withType<KotlinCompile>().configureEach {
    kotlinOptions {
        jvmTarget = "1.8"
        // need use-experimental for Ktor CIO
        freeCompilerArgs += listOf("-Xuse-experimental=kotlin.Experimental", "-progressive")
        // disable -Werror with: ./gradlew -PwarningsAsErrors=false
        allWarningsAsErrors = project.findProperty("warningsAsErrors") != "false"
    }
}


val fatjar by tasks.creating(Jar::class) {

    from(kraal.outputZipTrees) {
        exclude("META-INF/*.SF")
        exclude("META-INF/*.DSA")
        exclude("META-INF/*.RSA")
    }

    manifest {
        attributes("Main-Class" to "io.igx.kotlin.ApplicationKt")
    }

    destinationDirectory.set(project.buildDir.resolve("fatjar"))
    archiveFileName.set("ktor-native.jar")
}

tasks.named("assemble").configure {
    dependsOn(fatjar)
}

As usual, just edit the group/Main-Class to match your own packaging structure.

Let's make sure our app outputs some JSON based on a domain class:

data class Driver(val id: Int, val firstName: String, val lastName: String, val nationality: String)

Your Application should look like this one:

fun main(args: Array<String>) {
    val server = embeddedServer(CIO, 8080, module = Application::module)
    server.start(wait = true)
}

fun Application.module() {
    install(CallLogging)
    install(ContentNegotiation) {
        gson {
            setPrettyPrinting()
        }
    }
    routing {
       get("/drivers"){
           call.respond(Driver(102, "Ayrton", "Senna", "Brazilian"))
       }
    }
}

Testing

run ./gradlew clean build and then java -jar build/fatjar/ktor-native.jar and your app should be app and running:

[DefaultDispatcher-worker-1] INFO ktor.application - No ktor.deployment.watch patterns specified, automatic reload is not active
[DefaultDispatcher-worker-1] INFO ktor.application - Responding at http://0.0.0.0:8080

Hit http://0.0.0.0:8080/drivers

{
"id": 102,
"firstName": "Ayrton",
"lastName": "Senna",
"nationality": "Brazilian"
}

Native image

As with our previous post, run the native-image command:

$GRAALVM_BIN/native-image  --report-unsupported-elements-at-runtime --jar build/fatjar/ktor-native.jar ktor-native --enable-url-protocols=http --no-server

run ./ktor-native, hit http://0.0.0.0:8080/drivers and you will get:

{}

Say Whaaat?

Reflection issues

So, GraalVM needs a little help handling reflection as explained here.

Add the following file to the root of your project (reflection.json)


[
   {
    "name" : "io.igx.kotlin.model.Driver",
    "allDeclaredConstructors" : true,
    "allPublicConstructors" : true,
    "allDeclaredMethods" : true,
    "allPublicMethods" : true,
     "fields" : [
       { "name" : "id" },
       { "name" : "firstName" },
       { "name" : "lastName" },
       { "name" : "nationality" }
     ]
   },

  {
  "name" : "java.lang.Integer",
  "methods" : [{ "name" : "parseInt", "parameterTypes" : ["java.lang.String"]}]
}, {
  "name" : "java.lang.Long",
  "methods" : [{ "name" : "parseLong", "parameterTypes" : ["java.lang.String"]}]
}, {
  "name" : "java.lang.Boolean",
  "methods" : [{ "name" : "parseBoolean", "parameterTypes" : ["java.lang.String"]}]
}, {
  "name" : "java.lang.Byte",
  "methods" : [{ "name" : "parseByte", "parameterTypes" : ["java.lang.String"]}]
}, {
  "name" : "java.lang.Short",
  "methods" : [{ "name" : "parseShort", "parameterTypes" : ["java.lang.String"]}]
}, {
  "name" : "java.lang.Float",
  "methods" : [{ "name" : "parseFloat", "parameterTypes" : ["java.lang.String"]}]
}, {
  "name" : "java.lang.Double",
  "methods" : [{ "name" : "parseDouble", "parameterTypes" : ["java.lang.String"]}]
}
]

Run the native-image command again, but this time add the -H:ReflectionConfigurationFilesflag

~/java/graalvm-ce-1.0.0-rc13/Contents/Home/bin/native-image  --report-unsupported-elements-at-runtime -H:ReflectionConfigurationFiles=reflection.json  -jar build/fatjar/example.jar ktor-native --enable-url-protocols=http --no-server

Try it again and you will see the original results of your Driver JSON payload.

Final Thoughts

Dealing with reflection on GraalVM can be a bit challenging, luckily to us, several people on the industry like the folks at RedHat are trying to come up with frameworks such as quarkus.io to overcome this limitation.

I have no doubt that IDEs will soon catch up and be able to inspect the code and generate those reflection files for us.

Meanwhile enjoy your blazing fast ktor server running as a native image on your server.

Happy Coding!

Top comments (2)

Collapse
 
faakhy profile image
Rémy | FakeRunner

A little error in your command line:
->--jar it's -jar

Collapse
 
stokemasterjack profile image
David Ford

Ktor now defaults to using kotlinx-serialization (rather than gson or jackson).

Unlike gson or jackson, kotlinx-serialization does not use reflection.