1. Introduction

JVM languages promise their users: “Write once – run everywhere”. However, when it comes to shipping, many small problems arise. The ecosystem has evolved, and now it is unthinkable to create any application without dozens and dozens of dependency libraries. All of them must end up in the classpath, and if not – likely, the application won’t even start.

Sometimes we can afford to create another directory and to put all the dependency jars in there. But sometimes we want that uber-jar or else fat-jar, which can be run with java command:

java -jar my-application.jar

One of the ways to achieve that with Gradle has already been discussed on our site. Let’s discuss the other possibilities. We will also use Kotlin Gradle DSL in our build file instead of Groovy.

2. Lightweight Application With Gradle Plugin

Now, “self-executing jar” might be a bit of a misnomer. If we want our application to be runnable on Windows, Linux, and macOS platforms without any CLI fuss, we might use a standard Gradle Plugin – application:

plugins {
    application // enabling the plugin here
    kotlin("jvm") version "1.6.0"
}

// Other configuration here

application {
    mainClass.set("the.path.to.the.MainClass")
}

The only thing we need to do is point to the main class of our application – the class containing the function main(args: Array) that we want to invoke at the start.

The Application Plugin also automatically includes the Distribution Plugin. The Distribution Plugin creates two archives: TAR and ZIP. Their contents are identical and include the project jar, all dependency jars, and two scripts: Bash and .bat-file. Then, distributing our application is no problem at all: We can use our-project/build/distributions/our-project-1.0.1.zip, unpack it, and run the executable script:

unzip our-project-1.0.1.zip
./our-project-1.0.1/bin/our-project-1.0.1

This will start our application on Linux and macOS. The command ./our-project-1.0.1/bin/our-project-1.0.1.bat will start it on Windows.

However, this way of packing and distributing software is not ideal. Indeed, we must assume the existence of a TAR or ZIP un-archiver on the target host, as well as a valid JRE. What if we want the most minimal number of files to transfer and the most straightforward way to start the application?

Enter the fat jar.

3. “Fat Jar” for the Lightweight Application

The next way to ship our application is to build the application plugin, discussed in the previous chapter. We’ll add a custom task to reach our goal.

If we don’t plan to use our project as a dependency in some third project, then we might pack every dependency in a single jar and ship:

tasks {
    val fatJar = register<Jar>("fatJar") {
        dependsOn.addAll(listOf("compileJava", "compileKotlin", "processResources")) // We need this for Gradle optimization to work
        archiveClassifier.set("standalone") // Naming the jar
        duplicatesStrategy = DuplicatesStrategy.EXCLUDE
        manifest { attributes(mapOf("Main-Class" to application.mainClass)) } // Provided we set it up in the application plugin configuration
        val sourcesMain = sourceSets.main.get()
        val contents = configurations.runtimeClasspath.get()
            .map { if (it.isDirectory) it else zipTree(it) } +
                sourcesMain.output
        from(contents)
    }
    build {
        dependsOn(fatJar) // Trigger fat jar creation during build
    }
}

The usage of John Engelman’s ShadowJar plugin becomes necessary only if there’s an actual need for shading our dependencies. To shade a dependency is to rewrite its package name to not clash with the duplicate class files of another version of the same dependency. However, an executable jar is an unlikely candidate for a dependency, and we most probably can skip that in every project.

The code above produces one more jar file in our project’s build/libs directory: our-project-1.0.1-standalone.jar. We can run it directly:

java -jar our-project-1.0.1-standalone.jar

3. Spring Boot Application

Spring Boot applications are executable out of the box. Instead of repackaging all the jars, like in our second approach, the Spring Boot plugin wraps dependency jars with one more jar. The creation of a project is simple in this case. We can use Spring Initializr, choose the technologies we need in our project, and download a ready-to-go project. The plugins are all that we want:

plugins {
    id("org.springframework.boot") version "2.6.0"
    id("io.spring.dependency-management") version "1.0.11.RELEASE"
    kotlin("jvm") version "1.6.0"
    kotlin("plugin.spring") version "1.6.0"
}

And then, our application must be a valid Spring Boot application because to manipulate the classpath, Spring Boot uses its starter. After the Gradle build has succeeded, we’ll find our-project-1.0.1.jar in the build/libs directory.

java -jar our-project-1.0.1.jar

4. Conclusion

Depending on the project, its size, and the technologies used, we might opt for an unzipped distribution, a self-built fat jar, a jar with shaded dependencies, or a Spring Boot application. In all the cases, Gradle provides easy tools to achieve our goal.

The sample projects for the self-built fat jar and the Spring Boot application can be found over on GitHub.


» 下一篇: Kotlin面试问题