Sunday, October 22, 2017

How to deal with Java EE Modules

I recently spent some time trying to figure out why Hammock wasn't working on Java 9.  It was a very weird error, but something people run into quite often.

Caused by: java.lang.ClassNotFoundException: javax.xml.bind.JAXBException

Errr ok.  I've been using JAX-B for a long time.  I never realized that it was part of Java EE.  It made sense though, given the javax namespace.  Apparently the fix was to add some configuration when running Maven to include JAX-B's module from the JDK (which I guess I always was using?)

mvn clean install -DargLine="--add-modules java.xml.bind"

The -DargLine passes additional options to the forked surefire JVM, --add-modules (plural!) adds a single module (plus its dependencies) to the classpath, otherwise it's not visible.  All of these are runtime issues, visible when the CDI runtime attempts to start.  So after making this change, I figured things would work.

Caused by: java.lang.ClassNotFoundException:

Yay.  Now I remembered why I liked OSGi so much.  Apparently the fix for this one is to add the module to the build.  So I went ahead and ran

mvn clean install -DargLine="--add-modules java.xml.bind --add-modules"

Fair enough, another module.  Not too bad.  So after running that I was expecting more modules to be added, and got this output

Caused by: java.lang.ClassNotFoundException: javax.annotation.Priority

Ok, that's a weird one.  Why's it weird?  The @Priority annotation comes from javax.annotation-api JAR.  It's not provided by the system.  CDI runtimes will bring in this JAR (Weld uses the RI, OWB brings in the geronimo spec).  I spent about 10 hours over three days trying to dissect this one.  I tried all sorts of things to enable the javax.annotation module, even though it was on the classpath.  Before I reveal the solution, it's best to understand why the modules I explicitly enabled above are hidden.

These modules come from Java EE, yet the RI for them was in the JDK itself.  By placing the RI in the JDK, we open up external usage for these internal feature set.  We also set a precedent that all Java implementations will include these dependencies.  That can't be done if these modules are expected to be there but aren't.  We lose the build once, run anywhere mantra Java has followed for so long.  The good news though, each of the modules are available are standalone dependencies, distributed via Maven Central.

Anyways, back to how to fix this problem.  It turns out, when you enable the module, it includes a large number of internal dependencies, including the internal JDK javax.annotation package.  One of the modular changes in the JDK is that a package name can only exist in one module.  Since javax.annotation was coming from the JDK and a JAR on the classpath, the JDK version was being used.  So how to deal with this? Well, it turns out for this use case you cannot rely on JDK provided modules.  By bringing in the actual maven dependencies for these JARs, you can deal with this consistently - not depending on any JDK internals but by publically available JARs.  This is all I had to bring in to make an application compile and run on Java 9:


The great thing, because of the transitive dependencies all needed JARs will work.