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'
include ':stubbornjava-cms-server'

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.

ext {
    versions = [
        jackson           : '2.12.5',      // Json Serializer / Deserializer
        okhttp            : '4.9.1',       // HTTP Client
        slf4j             : '1.7.31',      // Logging 
        logback           : '1.2.5',       // Logging
        logbackJson       : '0.1.5',
        undertow          : '2.2.8.Final', // Webserver
        metrics           : '4.2.2',       // Metrics
        guava             : '30.1.1-jre',  // Common / Helper libraries
        typesafeConfig    : '1.4.1',       // Configuration
        handlebars        : '4.2.0',       // HTML templating
        htmlCompressor    : '1.5.2',       // HTML compression
        hikaricp          : '4.0.3',       // JDBC connection pool
        jool              : '0.9.14',      // Functional Utils
        hsqldb            : '2.6.0',       // In memory SQL db
        aws               : '1.12.62',     // AWS Java SDK
        flyway            : '5.1.4',       // DB migrations
        connectorj        : '8.0.25',      // JDBC MYSQL driver
        jooq              : '3.15.0',      // jOOQ
        hashids           : '1.0.3',       // Id hashing
        failsafe          : '1.1.0',       // retry and circuit breakers
        jsoup             : '1.14.1',      // DOM parsing library
        lombok            : '1.18.20',     // Code gen
        sitemapgen4j      : '1.1.2',       // Sitemap generator for SEO
        jbcrypt           : '0.4',         // BCrypt salted hashing library
        romeRss           : '1.0',         // RSS Library
        kotlin            : '1.4.0',       // Kotlin
        javax             : '1.3.2',
        jbossLogging      : '3.4.2.Final',
        jbossThreads      : '3.4.0.Final',
        wildflyCommon     : '1.5.4.Final-format-001',
        commonsCodec      : '1.15',
        junit             : '4.13.2',        // Unit Testing
    ]
    libs = [
        okhttp                    : "com.squareup.okhttp3:okhttp:$versions.okhttp",
        okhttpUrlConnection       : "com.squareup.okhttp3:okhttp-urlconnection:$versions.okhttp",
        loggingInterceptor        : "com.squareup.okhttp3:logging-interceptor:$versions.okhttp",
        jacksonCore               : "com.fasterxml.jackson.core:jackson-core:$versions.jackson",
        jacksonDatabind           : "com.fasterxml.jackson.core:jackson-databind:$versions.jackson",
        jacksonAnnotations        : "com.fasterxml.jackson.core:jackson-annotations:$versions.jackson",
        jacksonDatatypeJdk8       : "com.fasterxml.jackson.datatype:jackson-datatype-jdk8:$versions.jackson",
        jacksonDatatypeJsr310     : "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$versions.jackson",
        jacksonDataformatCsv      : "com.fasterxml.jackson.dataformat:jackson-dataformat-csv:$versions.jackson",
        jacksonDataFormatCbor     : "com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:$versions.jackson",
        metricsCore               : "io.dropwizard.metrics:metrics-core:$versions.metrics",
        metricsJvm                : "io.dropwizard.metrics:metrics-jvm:$versions.metrics",
        metricsJson               : "io.dropwizard.metrics:metrics-json:$versions.metrics",
        metricsLogback            : "io.dropwizard.metrics:metrics-logback:$versions.metrics",
        metricsHealthchecks       : "io.dropwizard.metrics:metrics-healthchecks:$versions.metrics",
        metricsGraphite           : "io.dropwizard.metrics:metrics-graphite:$versions.metrics",
        undertowCore              : "io.undertow:undertow-core:$versions.undertow",
        slf4j                     : "org.slf4j:slf4j-api:$versions.slf4j",
        slf4jLog4j                : "org.slf4j:log4j-over-slf4j:$versions.slf4j",
        logback                   : "ch.qos.logback:logback-classic:$versions.logback",
        logbackCore               : "ch.qos.logback:logback-core:$versions.logback",
        logbackJson               : "ch.qos.logback.contrib:logback-json-classic:$versions.logbackJson",
        logbackJackson			  : "ch.qos.logback.contrib:logback-jackson:$versions.logbackJson",
        guava                     : "com.google.guava:guava:$versions.guava",
        typesafeConfig            : "com.typesafe:config:$versions.typesafeConfig",
        handlebars                : "com.github.jknack:handlebars:$versions.handlebars",
        handlebarsJackson         : "com.github.jknack:handlebars-jackson2:$versions.handlebars",
        handlebarsMarkdown        : "com.github.jknack:handlebars-markdown:$versions.handlebars",
        handlebarsHumanize        : "com.github.jknack:handlebars-humanize:$versions.handlebars",
        handlebarsHelpers         : "com.github.jknack:handlebars-helpers:$versions.handlebars",
        htmlCompressor            : "com.googlecode.htmlcompressor:htmlcompressor:$versions.htmlCompressor",
        hikaricp                  : "com.zaxxer:HikariCP:$versions.hikaricp",
        jool                      : "org.jooq:jool:$versions.jool",
        hsqldb                    : "org.hsqldb:hsqldb:$versions.hsqldb",
        s3                        : "com.amazonaws:aws-java-sdk-s3:$versions.aws",
        flyway                    : "org.flywaydb:flyway-core:$versions.flyway",
        connectorj                : "mysql:mysql-connector-java:$versions.connectorj",
        jooq                      : "org.jooq:jooq:$versions.jooq",
        jooqCodegen               : "org.jooq:jooq-codegen:$versions.jooq",
        hashids                   : "org.hashids:hashids:$versions.hashids",
        failsafe                  : "net.jodah:failsafe:$versions.failsafe",
        jsoup                     : "org.jsoup:jsoup:$versions.jsoup",
        lombok                    : "org.projectlombok:lombok:$versions.lombok",
        sitemapgen4j              : "com.github.dfabulich:sitemapgen4j:$versions.sitemapgen4j",
        jbcrypt                   : "org.mindrot:jbcrypt:$versions.jbcrypt",
        romeRss                   : "rome:rome:$versions.romeRss",
        kotlin                    : "org.jetbrains.kotlin:kotlin-stdlib:$versions.kotlin",
        javaxAnnotation           : "javax.annotation:javax.annotation-api:$versions.javax",
        jbossLogging              : "org.jboss.logging:jboss-logging:$versions.jbossLogging",
        jbossThreads              : "org.jboss.threads:jboss-threads:$versions.jbossThreads",
        wildflyCommon             : "org.wildfly.common:wildfly-common:$versions.wildflyCommon",
        commonsCodec              : "commons-codec:commons-codec:$versions.commonsCodec",

        junit                     : "junit:junit:$versions.junit",
    ]
}

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.

plugins {
  id "org.sonarqube" version "3.3"
}

// Include a gradle script that has all of our dependencies split out.
apply from: "gradle/dependencies.gradle"

allprojects {
    // Apply the java plugin to add support for Java
    apply plugin: 'java-library'
    apply plugin: 'idea'
    apply plugin: 'eclipse'
    apply plugin: 'maven-publish'

    // Using Jitpack so I need the repo name in the group to match.
    group = 'com.stubbornjava.StubbornJava'
    version = '0.0.0-SNAPSHOT'
    
    sourceCompatibility = 15
    targetCompatibility = 15

    sourceSets {
        main {
            java {
                srcDirs = ["src/main/java", "src/generated/java"]
            }
            resources {
                srcDirs = ["src/main/resources", "ui/assets"]
            }
        }
    }

    repositories {
        mavenLocal()
        mavenCentral()
        maven { url 'https://jitpack.io' } // This allows us to use jitpack projects
    }
    
    task copyRuntimeLibs(type: Copy) {
        into "build/libs"
        from configurations.runtimeClasspath
    }
    
    build.finalizedBy(copyRuntimeLibs)

    configurations.all {
        resolutionStrategy {
            // fail eagerly on version conflict (includes transitive dependencies)
            // e.g. multiple different versions of the same dependency (group and name are equal)
            failOnVersionConflict()

            // Auto force all of our explicit dependencies.
            libs.each { k, v -> force(v) }
            force('commons-logging:commons-logging:1.2')
            force('com.google.code.findbugs:jsr305:3.0.2')

            // cache dynamic versions for 10 minutes
            cacheDynamicVersionsFor 10*60, 'seconds'
            // don't cache changing modules at all
            cacheChangingModulesFor 0, 'seconds'
        }
    }

    // Maven Publish Begin
    task sourceJar(type: Jar) {
        from sourceSets.main.allJava
    }

    // This publishes sources with our jars.
    publishing {
        publications {
            mavenJava(MavenPublication) {
                from components.java
                artifact sourceJar {
                    classifier "sources"
                }
            }
        }
    }
    
    sonarqube {
        properties {
            property "sonar.projectKey", "StubbornJava_StubbornJava"
            property "sonar.organization", "stubbornjava"
            property "sonar.host.url", "https://sonarcloud.io"
            property "sonar.exclusions", "**/src/generated/java/**/*.java"
        }
    }
    // Maven Publish End
}

stubbornjava-undertow/build.gradle

This project is for StubbornJava specific undertow helper classes. We only need to reference the libs.{library name} because we stored all the dependency strings in the ext tag in the parent project.

dependencies {
    api libs.undertowCore
    api libs.slf4j
    api libs.logback
    api libs.jbossLogging
    
    testImplementation libs.junit
}

stubbornjava-common/build.gradle

This project is for StubbornJava specific common code. Notice stubbornjava-undertow is a dependency.

dependencies {
    // Project reference
    api project(':stubbornjava-undertow')
    api libs.slf4j
    api libs.logback
    api libs.logbackJson
    api libs.logbackJackson
    api libs.jacksonCore
    api libs.jacksonDatabind
    api libs.jacksonDatabind
    api libs.jacksonAnnotations
    api libs.jacksonDatatypeJdk8
    api libs.jacksonDatatypeJsr310
    api libs.jacksonDataformatCsv
    api libs.jacksonDataFormatCbor
    api libs.metricsCore
    api libs.metricsJvm
    api libs.metricsJson
    api libs.metricsLogback
    api libs.metricsHealthchecks
    api libs.metricsGraphite
    api libs.guava
    api libs.typesafeConfig
    api libs.handlebars
    api libs.handlebarsJackson
    api libs.handlebarsMarkdown
    api libs.handlebarsHelpers
    api libs.handlebarsHumanize
    api libs.htmlCompressor
    api libs.hikaricp
    api libs.jool
    api libs.okhttp
    api libs.okhttpUrlConnection
    api libs.loggingInterceptor
    api libs.s3
    api libs.failsafe
    api libs.jsoup
    api libs.sitemapgen4j
    api libs.jbcrypt
    api libs.jooq
    api libs.jooqCodegen
    api libs.flyway
    api libs.connectorj
    api libs.javaxAnnotation
    api libs.commonsCodec
    api libs.kotlin

    compileOnly libs.lombok
    annotationProcessor libs.lombok

    testImplementation libs.junit
    testImplementation libs.hsqldb
}

stubbornjava-examples/build.gradle

This project is for StubbornJava specific examples.

dependencies {
    implementation project(':stubbornjava-undertow')
    implementation project(':stubbornjava-common')
    implementation libs.hsqldb
    implementation libs.hashids
    testImplementation libs.junit
}

Building a Fat JAR with Shadow

Now that we have a working multi-project build lets create an executable JAR. For our example embedded REST service. (Assume we are in the root gradle directory)

gradle shadowJar
Configuration on demand is an incubating feature.
:stubbornjava-undertow:compileJava UP-TO-DATE
:stubbornjava-undertow:processResources UP-TO-DATE
:stubbornjava-undertow:classes UP-TO-DATE
:stubbornjava-undertow:jar
:stubbornjava-common:compileJava
:stubbornjava-common:processResources UP-TO-DATE
:stubbornjava-common:classes
:stubbornjava-common:shadowJar
:stubbornjava-common:jar
:stubbornjava-examples:compileJava
:stubbornjava-examples:processResources UP-TO-DATE
:stubbornjava-examples:classes
:stubbornjava-examples:shadowJar
:stubbornjava-undertow:shadowJar

BUILD SUCCESSFUL

Total time: 6.638 secs

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.

java -Denv=local -Xmx640m -cp 'stubbornjava-examples/build/libs/stubbornjava-examples-0.1.2-SNAHOT.jar' com.stubbornjava.examples.undertow.rest.RestServer
2017-02-20 15:37:54.760 [main] DEBUG c.s.common.undertow.SimpleServer - ListenerInfo{protcol='http', address=/0:0:0:0:0:0:0:0:8080, sslContext=null}
curl -X POST "localhost:8080/users" -d '
{
  "email": "user1@test.com",
  "roles": ["USER"]
}
';
{"email":"user1@test.com","roles":["USER"],"dateCreated":"2017-01-16"}

curl -X POST "localhost:8080/users" -d '
{
  "email": "user2@test.com",
  "roles": ["ADMIN"]
}
';
{"email":"user2@test.com","roles":["ADMIN"],"dateCreated":"2017-01-16"}