Building Modular Java Applications with Gradle

July2019

Table of Contents

As a programming language Java has long been infamous for enforcing a rigid structure in class relationships, and in Java 9 it got even worse with a new feature for structuring relationships between larger blocks of code: modules

Modules are great. They encourage a design that breaks down a large system into cohesive units that facillitate code reuse by encapsulating a business domain. After doing some serious work with Java modules I've found the resulting codebase to have among the most clear, coherent, and stable designs I've worked with. When you build a system with modules the overall structure of the codebase and the relationships in it are at the top of your mind, and that results in a design with strong, independent units that facillitate code reuse.

Getting there is not easy though. Gradle has some support for building modules, but there are a number of serious problems you will run into in the course of setting up your build that are not covered in the documentation, nor in easily googlable blogs and tutorials. You'll end up cobbling together some knowledge of how modules work from various different sources, experimenting with command line compilation options, and (shudder) building your own jars from dependencies that are incompatible with the module system due to clashing packages.

I aim for this post to be the guide I wish I had when I was setting up my first modular system. We'll be building a RESTful web service to manage authentication tokens using dropwizard, mysql, and hibernate. The system will be composed of three modules, as dropwizard recommands.  This will give us the opportunity to create a system with non-trivial dependencies and internal module relationships.

The service will generate authentication tokens given a username and password, and determine if tokens are valid. We'll store a database of username and encrypted password pairs, and keep authentication tokens in an in-memory cache.

All of the example code in this blog is in a github repo, and each step will link to corresponding commits in the repo.


First Steps

The first module we're going to build will be a module that contains json models for interacting with our authentication service. We need two models - AuthenticationRequest with a user name and password to request an authentication token, andAuthenticationToken to wrap an authentication in a json model.

We're going to use a multi project build to build each of the modules within one main project, so the top-level build.gradle will just configure some common options for the subprojects:

plugins {
id 'java'
id 'com.zyxist.chainsaw' version '0.3.1' apply false
}


subprojects {
apply plugin: 'java'
apply plugin: 'com.zyxist.chainsaw'

repositories {
mavenCentral()
}

dependencies {
testCompile 'junit:junit:4.12'
}
}

Note our use of the plugin com.zyxist.chainsaw - this plugin configures gradle support for java modules. It's modeled after gradle's experimental-jigsaw plugin, but has more active support.

The models module we're setting up right now just needs to contain json models for both the service and the client to use. We're going to need jackson as a dependency, and we also want to include guava  for basic utilities. Here's our first cut at a build.gradle file for this module:

plugins {
id 'java-library'
}

dependencies {
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.9'
implementation group: 'com.google.guava', name: 'guava', version: '23.5-jre'
}

And here are the json models:

package com.alexkudlick.authentication.models;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.base.Preconditions;
import java.util.Objects;

public class AuthenticationRequest {

@JsonProperty("userName")
private String userName;

@JsonProperty("password")
private String password;

private AuthenticationRequest() {

}

public AuthenticationRequest(String userName, String password) {
Preconditions.checkArgument(userName != null && !userName.isEmpty());
Preconditions.checkArgument(password != null && !password.isEmpty());
this.userName = userName;
this.password = password;
}

public String getUserName() {
return userName;
}

public String getPassword() {
return password;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof AuthenticationRequest)) return false;
AuthenticationRequest that = (AuthenticationRequest) o;
return Objects.equals(userName, that.userName) &&
Objects.equals(password, that.password);
}

@Override
public int hashCode() {
return Objects.hash(userName, password);
}

@Override
public String toString() {
return "AuthenticationRequest{" +
"userName='" + userName + ''' +
"password='" + "*".repeat(password.length()) + ''' +
"}";
}
}
package com.alexkudlick.authentication.models;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.base.Preconditions;
import java.util.Objects;

public class AuthenticationToken {

public static final int MIN_LENGTH = 8;

@JsonProperty("token")
private String token;

private AuthenticationToken() {

}

public AuthenticationToken(String token) {
Preconditions.checkArgument(token != null && token.length() >= MIN_LENGTH);
this.token = token;
}

public String getToken() {
return token;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof AuthenticationToken)) return false;
AuthenticationToken that = (AuthenticationToken) o;
return Objects.equals(token, that.token);
}

@Override
public int hashCode() {
return Objects.hash(token);
}

@Override
public String toString() {
return "AuthenticationToken{" +
"token='" + "*".repeat(token.length() - 4) + token.substring(token.length() - 4, token.length()) + ''' +
"}";
}

}

At this point we are at this commit  and can try to build the module. Running ./gradlew clean build will output the following error:

FAILURE: Build failed with an exception.

* What went wrong:
A problem occurred configuring project ':authentication.models'.
> The project is lacking a Java module descriptor in
'/home/alex/workspace/modular-java-example/authentication.models/src/main/java' directory.

That's the com.zyxist.chainsaw telling us that we need to add a module-info.java file. Great! That's what we're here for. Let's add a basic module-info.java file:

module com.alexkudlick.authentication.models {
exports com.alexkudlick.authentication.models;
}

This build will still fail, but it will at least tell us the modules we need to require:

...

(package com.fasterxml.jackson.annotation is declared in module jackson.annotations,
but module com.alexkudlick.authentication.models does not read it)
...

(package com.google.common.base is declared in module com.google.common,
but module com.alexkudlick.authentication.models does not read it)
...

,

That's clear enough. Let's add those to our module-info.java file:

module com.alexkudlick.authentication.models {
requires jackson.annotations;
requires com.google.common;

exports com.alexkudlick.authentication.models;
}

And with that we get our first working build! We're still going to need to make more changes to this module-info.java, but this is a start.


Serialization and Deserialization

We should write tests to verify that our models can be serialized and deserialized to and from json strings. I wouldn't always do this, but since we're just getting the hang of building with modules, this is a good sanity check.

package com.alexkudlick.authentication.models;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Test;

import java.io.IOException;

import static org.junit.Assert.assertEquals;

public class AuthenticationRequestTest {

@Test
public void testDeserialize() throws IOException {
String json = "{" +
"\"userName\":\"testUserName\"," +
"\"password\":\"testPassword\"" +
"}";
AuthenticationRequest deserialized = new ObjectMapper().readValue(
json, AuthenticationRequest.class
);
assertEquals(
new AuthenticationRequest("testUserName", "testPassword"),
deserialized
);
}

}

Running ./gradlew clean build now gives us the following error:

com.alexkudlick.authentication.models.AuthenticationRequestTest > testDeserialize FAILED
java.lang.reflect.InaccessibleObjectException at AuthenticationRequestTest.java:18

1 test completed, 1 failed

FAILURE: Build failed with an exception.
,

The exception message indicates the problem:

java.lang.reflect.InaccessibleObjectException: Unable to make private 
com.alexkudlick.authentication.models.AuthenticationRequest() accessible:
module com.alexkudlick.authentication.models does not
"opens com.alexkudlick.authentication.models" to module com.fasterxml.jackson.databind

There important bit here is module com.alexkudlick.authentication.models does not "opens com.alexkudlick.authentication.models" to module com.fasterxml.jackson.databind.  Jackson requires reflective access to classes in order to serialize and deserialize them. With modules, we have to explicitly allow reflective access to classes using open/opens There are a few options. We can open specific packages to the world, we can open specific packages to specific modules, or we can open the whole module. Let's try opening the whole module first:

open module com.alexkudlick.authentication.models {
requires jackson.annotations;
requires com.google.common;

exports com.alexkudlick.authentication.models;
}

With this change, the deserialization test passes. Later on, we'll have an interesting decision to make about whether to open up a whole module or just open packages, but it makes sense to open the whole module here because this module is just for json models. All the classes in it are meant to be open. We'll finish up the tests and move on.


Setting up the Application Module

Now let's build the module containing our RESTful web service. To start with we'll setup inter-project dependencies. We know we're going to need our authentication-models module, so let's add that and see if the build goes through:

dependencies {
compile project(':authentication-models')
}
module com.alexkudlick.authentication.application {
requires com.alexkudlick.authentication.models;
}
./gradlew clean build

> Task :authentication-application:compileJava FAILED
/home/alex/workspace/modular-java-example/authentication-application/src/main/java/module-info.java:2:
error: module not found: com.alexkudlick.authentication.models
requires com.alexkudlick.authentication.models;
^
1 error

FAILURE: Build failed with an exception.

What? How does this module not exist? It's supplied by the authentication-models project, and we put a dependency in our build.gradle file, so it should be present, right?

Well, it turns out that gradle is not very smart about the order in which it builds projects. Event with the project dependency, it doesn't build authentication-models first. It just builds the projects in alphabetical order, no matter what their relationships are. The solution I've used is to create a build script to build the projects in the order in which they appear in settings.gradle:

#!/usr/bin/env bash

PROJECTS=$(cat settings.gradle | grep include | awk '{print $2}' | tr -d "'")
BUILD_COMMANDS=$(echo $PROJECTS | sed -e "s/ /:build /g" -e "s/$/:build/")
echo "./gradlew clean $BUILD_COMMANDS"
./gradlew clean $BUILD_COMMANDS

Running this gives us that sweet . From now on we'll be using ./build.sh to run the build, not ./gradlew clean build.


Ok, we've gotten that out of the way. We can build both modules. Let's try actually fleshing out the dropwizard application. First we need to add dropwizard as a dependency,  and then we can try running the build:

./build.sh

./gradlew clean authentication-models:build authentication-application:build

> Task :authentication-application:compileJava FAILED

error: the unnamed module reads package javax.annotation from both jsr305 and javax.annotation.api
error: module dropwizard.jersey reads package javax.annotation from both jsr305 and javax.annotation.api
error: module dropwizard.request.logging reads package javax.annotation from both jsr305 and javax.annotation.api
error: module dropwizard.jetty reads package javax.annotation from both jsr305 and javax.annotation.api
error: module dropwizard.logging reads package javax.annotation from both jsr305 and javax.annotation.api
error: module dropwizard.metrics reads package javax.annotation from both jsr305 and javax.annotation.api
error: module dropwizard.jackson reads package javax.annotation from both jsr305 and javax.annotation.api
error: module dropwizard.servlets reads package javax.annotation from both jsr305 and javax.annotation.api
error: module dropwizard.lifecycle reads package javax.annotation from both jsr305 and javax.annotation.api
error: module jul.to.slf4j reads package javax.annotation from both jsr305 and javax.annotation.api
error: module log4j.over.slf4j reads package javax.annotation from both jsr305 and javax.annotation.api
error: module jcl.over.slf4j reads package javax.annotation from both jsr305 and javax.annotation.api
error: module metrics.jersey2 reads package javax.annotation from both jsr305 and javax.annotation.api
error: module metrics.annotation reads package javax.annotation from both jsr305 and javax.annotation.api
error: module dropwizard.validation reads package javax.annotation from both jsr305 and javax.annotation.api
error: module dropwizard.util reads package javax.annotation from both jsr305 and javax.annotation.api
error: module metrics.servlets reads package javax.annotation from both jsr305 and javax.annotation.api
error: module metrics.jvm reads package javax.annotation from both jsr305 and javax.annotation.api
error: module metrics.logback reads package javax.annotation from both jsr305 and javax.annotation.api
error: module metrics.jetty9 reads package javax.annotation from both jsr305 and javax.annotation.api
error: module metrics.json reads package javax.annotation from both jsr305 and javax.annotation.api
error: module metrics.core reads package javax.annotation from both jsr305 and javax.annotation.api
error: module metrics.healthchecks reads package javax.annotation from both jsr305 and javax.annotation.api
error: module argparse4j reads package javax.annotation from both jsr305 and javax.annotation.api
error: module jetty.setuid.java reads package javax.annotation from both jsr305 and javax.annotation.api
error: module com.fasterxml.jackson.datatype.guava reads package javax.annotation from both jsr305 and javax.annotation.api
error: module com.fasterxml.jackson.datatype.jsr310 reads package javax.annotation from both jsr305 and javax.annotation.api
error: module com.fasterxml.jackson.datatype.jdk8 reads package javax.annotation from both jsr305 and javax.annotation.api
error: module com.fasterxml.jackson.module.paramnames reads package javax.annotation from both jsr305 and javax.annotation.api
error: module com.fasterxml.jackson.module.afterburner reads package javax.annotation from both jsr305 and javax.annotation.api
error: module com.fasterxml.jackson.datatype.joda reads package javax.annotation from both jsr305 and javax.annotation.api
error: module com.fasterxml.jackson.jaxrs.json reads package javax.annotation from both jsr305 and javax.annotation.api
error: module jackson.jaxrs.base reads package javax.annotation from both jsr305 and javax.annotation.api
error: module com.fasterxml.jackson.module.jaxb reads package javax.annotation from both jsr305 and javax.annotation.api
error: module com.fasterxml.jackson.databind reads package javax.annotation from both jsr305 and javax.annotation.api
error: module jackson.annotations reads package javax.annotation from both jsr305 and javax.annotation.api
error: module com.google.common reads package javax.annotation from both jsr305 and javax.annotation.api
error: module jsr305 reads package javax.annotation from both jsr305 and javax.annotation.api
error: module profiler reads package javax.annotation from both jsr305 and javax.annotation.api
error: module joda.time reads package javax.annotation from both jsr305 and javax.annotation.api
error: module com.fasterxml.jackson.dataformat.yaml reads package javax.annotation from both jsr305 and javax.annotation.api
error: module com.fasterxml.jackson.core reads package javax.annotation from both jsr305 and javax.annotation.api
error: module jersey.bean.validation reads package javax.annotation from both jsr305 and javax.annotation.api
error: module hibernate.validator reads package javax.annotation from both jsr305 and javax.annotation.api
error: module javax.el reads package javax.annotation from both jsr305 and javax.annotation.api
error: module org.apache.commons.lang3 reads package javax.annotation from both jsr305 and javax.annotation.api
error: module logback.classic reads package javax.annotation from both jsr305 and javax.annotation.api
error: module logback.access reads package javax.annotation from both jsr305 and javax.annotation.api
error: module logback.core reads package javax.annotation from both jsr305 and javax.annotation.api
error: module jetty.servlets reads package javax.annotation from both jsr305 and javax.annotation.api
error: module jetty.webapp reads package javax.annotation from both jsr305 and javax.annotation.api
error: module jetty.servlet reads package javax.annotation from both jsr305 and javax.annotation.api
error: module jetty.security reads package javax.annotation from both jsr305 and javax.annotation.api
error: module jetty.server reads package javax.annotation from both jsr305 and javax.annotation.api
error: module jetty.http reads package javax.annotation from both jsr305 and javax.annotation.api
error: module jetty.io reads package javax.annotation from both jsr305 and javax.annotation.api
error: module jetty.xml reads package javax.annotation from both jsr305 and javax.annotation.api
error: module jetty.util reads package javax.annotation from both jsr305 and javax.annotation.api
error: module jersey.container.servlet reads package javax.annotation from both jsr305 and javax.annotation.api
error: module jersey.container.servlet.core reads package javax.annotation from both jsr305 and javax.annotation.api
error: module jersey.server reads package javax.annotation from both jsr305 and javax.annotation.api
error: module jersey.metainf.services reads package javax.annotation from both jsr305 and javax.annotation.api
error: module jetty.continuation reads package javax.annotation from both jsr305 and javax.annotation.api
error: module checker.qual reads package javax.annotation from both jsr305 and javax.annotation.api
error: module error.prone.annotations reads package javax.annotation from both jsr305 and javax.annotation.api
error: module j2objc.annotations reads package javax.annotation from both jsr305 and javax.annotation.api
error: module animal.sniffer.annotations reads package javax.annotation from both jsr305 and javax.annotation.api
error: module validation.api reads package javax.annotation from both jsr305 and javax.annotation.api
error: module jboss.logging reads package javax.annotation from both jsr305 and javax.annotation.api
error: module classmate reads package javax.annotation from both jsr305 and javax.annotation.api
error: module snakeyaml reads package javax.annotation from both jsr305 and javax.annotation.api
error: module jersey.client reads package javax.annotation from both jsr305 and javax.annotation.api
error: module jersey.media.jaxb reads package javax.annotation from both jsr305 and javax.annotation.api
error: module jersey.common reads package javax.annotation from both jsr305 and javax.annotation.api
error: module javax.ws.rs.api reads package javax.annotation from both jsr305 and javax.annotation.api
error: module javax.annotation.api reads package javax.annotation from both jsr305 and javax.annotation.api
error: module hk2.locator reads package javax.annotation from both jsr305 and javax.annotation.api
error: module hk2.api reads package javax.annotation from both jsr305 and javax.annotation.api
error: module javax.inject reads package javax.annotation from both jsr305 and javax.annotation.api
error: module javax.servlet.api reads package javax.annotation from both jsr305 and javax.annotation.api
error: module jersey.guava reads package javax.annotation from both jsr305 and javax.annotation.api
error: module osgi.resource.locator reads package javax.annotation from both jsr305 and javax.annotation.api
error: module hk2.utils reads package javax.annotation from both jsr305 and javax.annotation.api
error: module aopalliance.repackaged reads package javax.annotation from both jsr305 and javax.annotation.api
error: module javassist reads package javax.annotation from both jsr305 and javax.annotation.api
error: module dropwizard.configuration reads package javax.annotation from both jsr305 and javax.annotation.api
error: module dropwizard.core reads package javax.annotation from both jsr305 and javax.annotation.api
/home/alex/workspace/modular-java-example/authentication-application/src/main/java/module-info.java:1: error: module com.alexkudlick.authentication.application reads package javax.annotation from both jsr305 and javax.annotation.api
module com.alexkudlick.authentication.application {
^
89 errors

FAILURE: Build failed with an exception.

How did such a small change lead to so many errors?


Debugging Split Packages

We've encountered our first real problem with the module system — Split Pacakges.  The java module system doesn't allow the same package to be used in multiple modules. So, if there's a module A with classes in com.myexample, there can't be any other module on the classpath (technically, the module path) with classes in com.myexample. Note that this only refers to packages with classes in them, not parent level packages. In our case, we're going to be fine with modules that have classes in com.alexkudlick.authentication.models andcom.alexkudlick.authentication.application.

But we aren't fine with split packages in our dependencies. Notice that all the errors say the same thing:

module X reads package javax.annotation from both jsr305 and javax.annotation.api

This tells us what to look for. But where do we look?


The gradle dependencies command is a very important tool in the module builder's toolbox. This command will print out a tree of all of a project's dependencies, which will be critical in debugging split packages. Here we'll run ./gradlew authentication-application:dependencies and search for the jsr305 and javax.annotation.api (noting that java creates automatic module names by translating non-alphanumeric characters to dots). We'll see that we do in fact include dependencies that provide those two modules:

+--- project :authentication-models
...
|
| --- com.google.guava:guava:23.5-jre
+--- ...
| +--- com.google.code.findbugs:jsr305:1.3.9 -> 3.0.2
|
--- io.dropwizard:dropwizard-core:1.2.9
+--- io.dropwizard:dropwizard-util:1.2.9
| +--- ...
| +--- com.google.code.findbugs:jsr305:3.0.2
+--- io.dropwizard:dropwizard-jersey:1.2.9
| +--- ...
| +--- org.glassfish.jersey.core:jersey-server:2.25.1
| | +--- org.glassfish.jersey.core:jersey-common:2.25.1
| | | +--- ...
| | | +--- javax.annotation:javax.annotation-api:1.2
,

So we're getting jsr305 from both guava and dropwizard-util, and we're getting javax.annotation-api from dropwizard-jersey. Why is this a problem?

To figure that out, it will help to take a look at the classes in both these jars. Our suspicion should be that they both contain classes in the javax.annotation package, because that was the package in the warning.

Running ./gradlew authentication-application:printJars will show us the location of the jsr305 and javax.annotation-api jars:

/home/alex/.gradle/caches/modules-2/files-2.1/com.google.code.findbugs/jsr305/3.0.2/25ea2e8b0c338a877313bd4672d3fe056ea78f0d/jsr305-3.0.2.jar
/home/alex/.gradle/caches/modules-2/files-2.1/javax.annotation/javax.annotation-api/1.2/479c1e06db31c432330183f5cae684163f186146/javax.annotation-api-1.2.jar

Using jar tvf and some bash fu, we can see that both these jars do indeed contain classes in the javax.annotation package:

jar tvf /home/alex/.gradle/caches/modules-2/files-2.1/javax.annotation/javax.annotation-api/1.2/479c1e06db31c432330183f5cae684163f186146/javax.annotation-api-1.2.jar | grep "javax/annotation/[^/]*.class" | awk '{print $8}'
javax/annotation/Generated.class
javax/annotation/ManagedBean.class
javax/annotation/PostConstruct.class
javax/annotation/PreDestroy.class
javax/annotation/Priority.class
javax/annotation/Resource$AuthenticationType.class
javax/annotation/Resource.class
javax/annotation/Resources.class
jar tvf /home/alex/.gradle/caches/modules-2/files-2.1/com.google.code.findbugs/jsr305/3.0.2/25ea2e8b0c338a877313bd4672d3fe056ea78f0d/jsr305-3.0.2.jar | grep "javax/annotation/[^/]*.class" | awk '{print $8}'
javax/annotation/CheckForNull.class
javax/annotation/CheckForSigned.class
javax/annotation/CheckReturnValue.class
javax/annotation/Detainted.class
javax/annotation/MatchesPattern$Checker.class
javax/annotation/MatchesPattern.class
javax/annotation/Nonnegative$Checker.class
javax/annotation/Nonnegative.class
javax/annotation/Nonnull$Checker.class
javax/annotation/Nonnull.class
javax/annotation/Nullable.class
javax/annotation/OverridingMethodsMustInvokeSuper.class
javax/annotation/ParametersAreNonnullByDefault.class
javax/annotation/ParametersAreNullableByDefault.class
javax/annotation/PropertyKey.class
javax/annotation/RegEx$Checker.class
javax/annotation/RegEx.class
javax/annotation/Signed.class
javax/annotation/Syntax.class
javax/annotation/Tainted.class
javax/annotation/Untainted.class
javax/annotation/WillClose.class
javax/annotation/WillCloseWhenClosed.class
javax/annotation/WillNotClose.class

That means that these two jars are incompatible with a modular build - we can't have a build with both of them on the module path. We need to update our build configuration so that these are not both in our dependency tree.

But which jar should we choose to keep on the module path? How do we know which we classes we will need? Since these jars provde disjoint sets of classes, we might need all of the classes on our classpath. What would we do then?

In that case, I would use a techinque I call Exclude & Include. We'll talk about that later; for now, it turns out that we don't actually need the classes from the jsr305 module to build and run a dropwizard application, so we can just exclude that dependency:

implementation(group: 'com.google.guava', name: 'guava', version: '23.5-jre') {
exclude group: 'com.google.code.findbugs', module: 'jsr305'
}
compile(group: 'io.dropwizard', name: 'dropwizard-core', version: '1.2.9') {
exclude group: 'com.google.code.findbugs', module: 'jsr305'
}

And now we get a

Whew! we had to fix a few significant problems just to get to the point where we can include dropwizard as a dependency. My experience with modular builds is that adding large dependencies like dropwizard requires quite a bit of work to resolve split packages. We actually got off easy this time; when we get to the point where we need to add dropwizard-hibernate as a dependency, we're going to have significantly harder problems.


Token Validation

We have all the dependencies we need to add a basic endpoint to validate that tokens are valid. This endpoint will just check if a token is in an in-memory cache, returning 200 OK  if it is, and 404 NOT FOUND if it is not. We'll add the ability to generate tokens later. For now, here is the meat of our logic:

@Path("/api/tokens/")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class AuthenticationTokenResource {

private final AuthenticationTokenManager manager;

public AuthenticationTokenResource(AuthenticationTokenManager manager) {
this.manager = Objects.requireNonNull(manager);
}

@GET
@Path("{token}/")
public Response checkTokenValidity(@PathParam("token") String token) {
if (manager.isValid(token)) {
return Response.ok().build();
} else {
return Response.status(Response.Status.NOT_FOUND).build();
}
}
}
public class AuthenticationTokenResourceTest {

private AuthenticationTokenManager manager;
private AuthenticationTokenResource resource;

@Before
public void setUp() {
manager = mock(AuthenticationTokenManager.class);
resource = new AuthenticationTokenResource(manager);
}

@Test
public void testCheckTokenValidityReturnsOkIfTokenIsValid() {
when(manager.isValid(anyString())).thenReturn(true);

Response response = resource.checkTokenValidity("21l2kjh253424");

assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());

verify(manager).isValid("21l2kjh253424");
verifyNoMoreInteractions(manager);
}

@Test
public void testCheckTokenValidityReturnsNotFoundIfTokenIsNotValid() {
when(manager.isValid(anyString())).thenReturn(false);

Response response = resource.checkTokenValidity("lky4hq32kk");

assertEquals(Response.Status.NOT_FOUND.getStatusCode(), response.getStatus());

verify(manager).isValid("lky4hq32kk");
verifyNoMoreInteractions(manager);
}
}
public class AuthenticationTokenManager {

private final Cache<String, String> tokenCache;

public AuthenticationTokenManager(Cache<String, String> tokenCache) {
this.tokenCache = Objects.requireNonNull(tokenCache);
}

public boolean isValid(String token) {
return tokenCache.getIfPresent(token) != null;
}
}
public class AuthenticationTokenManagerTest {

private LoadingCache<String, String> cache;
private AuthenticationTokenManager manager;

@Before
public void setUp() {
cache = mock(LoadingCache.class);
manager = new AuthenticationTokenManager(cache);
}

@Test
public void testTokenIsValidIfInCache() {
when(cache.getIfPresent(anyString())).thenReturn("asdfasdfasdf");

assertTrue(manager.isValid("asdfasdfasdf"));

verify(cache).getIfPresent("asdfasdfasdf");
verifyNoMoreInteractions(cache);
}

@Test
public void testTokenIsInValidIfNotInCache() {
when(cache.getIfPresent(anyString())).thenReturn(null);

assertFalse(manager.isValid("asdfasdfasdf"));

verify(cache).getIfPresent("asdfasdfasdf");
verifyNoMoreInteractions(cache);
}

}
,

Unfortunately, our tests don't pass:

com.alexkudlick.authentication.application.tokens.AuthenticationTokenManagerTest > testTokenIsValidIfInCache FAILED
java.lang.IllegalAccessException

com.alexkudlick.authentication.application.tokens.AuthenticationTokenManagerTest > testTokenIsInValidIfNotInCache FAILED
java.lang.IllegalAccessException

com.alexkudlick.authentication.application.web.AuthenticationTokenResourceTest > testCheckTokenValidityReturnsNotFoundIfTokenIsNotValid FAILED
java.lang.IllegalAccessException

com.alexkudlick.authentication.application.web.AuthenticationTokenResourceTest > testCheckTokenValidityReturnsOkIfTokenIsValid FAILED
java.lang.IllegalAccessException

4 tests completed, 4 failed

FAILURE: Build failed with an exception.

The exception message tells us what the problem is:

java.lang.IllegalAccessException: class org.junit.runners.BlockJUnit4ClassRunner (in module junit) cannot access class com.alexkudlick.authentication.application.tokens.AuthenticationTokenManagerTest (in module com.alexkudlick.authentication.application) because module com.alexkudlick.authentication.application does not export com.alexkudlick.authentication.application.tokens to module junit

Junit can't access our test class to instantiate it reflectively and run it. This sounds a lot like our problem opening classes to jackson, but we wouldn't want to modify our module-info.java file to solve this problem, because module-info.javais meant to be a descriptor for your main code artifact, whereas unit tests live alongside the main code but aren't part of it.

The module system has facilities for addressing this case. There are command line flags  that can be added to java commands that alter the module system at runtime. What we want to do here is --add-opens —  effectively, we want to add --add-opens com.alexkudlick.authentication.application/com.alexkudlick.authentication.application.tokens=junit to our java command line when we run the tests.

The chainsaw plugin provides the ability to add command line flags  using the javaModule.hacks. The plugin automatically adds these flags for packages that your module exports, but this module is an application and won't export anything. We'll just add those opens statements:

javaModule.hacks {
opens(
'com.alexkudlick.authentication.application',
'com.alexkudlick.authentication.application.tokens',
'junit'
)
opens(
'com.alexkudlick.authentication.application',
'com.alexkudlick.authentication.application.web',
'junit'
)
}

Now junit can access our test classes, but we have another problem — mockito can't mock our AuthenticationTokenManager class.

Mockito cannot mock this class:
class com.alexkudlick.authentication.application.tokens.AuthenticationTokenManager.

This is actually the same problem as with junit; mockito doesn't have access to our package, so it fails to create a mock. We just need to open that package to mockito to get that sweet gravy.


Running the Application

Dropwizard recommends bundling your application and its dependencies into an uberjar, so that's what we'll do here, using the shadowjar plugin.

When we run java -jar authentication-application/build/libs/authentication-application.jar server /authentication_application.yml,  we get a whole bunch of errors about missing classes:

java.lang.RuntimeException: java.util.concurrent.ExecutionException: 
java.lang.NoClassDefFoundError: javax/xml/bind/Unmarshaller

Understanding this problem requires a bit of history. There were some Java EE packages that used to be bundled into the the jdk, but were removed in JDK11. Amongst these is javax.xml.bind, aka JAXB. There are a lot of classes out there that reference classes from the javax.xml.bind package but their maven artifacts don't include anything like that as a dependency, because it was just part of the jvm when that code was written.

So we're going to have to find a way to get those classes into our classpath. We can add a  dependency from maven:

runtime group: 'javax.xml.bind', name: 'jaxb-api', version: '2.3.1'
,

Now, not only do we get , but we also get the application to start up:

java -jar authentication-application/build/libs/authentication-application.jar server authentication_application.yml

...

INFO [2019-06-06 21:56:44,608] io.dropwizard.jersey.DropwizardResourceConfig: The following paths were found for the configured resources:

GET /api/tokens/{token}/ (com.alexkudlick.authentication.application.web.AuthenticationTokenResource)

INFO [2019-06-06 21:56:44,609] org.eclipse.jetty.server.handler.ContextHandler: Started i.d.j.MutableServletContextHandler@216f01{/,null,AVAILABLE}
INFO [2019-06-06 21:56:44,612] io.dropwizard.setup.AdminEnvironment: tasks =

POST /tasks/log-level (io.dropwizard.servlets.tasks.LogConfigurationTask)
POST /tasks/gc (io.dropwizard.servlets.tasks.GarbageCollectionTask)

Using a Module-Compliant Runtime

Above, we ran the application through the bundled up shadow jar, which was configured as a build artifact in build.gradle:

shadowJar {
mergeServiceFiles()
baseName = 'authentication-application'
classifier = null
version = null
}

jar {
manifest {
attributes 'Main-Class': 'com.alexkudlick.authentication.application.AuthenticationApplication'
}
}

build.finalizedBy(shadowJar)

That's the practice recommended by dropwizardand it makes sense for deployed environments, but, since all our classes and our dependencies' classes are just bundled into the same jar, there's no way we could enforce modularity at runtime using--module-path. If we wanted to run our application in a debugger, say, with the following application configuration in IntelliJ:

then we would find that our application has some holes that make it not actually compliant with the module system at runtime:

Caused by: java.lang.ClassNotFoundException: java.sql.Date
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:583)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521)

The application requires the built-in java.sql module at runtime - the yaml parser used by dropwizard references java.sql.Date - but the codebase doesn't declare that fact. We can add that requires to our module-info.java.

Next we would find that the application can't load resources from the classpath:

io.dropwizard.configuration.ConfigurationParsingException: authentication_application.yml has an error:
* Configuration at authentication_application.yml must not be empty

This makes sense, and is actually not something that I would want to attempt to solve within module-info.java because it's specific to the way in which we're running the application. Since we're using intellij, we'll want to load resources straight out of our src/main/resources folder. We don't want to bake assumptions into our application about the structure of it's runtime classpath, so we should solve this by using --patch-module at runtime:

Now we stumble onto another genuine hole in our module config:

Unable to make public com.alexkudlick.authentication.application.config.AuthenticationConfiguration() accessible:
module com.alexkudlick.authentication.application does not
"exports com.alexkudlick.authentication.application.config" to module com.fasterxml.jackson.databind

Dropwizard can't deserialize our configuration because we didn't open the package to jackson. Once we add that to our module-info.java, the application starts successfully. Making a quick check with curl reveals a problem, though:

curl -i http://localhost:8080/api/tokens/asdf/
HTTP/1.1 500 Request failed.
Date: Thu, 11 Jul 2019 23:48:37 GMT
Cache-Control: must-revalidate,no-cache,no-store
Content-Type: text/html;charset=iso-8859-1
Content-Length: 260

<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<title>Error 500 Request failed.</title>
</head>
<body><h2>HTTP ERROR 500</h2>
<p>Problem accessing /api/tokens/asdf/. Reason:
<pre> Request failed.</pre></p>
</body>
</html>
java.lang.IllegalAccessException: class org.glassfish.jersey.server.model.internal.ResourceMethodInvocationHandlerFactory$1 (in module jersey.server) cannot access class com.alexkudlick.authentication.application.web.AuthenticationTokenResource (in module com.alexkudlick.authentication.application)
because module com.alexkudlick.authentication.application does not export com.alexkudlick.authentication.application.web to module jersey.server

Jersey needs runtime reflective access to Resource classes in order to instantiate them and call their methods. We need to open that package to jersey.server.


Adding Hibernate

So now we have the ability to check if a token is valid, but no ability to create one. We'll add that in this section.

We're going to keep a database of username, encrypted password pairs, and allow clients to create an authentication token by POSTing with a username and password. We'll also expose an endpoint to create users.

We need a few dependencies in order to accomplish this task. We'll use some more dropwizard modules - dropwizard-hibernate  to integrate with Hibernate, dropwizard-migrations to manage database migrations, and dropwizard-testing to make database testing easier. We'll also add a spring crypto dependency to encrypt passwords, a runtime dependency on the mysql jdbc driver, and a test runtime dependency on h2 to create test dbs. Let's go ahead and add those to build.gradle and run the build:

compile(group: 'io.dropwizard', name: 'dropwizard-hibernate', version: '1.2.9') {
exclude group: 'com.google.code.findbugs', module: 'jsr305'
}

compile(group: 'io.dropwizard', name: 'dropwizard-migrations', version: '1.2.9') {
exclude group: 'com.google.code.findbugs', module: 'jsr305'
}

testCompile(group: 'io.dropwizard', name: 'dropwizard-testing', version: '1.2.9') {
exclude group: 'com.google.code.findbugs', module: 'jsr305'
}
testRuntime group: 'com.h2database', name: 'h2', version: '1.4.197'

// crypto
compile group: 'org.springframework.security', name: 'spring-security-crypto', version: '5.1.3.RELEASE'
runtime group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.60'
//end crypto

runtime group: 'mysql', name: 'mysql-connector-java', version: '5.1.6'

And... the build fails

> Task :authentication-application:test FAILED
Error occurred during initialization of boot layer
java.lang.module.FindException: Unable to derive module descriptor for
~/.gradle/caches/modules-2/files-2.1/org.jboss.spec.javax.transaction/jboss-transaction-api_1.2_spec/1.0.1.Final/4441f144a2a1f46ed48fcc6b476a4b6295e6d524/jboss-transaction-api_1.2_spec-1.0.1.Final.jar
Caused by: java.lang.IllegalArgumentException: jboss.transaction.api.1.2.spec:
Invalid module name: '1' is not a Java identifier

What's this about deriving a module descriptor? Java creates Automatic Modules  for every jar on the module path that doesn't define an explicit module via a module-info.class file. Java attempts to determine a module name for the jar by substituting non-alphanumeric characters to dots, and here it changesjboss-transaction-api_1.2_spec-1.0.1.Final.jar to jboss.transaction.api.1.2.spec. Well, that's not a valid module name because you can't have a number by itself inside a dot. So we can't include that jar in our build.

We have a dependency on a jar that is incompatible with the module system. We are going to actually need the classes in this jar, so we can't just exclude it. This is a use case for a technique I call Exclude & Include — exclude the problematic dependency and include a compatible jar with the classes from the excluded dependencies.

The easiest thing to try here would be just a newer version of the jboss transaction dependency, but that would actually open a big can of worms (trust me). Instead, let's use our printJars gradle task  to find this jar and see what's in there:

jar tvf /home/alex/.gradle/caches/modules-2/files-2.1/org.jboss.spec.javax.transaction/jboss-transaction-api_1.2_spec/1.0.1.Final/4441f144a2a1f46ed48fcc6b476a4b6295e6d524/jboss-transaction-api_1.2_spec-1.0.1.Final.jar | grep ".class$"
477 Mon Apr 25 14:55:36 PDT 2016 javax/transaction/HeuristicCommitException.class
468 Mon Apr 25 14:55:36 PDT 2016 javax/transaction/NotSupportedException.class
456 Mon Apr 25 14:55:36 PDT 2016 javax/transaction/RollbackException.class
541 Mon Apr 25 14:55:36 PDT 2016 javax/transaction/Status.class
195 Mon Apr 25 14:55:36 PDT 2016 javax/transaction/Synchronization.class
585 Mon Apr 25 14:55:36 PDT 2016 javax/transaction/SystemException.class
771 Mon Apr 25 14:55:36 PDT 2016 javax/transaction/Transaction.class
1360 Mon Apr 25 14:55:36 PDT 2016 javax/transaction/Transactional$TxType.class
863 Mon Apr 25 14:55:36 PDT 2016 javax/transaction/Transactional.class
463 Mon Apr 25 14:55:36 PDT 2016 javax/transaction/TransactionalException.class
885 Mon Apr 25 14:55:36 PDT 2016 javax/transaction/TransactionManager.class
494 Mon Apr 25 14:55:36 PDT 2016 javax/transaction/TransactionRequiredException.class
500 Mon Apr 25 14:55:36 PDT 2016 javax/transaction/TransactionRolledbackException.class
499 Mon Apr 25 14:55:36 PDT 2016 javax/transaction/TransactionScoped.class
631 Mon Apr 25 14:55:36 PDT 2016 javax/transaction/TransactionSynchronizationRegistry.class
661 Mon Apr 25 14:55:36 PDT 2016 javax/transaction/UserTransaction.class
1468 Mon Apr 25 14:55:36 PDT 2016 javax/transaction/xa/XAException.class
1062 Mon Apr 25 14:55:36 PDT 2016 javax/transaction/xa/XAResource.class
292 Mon Apr 25 14:55:36 PDT 2016 javax/transaction/xa/Xid.class
474 Mon Apr 25 14:55:36 PDT 2016 javax/transaction/HeuristicMixedException.class
491 Mon Apr 25 14:55:36 PDT 2016 javax/transaction/InvalidTransactionException.class
483 Mon Apr 25 14:55:36 PDT 2016 javax/transaction/HeuristicRollbackException.class

It's a bunch of classes in the javax.transaction package. This is common with those old J2EE specs  — you'll find jars providing the classes all over the place. In fact, there is another dependency on maven central that provides these classes — javax.transaction:javax.transaction-api.

Let's try exclude & include with this:

compile(group: 'io.dropwizard', name: 'dropwizard-hibernate', version: '1.2.9') {
exclude group: 'com.google.code.findbugs', module: 'jsr305'
exclude group: 'org.jboss.spec.javax.transaction', module: 'jboss-transaction-api_1.2_spec'
}
compile group: 'javax.transaction', name: 'javax.transaction-api', version: '1.3'

! Sweet!

Now we can go ahead and write a resource to create users.

@Path("/api/users/")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class UserResource {

private final UserDAO userDAO;

public UserResource(UserDAO userDAO) {
this.userDAO = Objects.requireNonNull(userDAO);
}

@POST
@UnitOfWork
public Response createUser(UserRequest userRequest) {
userDAO.createUser(userRequest.getUserName(), userRequest.getPassword());
return Response.status(Response.Status.CREATED).build();
}
}
public class UserDAO extends AbstractDAO<UserEntity> {

private final PasswordEncoder passwordEncoder;

public UserDAO(SessionFactory sessionFactory, PasswordEncoder passwordEncoder) {
super(sessionFactory);
this.passwordEncoder = Objects.requireNonNull(passwordEncoder);
}

public void createUser(String userName, String password) {
UserEntity.withUnsavedInstance(
userName,
passwordEncoder.encode(password),
this::persist
);
}
}
@Entity
@Table(name = "users")
public class UserEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;

@Column(name = "username", nullable = false, unique = true)
private String userName;

@Column(name = "encrypted_password", nullable = false, length = 1024)
private String encryptedPassword;

private UserEntity() {

}

public static void withUnsavedInstance(String userName, String encryptedPassword, Consumer<UserEntity> consumer) {
UserEntity entity = new UserEntity();
entity.userName = Objects.requireNonNull(userName);
entity.encryptedPassword = Objects.requireNonNull(encryptedPassword);
consumer.accept(entity);
}

public long getId() {
return id;
}

public String getUserName() {
return userName;
}

public String getEncryptedPassword() {
return encryptedPassword;
}
}
public class UserDAOTest {

@Rule
public DAOTestRule database = DAOTestRule.newBuilder()
.addEntityClass(UserEntity.class)
.build();

private PasswordEncoder passwordEncoder;
private UserDAO dao;

@Before
public void setUp() {
passwordEncoder = mock(PasswordEncoder.class);
dao = new UserDAO(database.getSessionFactory(), passwordEncoder);
}

@Test
public void testCreateUser() {
when(passwordEncoder.encode(anyString())).thenReturn("ENCRYPTED");

database.inTransaction(() -> dao.createUser("user123", "password123"));

UserEntity entity = database.getSessionFactory().getCurrentSession().get(UserEntity.class, 1);

assertEquals("user123", entity.getUserName());
assertEquals("ENCRYPTED", entity.getEncryptedPassword());

verify(passwordEncoder).encode("password123");
verifyNoMoreInteractions(passwordEncoder);
}
}

We get a strange error compiling the dao test:

/home/alex/workspace/modular-java-example/authentication-application/src/test/java/com/alexkudlick/authentication/application/dao/UserDAOTest.java:39: error: cannot access Referenceable
UserEntity entity = database.getSessionFactory().getCurrentSession().get(UserEntity.class, 1);
^
class file for javax.naming.Referenceable not found

This is surprising because there's no error in the IDE (in IntelliJ at least). Some googling reveals that javax.naming.Referenceable  is part of a module included in the jdk, java.naming. We need to add a requires statement for that module to our module-info.java descriptor:

module com.alexkudlick.authentication.application {
requires com.alexkudlick.authentication.models;

requires jackson.annotations;
requires com.google.common;
requires javax.ws.rs.api;
requires java.naming;

requires hibernate.jpa;
requires dropwizard.hibernate;
requires hibernate.core;
requires spring.security.crypto;
requires dropwizard.servlets;
requires dropwizard.db;
requires dropwizard.migrations;
requires com.fasterxml.jackson.databind;

requires dropwizard.core;
requires dropwizard.configuration;
}

Now the test compiles, but it fails at runtime:

org.hibernate.MappingException: Could not instantiate persister
org.hibernate.persister.entity.SingleTableEntityPersister
...
Caused by: java.lang.reflect.InaccessibleObjectException:
Unable to make field private long
com.alexkudlick.authentication.application.entities.UserEntity.id accessible:
module com.alexkudlick.authentication.application does not
"opens com.alexkudlick.authentication.application.entities" to module hibernate.core

This message should look familiar: module com.alexkudlick.authentication.application does not "opens com.alexkudlick.authentication.application.entities" to module hibernate.core.  It's basically the same problem we had deserializing our json models in the authentication-models module.  In that case we solved it by opening the whole module, but that's not appropriate here. This module only needs to open specific packages to specific modules, so we can open them individually:

module com.alexkudlick.authentication.application {
requires com.alexkudlick.authentication.models;

requires jackson.annotations;
requires com.google.common;
requires javax.ws.rs.api;
requires java.naming;

requires hibernate.jpa;
requires dropwizard.hibernate;
requires hibernate.core;
requires spring.security.crypto;
requires dropwizard.servlets;
requires dropwizard.db;
requires dropwizard.migrations;
requires com.fasterxml.jackson.databind;

requires dropwizard.core;
requires dropwizard.configuration;

opens com.alexkudlick.authentication.application.entities to hibernate.core;
}

Now we get a different error: module java.base does not "opens java.lang" to module javassist. Hibernate uses javassist to generate proxy classes at runtime, and under the module system, javassist can't reflectively access the ClassLoader#defineClass  method. We can't solve this problem by adding an opens statement to our module-info.java file, because the class that needs to be opened isn't in our module. It's in java.base. So we need to add an opens statement at runtime using the chainsaw plugin, just like we did for unit test packages:

opens('java.base', 'java.lang', 'javassist')
,

We also need to open our entity package to javassist, and when we do we get a .

However, the application is still not module-compliant at runtime, as verified by running in IntelliJ

Caused by: java.lang.ClassNotFoundException: javax.xml.bind.JAXBException
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:583)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521)

Hibernate needs access to this class from the java.xml.bind module at runtime. We added that as a runtime dependency,  but we didn't declare the fact that our module needs it. If we want to add a requires statement for this module, we'll need to move it to a compile time dependency and add the requires statement.

After fixing the java.xml.bind issue, we can add a resource to login  and run some quick tests to verify that the application is working as expected:

curl -i -X POST -H "Content-Type: application/json" --data '{"userName": "alex", "password": "test"}' http://localhost:8080/api/users/
HTTP/1.1 201 Created
Date: Thu, 11 Jul 2019 21:31:40 GMT
Content-Length: 0

curl -i -X POST -H "Content-Type: application/json" --data '{"userName": "alex", "password": "test"}' http://localhost:8080/api/tokens/; echo ""
HTTP/1.1 200 OK
Date: Thu, 11 Jul 2019 21:31:58 GMT
Content-Type: application/json
Content-Length: 48

{"token":"3c3f1a08-6d9a-40bd-96e9-46be143a61ee"}
alex@alex-lenovo:~/workspace/modular-java-example$ curl -i http://localhost:8080/api/tokens/3c3f1a08-6d9a-40bd-96e9-46be143a61ee/; echo ""
HTTP/1.1 200 OK
Date: Thu, 11 Jul 2019 21:32:35 GMT
Vary: Accept-Encoding
Content-Length: 0

alex@alex-lenovo:~/workspace/modular-java-example$ curl -i http://localhost:8080/api/tokens/foobar/; echo ""
HTTP/1.1 404 Not Found
Date: Thu, 11 Jul 2019 21:32:41 GMT
Cache-Control: must-revalidate,no-cache,no-store
Content-Type: text/html;charset=iso-8859-1
Content-Length: 250
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<title>Error 404 Not Found</title>
</head>
<body><h2>HTTP ERROR 404</h2>
<p>Problem accessing /api/tokens/foobar/. Reason:
<pre> Not Found</pre></p>
</body>
</html>

We can POST to create a user, POST to get an authentication token, and GET to check token validity. Valid tokens return a 200 OK, and invalid ones return a 404 Not Found.


Adding Input Validation

Our application works, but it doesn't return useful error messages. If you make a bad request, you can trigger errors on the server side that give you no useful information. For instance, if you POST to the /users  endpoint without providing a password, you'll run into some of our internal null checks and get a 500 back:

curl -i -X POST -H "Content-Type: application/json" --data '{"userName": "akud123"}' http://localhost:8080/api/users/; echo ""
HTTP/1.1 500 Internal Server Error
Date: Thu, 18 Jul 2019 20:23:31 GMT
Content-Type: application/json
Content-Length: 110

{"code":500,"message":"There was an error processing your request. It has been logged (ID e535df057e4aa8df)."}
ERROR [2019-07-18 20:44:47,602] io.dropwizard.jersey.errors.LoggingExceptionMapper: Error handling a request: 9d8a9694e2354c7c
! java.lang.IllegalArgumentException: null
! at com.google.common@23.5-jre/com.google.common.base.Preconditions.checkArgument(Preconditions.java:121)
! at com.alexkudlick.authentication.application/com.alexkudlick.authentication.application.dao.UserDAO.createUser(UserDAO.java:22)
! at com.alexkudlick.authentication.application/com.alexkudlick.authentication.application.web.UserResource.createUser(UserResource.java:29)
! at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
! at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
! at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
! at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    public void createUser(String userName, String password) {
Preconditions.checkArgument(userName != null);
Preconditions.checkArgument(password != null);
UserEntity.withUnsavedInstance(
userName,
passwordEncoder.encode(password),
this::persist
);
}

We should take advantage of dropwizard's json validation  to return useful information to api clients. All we have to do is add some validation annotations to our json models (with the appropriate module-info statements, of course):

public class AuthenticationRequest {

@JsonProperty("userName")
@NotNull
@Length(min = 4)
private String userName;

@JsonProperty("password")
@NotNull
@Length(min = 8)
private String password;
    @POST
@UnitOfWork(readOnly = true)
public Response createToken(@Valid @NotNull AuthenticationRequest request) {
...
}

@GET
@Path("{token}/")
public Response checkTokenValidity(@NotEmpty @PathParam("token") String token) {
...
}

...

@POST
@UnitOfWork
public Response createUser(@Valid @NotNull AuthenticationRequest request) {
...
}
...


module com.alexkudlick.authentication.application {
requires com.alexkudlick.authentication.models;

requires jackson.annotations;
requires com.google.common;
requires javax.ws.rs.api;
requires java.naming;

requires java.sql;
requires java.xml.bind;

requires hibernate.jpa;
requires dropwizard.hibernate;
requires hibernate.core;
requires spring.security.crypto;
requires dropwizard.servlets;
requires dropwizard.db;
requires dropwizard.migrations;
requires com.fasterxml.jackson.databind;

requires validation.api;
requires hibernate.validator;

requires dropwizard.core;
requires dropwizard.configuration;

opens com.alexkudlick.authentication.application.config to com.fasterxml.jackson.databind;
opens com.alexkudlick.authentication.application.web to jersey.server;
opens com.alexkudlick.authentication.application.entities to hibernate.core, javassist;
}

Now we get an explicit error message when we misuse the api:

curl -i -X POST -H "Content-Type: application/json" --data '{"userName": "akud123"}' http://localhost:8080/api/users/; echo ""
HTTP/1.1 422
Date: Thu, 18 Jul 2019 23:13:36 GMT
Content-Type: application/json
Content-Length: 39

{"errors":["password may not be null"]}

We have another interesting error case where our application fails opaquely — when a clientPOSTs with a duplicate user name:

curl -i -X POST -H "Content-Type: application/json" --data '{"userName": "akud123", "password": "********"}' http://localhost:8080/api/users/; echo ""
HTTP/1.1 201 Created
Date: Thu, 18 Jul 2019 23:18:08 GMT
Content-Length: 0

curl -i -X POST -H "Content-Type: application/json" --data '{"userName": "akud123", "password": "********"}' http://localhost:8080/api/users/; echo ""
HTTP/1.1 500 Internal Server Error
Date: Thu, 18 Jul 2019 23:18:10 GMT
Content-Type: application/json
Content-Length: 110

{"code":500,"message":"There was an error processing your request. It has been logged (ID 7f04d2a8f8d844f4)."}

For someone with access to application server logs, the problem is easy to diagnose:

ERROR [2019-07-18 23:18:10,459] io.dropwizard.jersey.errors.LoggingExceptionMapper: Error handling a request: 7f04d2a8f8d844f4
! com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException:
Duplicate entry 'akud123' for key 'username_unique_constraint'
! at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
! at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
! at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
! at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:490)

We have an unhandled exception inserting a record in the database, because it violates the uniqueness constraint on user names. Api clients won't have access to these logs, so we should provide enough information to understand the problem rather than just failing. ExceptionMappers are a perfect solution here:

public class ConstraintViolationExceptionMapper implements ExceptionMapper<ConstraintViolationException> {
@Override
public Response toResponse(ConstraintViolationException exception) {
return Response.status(Response.Status.CONFLICT)
.entity(new ErrorResponse("duplicate user name"))
.build();
}
}
@Override
public void run(AuthenticationConfiguration configuration, Environment environment) throws Exception {

...

environment.jersey().register(new ConstraintViolationExceptionMapper());
}

Now clients will get an informative error response when they create duplicate users:

curl -i -X POST -H "Content-Type: application/json" --data '{"userName": "akud1234", "password": "********"}' http://localhost:8080/api/users/; echo ""
HTTP/1.1 409 Conflict
Date: Thu, 18 Jul 2019 23:46:25 GMT
Content-Type: application/json
Content-Length: 34

{"errors":["duplicate user name"]}
,

Building the Client Module

At this point, we have built a fully modularized restful web service with reasonable functionality. Compared to that, building a client module is a piece of cake.

There still are some interesting decisions around modularity here, specifically which modules to require transitively. Therequires transitive directive instructs the module system to cause any downstream code that requires our module to also require the specified dependency. That's important for dependencies whose classes are exposed by the public api of our module. Let's take a look at the client's module-info file to see what this looks like in practice:

module com.alexkudlick.authentication.client {
requires transitive com.alexkudlick.authentication.models;

requires com.google.common;

requires transitive org.apache.httpcomponents.httpclient;
requires org.apache.httpcomponents.httpcore;
requires org.apache.commons.io;

requires com.fasterxml.jackson.databind;

exports com.alexkudlick.authentication.client;
}

The client module requires two dependencies transitively: our models module and apache's http client module.

We want requires transitive com.alexkudlick.authentication.models because there are methods on the client class that return classes from it. Specifically, tryLogin, which returns Optional<AuthenticationToken>:

public Optional<AuthenticationToken> tryLogin(String userName, String password)
throws IOException, InvalidAuthenticationRequestException {
HttpResponse response = post("/api/tokens/", new AuthenticationRequest(userName, password));
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
return Optional.of(parseResponseEntity(response.getEntity(), AuthenticationToken.class));
} else {
return Optional.empty();
}
}

If someone else uses this tryLogin method in their module, they are going to need a requires com.alexkudlick.authentication.models statement or their code won't compile. The models module is required to use this module, so we should definitely have a transitive dependency on it.

The second transitive dependency is more of a judgement call, though, and stems from choices in designing the client class. Take a look at the constructor:

public AuthenticationClient(HttpClient httpClient, URL serviceUrl) {
this.httpClient = Objects.requireNonNull(httpClient);
this.baseURL = Objects.requireNonNull(serviceUrl);
}

This constructor gives any code using this class the greatest flexibility in configuring the behavior of the http client, because they get to construct that client. The downside is that we are exposing our dependency on org.apache.httpcomponents.httpclient to downstream consumers of our module. That's usually a detail you want to keep internal, so you can change it. Within the module system, it also means downstream consumers would have to have requires org.apache.httpcomponents.httpclient statement in their module-info to construct this class. So if we're going to build our class like this, we need a transitive dependency on the apache http client library.

Another approach would be to hide the HttpClient constructor from other code and expose a builder. Then code that uses this module wouldn't have any coupling to the underlying http client. That would, however, constrain calling code's options in terms of configuring http client behavior, since we would have to write builder methods for any configuration we wanted to expose.

I chose to expose the http client module and use a transitive dependency mainly for simplicity. Apache's http client is basically a stock dependency at this point, so it's not unreasonable to add it into downstream dependencies.

These two choices are interesting examples of how java's module system forces you to think about how your module is going to be used by other modules, thus guiding you to a more structured system of module relationships.


Conclusion

In this post, we built a modular java application with internal module relationships and non-trivial dependencies. We learned a few things:

Getting a modular java build set up is a lot of work, there's no doubt about that. But once you get your initial project structure set up with dependencies, things get much smoother. Throughout the post, I highlighted almost every issue with modules to demonstrate the problems, but those are not the sort of things I would normally make a single commit for. I hope this blog doesn't discourage anyone from building modularized java projects — the rewards are definitely there. Modularity helps you structure your code into reusable blocks of functionality, and provides the tools to structure the interaction between those blocks.