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:
{}
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)
A little error in your command line:
->
--jar
it's-jar
Ktor now defaults to using kotlinx-serialization (rather than gson or jackson).
Unlike gson or jackson, kotlinx-serialization does not use reflection.