Sunday, September 23, 2018

Removing Joda from Spring Boot

Automated Acceptance Criteria

Removing Joda from Spring Boot

The problem

We recently migrated a medium-sized Java project to Spring Boot 2 from version 1. One of the challenges was migrating to the JDK date-time library from Joda. It turns out that Spring Boot 2 has excellent native support for JDK date-times, as does Jackson (JSON) and Hibernate (database), the default technologies offered by Spring Boot 2 for these features.

The migration itself went smoothly, which is unsurprising given the fantastic work of Stephen Colebourne in designing JDK date-time support based on his authorship of Joda.

So we looked at disabling Joda completely in our Gradle build. The most concise approach we found was:

configurations {
    compile.exclude group: 'joda-time'
}

This removed Joda completely from configurations (classpaths) related to Java. However, this had unintended side effects:

  • During tests, we needed Joda in the runtime classpath for a 3rd-party library, OpenSAML
  • During boot run (running the app), we needed Joda in the classpath for another 3rd-party library, SpringFox

We easily found a workaround for SpringFox, but not for OpenSAML.

(If you're curious, yes, we do intent to migrate from OpenSAML 2 (desupported in 2016) to OpenSAML 3; however, we would like spring-security-saml2-core support first.)

A solution in progress

The exclusion in the compile configuration does exactly what we need: Joda disappears! But what to do about SpringFox and OpenSAML?

For the Spring Boot runtime classpath, there is another concise solution, though finding it was rather troublesome, and it is not well-documented by Pivotal or in Stack Overflow.

First, we setup another classpath of our own making named bootRuntime:

configurations {
    // Other parts of "configurations", including the Joda exclusion from above

    bootRuntime // Synthetic configuration for deps needed *only* to launch app
}

Then we added Joda to that synthetic classpath relying on Spring Boot plugin's definition for the version of Joda to use:

dependencies {
    // Other parts of "dependencies"

    bootRuntime 'joda-time:joda-time'
}

Lastly, we taught Spring Boot to include this synthetic classpath when launching our app (this was the trickiest part):

bootJar {
    bootInf {
        from configurations.bootRuntime
        into 'lib'
    }
}

bootRun {
    classpath += configurations.bootRuntime
}

This adds Joda to the runtime classpath for both the single "fat jar" built by Spring (bootJar), and when launching the app on the command line with gradle (bootRun).

Unless you are a heavy Gradle user, from ... into ... syntax may be unfamiliar: this copies the jars in the synthetic configuration into the fat jar at the location Boot expects to find them. The "'lib'" is literally a directory location within the jar. Useful magic, but a bit obtuse. The outcome:

$ jar tf build/libs/the-project.jar | grep joda-time
BOOT-INF/lib/joda-time-2.9.9.jar

As a matter of fact, Joda is the very last file in the boot jar, a suggestion that it was added by our bootInf section after the Boot plugin built the jar.

(Our workaround is intentionally small. If we're unable to make it work, we'll switch to brute-force library exclusions in our dependencies lists. The goal is to prevent accidental import from Joda, for example, of LocalDate.)

Remaining work

For running the boot app, this solution is great: it is small, readable, easy to maintain, and it works. However, for tests which exercise our user authentication with OpenSAML, it fails. Joda is not in the test classpath, and we cannot use or mock OpenSAML methods which use Joda types.

Barring another magical solution like bootRuntime, we'll fall back on manually excluding Joda from each dependency, and adding it back in to the test classpath. A pity given how pithy the solution is with exclusion from the compile configuration.

No comments: