Multi-project builds with Gradle and Fat JARs with Shadow

When deciding on your dependency manager in Java you have two main choices Maven and Gradle (or something more complex like Google's Bazel). Both manage dependencies well, have robust plugin systems, support checkstyles, run tests and build / publish JARs and sources. Pick whatever you are comfortable with. Gradle is a little less verbose and what we will be using.

Multi-project Builds

Multi-project builds are very useful for splitting a project into separate dependencies. For example you may have a REST service that is split into 3 projects core for common models / logic, client for the HTTP client that interacts with the server and the server. You wouldn't want database dependencies in the client library so this is a clean separation of concerns. We will be using the StubbornJava projects in the example but the separation of logic still holds.

Parent Project

The root project for StubbornJava is the root on the StubbornJava GitHub Repository. Parent projects generally only have a few gradle files and no source code.

settings.gradle

This file is responsible for setting the root project name and including all child projects.

rootProject.name = 'stubbornjava-parent'
include ':stubbornjava-undertow'
include ':stubbornjava-common'
include ':stubbornjava-examples'
include ':stubbornjava-webapp'

gradle/

The gradle/ directory is the default location for including gradle scripts. This is a convienent location to split out our dependencies. The build.gradle file tends to get a bit cluttered, since dependencies are one of the most updated sections and self contained its a great idea to split into its own file gradle/dependencies.gradle. We will be using Gradle's ext tag that is used for extra properties. This is a good spot for shared variables. Normally projects only store the version numbers here but we also store the full dependency strings so they can be reused.

build.gradle

The build.gradle file is where we will load all plugins and include our previous gradle/dependencies.gradle file. This is also where we handle building our fat JAR using the Shadow JAR plugin. Ever run into issues where maven / gradle have multiple versions of the same library from different transitive dependencies? Turning on failOnVersionConflict() will help track down and resolve all these issues. Since we also stored all of our dependency strings in a variable we can iterate them and force their versions to always be used libs.each { k, v -> force(v) }. This means we only need to override library versions if multiple transitive dependencies share a same library with different versions.

You should now be able to run the self contained JAR java -Denv={env} -Xmx{max-heap} -cp '{path-to-jar}' {fully-qualified-class-with-main}. What is very nice about this style of passing the main class instead of using a manifest is the same JAR can be used to run any main method. In this case any of the example servers can be run with this JAR.