Recently we wanted to add a specific kind of alerting to some of our build process via a third-party product. As part of that, we needed to run some of our Kotlin code in the third-party product as a standalone application. The third party had a limitation in that they only support this kind of execution using java -jar file.jar
directly with a single jar file.
Luckily, java
easily supports this via jar files with:
An application’s entry point via the
Main-Class
property in the jar’s manifest filePacking any third-party classes in the jar (AKA a ‘fat’ jar).
We use Gradle for our build tool, and have used the application
plugin to build and run arbitrary standalone applications. In this case, we already had the class defined in Kotlin with a main function used by Gradle’s application
plugin that we wanted to run in this new third-party system.
Our main class is in a file com/stackhawk/example/Main.kt
:
fun main(args: Array<String>) {
println("Hello World!")
}
Note that unlike Java, Kotlin takes that file name and modifies for Java referencing in Gradle.
Our build.gradle.kts
file then uses it with the application
plugin like this:
plugins {
application
}
application {
mainClassName = "com.stackhawk.example.MainKt" // Really Main.kt
}
With that, we can do this at the shell:
> ./gradlew run
Hello World!
We can’t use Gradle in this third-party system, so we need to produce a jar file with the Main-Class
property set in the manifest to the same class. We can do that using the Gradle jar
task:
tasks.jar { // could also be a new task rather than the default one
manifest {
attributes["Main-Class"] = "com.stackhawk.example.MainKt"
}
}
But we also have a bunch of third-party dependencies that we need to pack into that jar file. Luckily, the jar
task can be expanded to include them in our jar.
tasks.jar { // could also be a new task rather than the default one
manifest {
attributes["Main-Class"] = "com.stackhawk.example.MainKt"
}
from(sourceSets.main.get().output)
dependsOn(configurations.runtimeClasspath)
from({
configurations.runtimeClasspath.get().filter { it.name.endsWith("jar") }.map { zipTree(it) }
})
}
Now we can do this:
> ./gradlew build
> java -jar build/libs/example-0.0.1.jar
Hello World!
Which is exactly how the third-party system wants to run Java/Kotlin applications.
Here’s a complete example that supports both ./gradlew run
and java -jar $OUTPUT_JARFILE
:
val main = "com.stackhawk.example.MainKt"
application {
mainClassName = main
}
tasks.jar {
manifest {
attributes["Main-Class"] = main
}
from(sourceSets.main.get().output)
dependsOn(configurations.runtimeClasspath)
from({
configurations.runtimeClasspath.get().filter { it.name.endsWith("jar") }.map { zipTree(it) }
})
}
Once we had this set up in the project, we were able to generate a jar that worked as expected in the third-party alerting system. Since then, we’ve had other reasons, such as one-off and scheduled jobs, to use a jar in this manner and this Gradle set up has worked for those uses cases as well.