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"}