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 usingjava -jar file.jar
directly with a single jar file.Luckily,java
easily supports this via jar files with:
- An application’s entry point via theMain-Class property in the jar’s manifest file
Packing 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’sapplication
plugin that we wanted to run in this new third-party system.Our main class is in a filecom/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.
Ourbuild.gradle.kts
file then uses it with theapplication
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 theMain-Class
property set in the manifest to the same class. We can do that using the Gradlejar
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, thejar
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
andjava -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.
