The Java module system not only comes with an entire set of new rules to abide by, it also introduces a host of command line options to break them. Whether you need to access internal APIs, add unforeseen modules, or extend modules with classes of your own, they have you covered. In this post I want to go over the five most important command line options that you will need to get your project to compile, test, and run in the face of various migration challenges.
Beyond presenting a few specific options, I close with some general thoughts on command line options and particularly their pitfalls.
By the way, I use
$var as placeholders that you have to replace with the module, package, or JAR names that fix your problem.
▚Five Critical Command Line Options
This post covers
Let's get it on!
▚Accessing Internal APIs With
The command line option
--add-exports $module/$package=$readingmodule exports
$package of $module to $readingmodule.
Code in $readingmodule can hence access all public types in
$package but other modules can not.
(The option is available for the
When setting $readingmodule to
ALL-UNNAMED, all code from the class path can access that package.
When accessing internal APIs during a migrating to Java 9, you will always use that placeholder - only once your own code runs in modules does it really make sense to limit exports to specific modules.
As an example, assume you have a class that uses
com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel - during compilation you would get the following error:
error: package com.sun.java.swing.plaf.nimbus is not visible
(package com.sun.java.swing.plaf.nimbus is declared
in module java.desktop, which does not export it)
The troublesome class imports
NimbusLookAndFeel from the encapsulated package
Note how the error message points out the specific problem, including the module that contains the class.
This clearly doesn't work out of the box on Java 9, but what if we want to keep using it?
Then we'd likely be making a mistake because there's a standardized alternative in
javax.swing.plaf.nimbus, but for the sake of this example let's say we still want to use this one - maybe to interact with legacy code that can not be changed.
All we have to do to successfully compile against
com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel is to add
--add-exports java.desktop/com.sun.java.swing.plaf.nimbus=ALL-UNNAMED to the compiler command.
If we do that manually, it would like as follows:
This way, code happily compiles against encapsulated classes. But it is important to realize that we've only pushed the problem to run time! Adding this export on the command line really only changes the one compilation - there is no information put into the resulting bytecode that would allow that class to access that package during execution. So we still have to figure out how to make it work at run time.
Adding exports on the command line only changes the one compilation
▚Reflectively Accessing Internal APIs With
--add-opens $module/$package=$reflectingmodule can be used to open
$package of $module for deep reflection to $reflectingmodule.
Code in $reflectingmodule can hence reflectively access all types and members in
$package but other modules can not.
When setting $reflectingmodule to
ALL-UNNAMED, all code from the class path can reflectively access that package.When accessing internal APIs during a migrating to Java 9, you will always use that placeholder - only once your own code runs in modules does it really make sense to limit exports to specific modules.
A common case are dependency injection libraries like Guice that use the class loader's internal API, which results in errors like the following:
Caused by: java.lang.reflect.InaccessibleObjectException:
Unable to make ClassLoader.defineClass accessible:
module java.base does not "opens java.lang" to unnamed module
Note how the error message points out the specific problem, including the module that contains the class. To make this work we simply need to open the package containing the class:
▚Adding Classes To Modules With
The compiler and runtime option
--patch-module $module=$artifact merges all classes from
$artifact into $module.
There are a few things to look out for, but let's see an example before we get to them.
When discussing split packages during migration, we looked at the example of a project that uses the annotations
@Generated (from the java.xml.ws.annotation module) and
@Nonnull (from a JSR 305 implementation).
We discovered three things:
- both annotations are in the
javax.annotationpackage, thus creating a split
- we need to add the module manually because it's a Java EE module
- doing so makes the JSR 305 portion of the split package invisible
We can use
--patch-module to mend the split:
This way all classes in
jsr305-3.0.2.jar becomes part of the module java.xml.ws.annotation and can hence be loaded for a successful execution.
There are a few things to look out for, though.
First, patching a module does not automatically add it to the module graph.
If it is not required explicitly, it might still need to be added with
Then, classes added to a module with
--patch-module are subject to normal accessibility rules:
- code that depends on them needs to read the patched module, which must export the necessary packages
- likewise these classes' dependencies need to be in an exported package in a module read by the patched one
This might require manipulating the module graph with command line options like
Since named modules can not access code from the class path, it might also be necessary to create some automatic modules.
▚Extending The Module Graph With
--add-modules $modules, which is available on
java, allows explicitly defining a comma-separated list of root modules beyond the initial module.
(Root modules form the initial set of modules from which the module graph is built by resolving their dependencies.)
This allows you to add modules (and their dependencies) to the module graph that would otherwise not show up because the initial module does not depend on them (directly or indirectly).
A particularly important use case for
--add-modules are Java EE modules, which are not resolved by default when running an application from the class path.
As an example, let's pick a class that uses
JAXBException from the Java EE module java.xml.bind.
Here's how to make that module available for compilation with
An important use case for
are Java EE modules
When the code is compiled and packaged, you need to add the module again for execution:
Other use cases for
--add-modules are optional dependencies.
--add-modules option has three special values:
The first two only work at run time and are used for very specific cases that this post does not discuss.
The last one can be quite useful, though: With it, all modules on the module path become root modules and hence all of them make it into the module graph.
When adding modules it might be necessary to let other modules read them, so let's do that next.
▚Extending The Module Graph With
The compiler and runtime option
--add-reads $module=$targets adds readability edges from $module to all modules in the comma-separated list $targets.
This allows $module to access all public types in packages exported by those modules even though $module has no
requires clauses mentioning them.
If $targets is set to
ALL-UNNAMED, $module can even read the unnamed module.
As an example let's turn to the ServiceMonitor application, which has a monitor.statistics module that could sometimes make use of a monitor.statistics.fancy module.
Without resorting to optional dependencies (which would likely be the proper solution for this specific case), we can use
--add-modules to add the fancy module and then
add-reads to allow monitor.statistics to read it:
▚Thoughts On Command Line Options
With Java 9, you might end up applying more command line options than ever before - it sure has been like that for me. While doing so I had a few insights that might make your life easier.
Command line options do not actually have to be applied to the command.
An alternative are so-called argument files (or @-files), which are plain text files that can be referenced on the command line with
Compiler and runtime will then act as if the file content had been added to the command.
The example on
--patch-module showed how to run code that uses annotations from Java EE and JSR 305:
--patch-module are added to make the compilation work on Java 9.
We could put these two lines in a file called
java-9-args and then launch as follows:
What's new in Java 9 is that the JVM also recognizes argument files, so they can be shared between compilation and execution.
Unfortunately, argument files don't work with Maven because the compiler plugin already creates a file for all of its own options and Java does not supported nested argument files. Sad.
▚Relying On Weak Encapsulation
The Java 9-15 runtimes allow illegal access by default to code on the class path with nothing more than a warning.
That's great to run unprepared applications on newer Java versions, but I advise against relying on that during a proper build because it allows new illegal accesses to slip by unnoticed.
Instead, I collect all the
--add-opens I need and then activate strong encapsulation at run time with
Don't rely on weak encapsulation
On Java 16, this default behavior switched to denying illegal access by default, but
--illegal-access could be used to overwrite that.
In Java 17 and later, there is no blanket escape hatch: the
--illegal-access option becomes a no-op, but
--add-opens remain in place for specific exceptions.
▚The Pitfalls Of Command Line Options
Using command line options has a few pitfalls:
- these options are infectious in the sense that if a JAR needs them, all of its dependencies need them as well
- developers of libraries and frameworks that require specific options will hopefully document that their clients need to apply them, but of course nobody reads the documentation until it's too late
- application developers will have to maintain a list of options that merge the requirements of several libraries and frameworks they use
- it is not easy to maintain the options in a way that allow sharing them between different build phases and execution
- it is not easy to determine which options can be removed due to a dependency update to a Java 9 compatible version
- it can be tricky to apply the options to the right Java processes, for example for a build tool plugin that does not run in the same process as the build tool
All of these pitfalls make one thing very clear: Command line options are a fix, not a proper solution, and they have their own long-term costs. This is no accident - they were designed to make the undesired possible. Not easy, though, or there would be no incentive to solve the underlying problem.
Command line options are a fix, not a proper solution
So do your best to only rely on public and supported APIs, not to split packages, and to generally avoid picking fights with the module system. And, very importantly, reward libraries and frameworks that do the same! But the road to hell is paved with good intentions, so if everything else fails, use every command line option at your disposal.
These five options should get you through most thickets:
--add-exportsto export a package, which makes its public types and members accessible (
--add-opensto open a package, which makes all its types and members accessible (
--patch-moduleadds classes to a specific module
--add-modulesadds the listed modules and their transitive dependencies to the module graph
--add-readsmakes one module read another
As discussed, command line options come with a set of pitfalls, so make sure to only use them where absolutely necessary and work to reduce those cases.