~ 14 min read
FOMO: Java Modules
If you are interested in Java modules because the JavaFX docs make it seem like you need to use them, you don’t. Here’s a GitHub template to build nice JavaFX apps with a slim native jpackage installers & the ordinary classpath for your application. Works great, have fun.
tl;dr version: Skip the FOMO on the Java module system. If Oracle makes a push to enhance it or otherwise invest in the system, it might make sense to revisit. Until then, enjoy the tiny JVMs produced by jlink and don’t worry about the rest.
The Java module system was introduced with Java 9 under the name Project Jigsaw. From the description:
__“As described in the JSR, the specific goals of the module system are to provide
- Reliable configuration, to replace the brittle, error-prone class-path mechanism with a means for program components to declare explicit dependences upon one another, along with
- Strong encapsulation, to allow a component to declare which of its public types are accessible to other components, and which are not.
These features will benefit application developers, library developers, and implementors of the Java SE Platform itself directly and, also, indirectly, since they will enable a scalable platform, greater platform integrity, and improved performance.”__
At the time, I remember thinking “interesting, but looks like it will need a lot of developer support to make it happen.”
It’s now 2021, five years later. I saw countless articles on Java modules fly by, read descriptions of the tools such as jdeps and jlink, and felt vaguely unsure. Is this something I’m supposed to worry about? Am I a luddite for not getting into modules?
Am I A Bad Developer™ if I don’t modularize my application?
This finally broke from a vague discomfort to a feeling that I really needed to understand this as part of my interest in building Java desktop applications. I was very interested in jpackage - building native desktop installers - and the documentation really, really made it seem like if I didn’t use modules I was somehow a terrible person.
“Classpath?” the documentation seemed to sneer at me. “You are so not with it. Everyone should be using the module system.”
Over the years I’ve gotten very comfortable working with the Maven dependency system. While the XML is a bit verbose, it’s structured and rigorous while flexible enough to do everything I need. And, it’s also a very well established industry standard. But, Maven doesn’t know anything about modules.
So, I tried. I really, really tried.
Now, before we continue, we need to take a few minutes to explain what a Java module really is in (relatively) simple terms.
Java Modules (Arguably Overly Simplified)
At the most basic level, a Java module is a jar file that contains a module-info.class file. That module-info.class file is generated by a module-info.java source file. The module-info.java source file is… not really Java code in the sense of anything you are likely used to. It uses confusing terminology, duplicates information already available in other systems, and is generally pretty difficult and confusing to work with. If you are curious, here’s an article from Oracle attempting to explain modules.
As an example of the confusing terminology used by the module system, requires static in a module-info.java file means that the required dependency is supposed to be optional. This is pretty awful naming. It’s not actually required, and static means something completely different to a Java developer. Perhaps the keyword “optional” could have been used instead?
Whatever. Maybe there was something about the parser/grammar that made it easier to just… reuse an existing Java keyword. ::shrug::
There are a few additional complications (such as multi-release jars, which may contain multiple module-info.class files), but that’s really all you need to know - module-info.java declares dependencies and access rules. Those declarations are used by the JVM to enforce limits on what code can talk to other code. There are other complications, but that’s pretty much it.
For most Java developers, this is actually not solving a real world problem. Right now, most developers use a dependency management system like Maven. While conflicts do occasionally arise, it’s pretty easy to sort them out with (among other things) excludes/includes. Developers would rather use the proper APIs for things, but making something work is also important. This is why stuff like com.sun.misc.Unsafe is still in use - I mean, anyone who reads that package and class name is instantly going to get that it’s probably not a good idea to use it, but if it works… you do what you have to do.
The thing about modules, however, is that it’s an entirely new dependency management system that has no integration with or even concept of the existing Maven dependency management system. The dependency system with almost twenty million published artifacts as of this writing.
Surely it must be possible to bridge these two things? After all, the JDK includes a tool called jdeps that claims to be able to automatically generate module-info.java files from existing code. Perhaps that will work? Maybe we can just use jdeps to generate module-info.java files and have the best of both worlds - nice, official modularized apps while also leveraging the existing Maven dependency system.
Wait, Why Are Modules Important?
Now, if you remember from the beginning, I started by getting interested in JavaFX, which lists modules right on the download page. I was reading the documentation for jpackage, which also heavily references modules throughout.
I really like the idea of small, cross-platform, native installers with JavaFX. I looked into a bunch of other cross-platform frameworks (including everything from Electron to Qt to various C# tooling) and kept coming back to JavaFX. It looked like I needed to learn modules and make my application modular… so. Let’s do this!
First, I tried using Maven to shade my entire application into a single jar. I used jdeps to automatically generate a module-info.java, then pointed jpackage at the project jar and the JavaFX modules. It worked!
Except, it kind of didn’t. As soon as I started pulling in non-trivial dependencies, the shaded jar started to break. The configuration for what to include and what to drop started getting more and more complex and unintuitive.
Then I got a report from a user who was trying to drop Spring Boot with Spring Data JPA into a JavaFX application. Spring Data JPA, of course, has a set of dependencies that includes popular Java libraries such as Hibernate, which in turn makes heavy use of annotations, code generation and reflection.
The shaded jar approach broke horribly. Some dependencies included module-info.class already, some of those were in various sub-directories due to multi-release jars, and there were conflicts in some of the manifest files. The exclusions for the merged, shaded quickly became unreasonable. Each jar needed to be treated differently.
A new approach was needed.
Next, I wrote a plugin to process everything in the declared Maven dependencies and drop everything into two directories. Anything that was already a module would go into one directory, and ordinary non-modular jars would go into another directory. I would then run jdeps on all of the non-modular jars, attach module-info.class to each of those jars, and voila! An entire project of modules, automatically generated from a Maven path! Fantastic!
Except… that also didn’t work.
It turned out that jdeps didn’t actually generate usable module-info.java files for many of the jars. It could generate “open” module-info.java files, but those would exclude important service declarations. Or, it could generate explicit module-info.java files, but those would be restrictive about important reflection operations that would cause breakage.
Grimly, jdeps treated “requires static” as a hard dependency. One of the Spring Boot dependencies actually had a module-info.class (yay!) which declared a few “optional” dependencies (i.e. requires static) on other modules, which in turn had a hard dependency on the Java Activation Framework… which is long since deprecated and no longer included in the JDK. Boo.
So, to recap:
- Some of the jars in the Maven dependency path are already modules, some are not.
- At least one of the jars includes a module, but that module declares a dependency on a no-longer provided JDK library.
- Solution? Strip the existing module-info.class and regenerate a module-info.java file via jdeps!
- Some of the jars need to be declared as open modules (to allow reflection) and some of them need explicit modules (to be exposed as a service provider).
I was (dumb) enough to go ahead and build all of the plumbing required to make all of this work via a Maven plugin. You can actually use the plugin to override on a jar-by-jar basis the automatic generation of module-info.java for every single dependency in your application. Some could be declared open, some regular, and some even custom. Pretty much every jar needed a different selection.
I did my best. I went through the entire dependency tree in all of Spring Boot, make sure that there’s a correct module-info.java file generated by jdeps for every dependency.
After a lot of back and forth, I was able to get jpackage to run jlink and happily build a Spring Boot application as a completely modular application.
Except… it didn’t actually, like, run.
It turned out that at runtime, the application wouldn’t actually launch Spring Boot because the Spring Core jar had a dependency on the Spring Annotations jar that required reflection. The error message? Spring Core needed open module access to Spring Annotations.
I checked, and both of those modules were declared as open by the jdeps generated module-info.java files.
That was it. While I could maybe eventually figure this out, there was absolutely no way in the world an ordinary Java developer would be able to sort this out.
Spring Boot is (arguably) the most popular Java framework for ordinary Java developers right now. I just spent days trying to get this work, and it was nothing but a long, long list of confusing error messages and terminology.
I finally just decided to go ahead and file a bug with the JDK team. The combination of the module system, jdeps, and jlink just didn’t seem to work.
You can read the response to the bug report at the bottom of the page. The bug was closed, with the comment that unless the Spring Boot maintainers try to convert to modules, it’s just not going to happen.
As a side note, it’s interesting to me that the Spring Boot team has taken on the project of making Spring Boot compile to native code via Graal. But no similar investment is being made with modules.
Edit 9/20/2021 - It turns out the lack of tooling for the module system can even bite the experts. https://bugs.openjdk.java.net/browse/JDK-8264998 for a tale of confusion and problems even for the folks who work on the JDK and JavaFX. As of this writing, they are still going back and forth over the use of an automatic module declaration - some things break if it’s present, some if it’s not.
So. I was feeling a bit flummoxed. There was no way that an ordinary user was going to be able to get their app to run modularized, which made it seem that jpackage wasn’t really a viable option.
But wait. There was a reference in the documentation (and bug report) to running jpackage with an existing JVM image. And jlink seemed to have options to generate an image by just declaring which JVM modules were needed.
By default, jpackage invokes jlink automatically. But, with a bit of tweaking, it turned out it was possible to run jlink first, create a nicely trimmed JVM image, and then run jpackage using a classpath.
By using a combination of the Maven copy dependencies, running jlink, and then running jpackage… it all just works. The installers are still nice and small, and everything works as expected by a Maven developer. Just… declared dependencies in Maven as normal, and then it all gets bundled and runs fine.
Here is the final result - a nice JavaFX + Maven template that uses GitHub Actions to generate small, native desktop apps. Yay!
So, all is happy that ends well, right?
But wait.. what in world is going on with Java modules? They were introduced in Java 9, years ago, and… nobody cares?
How is that possible?
A Working Theory on Java Modules
Here’s my guess as to what’s going on with the Java module system - what happened, why, and the future of modules.
I suspect the Java module system was created for one reason, and one reason alone - it is a way to technically enforce the breaking up of the JDK into individual components. The JDK team doesn’t care about stuff like Maven dependencies - that’s just end developer stuff. The JDK itself is never going to use Maven. And, even more to the point, the traditional dependency system is worthless for enforcing things like keeping different chucks of code from using reflection to poke into each other’s internals.
If you are the lead on the JDK, you want a technical way to absolutely enforce the breakup of the JDK itself. You want to be able to build nicely trimmed JVM instances by picking and choosing components. You want a simple file that you know is going to enforce the separation. And for that, you are going to need something like the Java module system
JavaFX is particularly instructive here. For a while, it was bundled with the JDK itself. Then, at some point, someone at Oracle decided it shouldn’t be bundled anymore. The Java module system is perfect for this scenario - JavaFX only has dependencies on components in the JDK itself, so everything it needs is right there.
But… for ordinary Java developers, the module system is a solution in search of a problem. Ordinary Java developers use libraries that use a lot of reflection to poke into all sorts of things. Ordinary Java developers use libraries that hook into low-level, hidden Java API to add desktop app integration features that don’t exist. They do this because they have to, because they need to ship.
I think that over the last few years, Oracle has decided that while modules exist for the JDK itself, they realize that the entire system is a lot of headache with very, very limited gain for a typical Java developer.
That’s why my bug report was shut down so quickly. If anyone was serious about making the Java module system a reality, there would be a real developer evangelism and tooling initiative. There would be improvements to the error reporting by the tooling. There would have been a concrete effort to work with the Spring team to modularize Spring Boot (just like the initiative to support Graal).
Ironically, the module system is great, because it lets me build a tiny JVM by using jlink. That’s fantastic!
But, and this point I could not stress enough - the module system is not for use by ordinary Java developers. Without a push from Oracle to invest in tooling and evangelism, not only would I recommend that developers avoid adding module-info.java files, I would actually recommend that developers of ordinary libraries remove their module-info.java files if they added them. Unless they are written perfectly, they will actually make it harder for end users to use their jar files on the off chance that someone will want to try to modularize them (e.g. jdeps treats “optional” requires static declarations as mandatory).
The worst part is that the module-info.java files that have been written are often broken or wrong. And even if they are right (for example, declaring optional dependencies via … requires static), the module tooling (jdeps) treats it as required.
The only reason an Java developer should care about modules is if they want to use jlink to create slimmed down JVMs for their application. This is actually pretty cool for certain server applications - just ship a tiny prebuilt JVM with ultrafast boot times. It’s also very cool (of course) for distributing Java desktop apps.
As a reminder, the two goals stated in the overview above were to replace the classpath with a new library loading system, and to provide a mechanism for increasing encapsulation. Unfortunately, without investing in tooling (to make the developer experience palatable) and developer evangelism, instead we are now left with a confusing mess. It’s too bad, as both goals are solid and would be helpful.
My personal “acid test” for the module system is Spring Boot (or whatever the most popular server framework of the day happens to be). Can an ordinary developer download and start working with a modularized Spring Boot? If that’s not a priority, it’s not going to happen.
Skip the FOMO on the Java module system. If Oracle makes a push to enhance it or otherwise invest in the system, it might make sense to revisit. Until then, enjoy the tiny JVMs produced by jlink and don’t worry about the rest.