Saturday, December 30, 2017

Gradle Recipe: Building a Fat JAR

So far, I haven't seen a Build Tool that is easy to use. Doesn't matter if it was called Make, Maven or SBT. They're all very complex and far away from easy and intuitive.

Since I'm struggling a lot with Gradle lately, this is the first post that shows how I solved my (common) problem: Building a Fat JAR.

I don't know if I see things differently, but my first expectation on a Build Tool is, that it builds my software with all the configured dependencies. Therefore, when using Gradle, you can choose between three dependency types:

  • compile
  • runtime
  • testCompile
As soon as you add a new dependency, you would probably declare it as a compile dependency, because you need it to be able to compile your code. In case you are adding TestNG or JUnit, you declare it as testCompile, because you only need it for your tests. The runtime dependency extends from the compile dependency. My understanding of runtime would mean, that I need this dependency in order to be able to execute my code (for example on a production environment). The good thing is, that it means exactly this. The bad thing is, that it does not automatically pack all your dependencies together, so that you can ship your executable.

Lets say you use Apache Log4j2:

dependencies {
   runtime 'org.apache.logging.log4j:log4j-core:2.10.0'
}

From a user perspective, I would assume that this is everything I need to do - but its not. If you try to execute the JAR that is being build, you'll get a ClassNotFoundException.

The solution is the following build.gradle file:

apply plugin: 'java'

test {
   useTestNG()
}

repositories {
   mavenCentral()
}

dependencies {
   compile 'org.apache.logging.log4j:log4j-api:2.10.0'
   compile 'org.apache.logging.log4j:log4j-core:2.10.0'
   testCompile group: 'org.testng', name: 'testng', version: '6.13.1'
}

task fatJar(type: Jar) {
   baseName = project.name
   from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
   with jar
   manifest {
      attributes(
         'Main-Class': 'org.tobster.foo.GradleExample'
      )
   }
}

assemble.finalizedBy fatJar

Since this is a very common problem, you'll find a lot of solutions, but this was the only one that worked for me. The most interesting part for me is, that you have to write a custom Gradle Task that assembles a Fat JAR. Why do I've to do that? Wouldn't it be possible to have some kind of flag, e.g. fatJAR=true. Maybe that would be to easy.