JEP 261: Module System
Summary
Implement the Java Platform Module System, as specified by JSR 376, together with related JDK-specific changes and enhancements.
Description
The Java Platform Module System (JSR 376) specifies changes and extensions to the Java programming language, the Java virtual machine, and the standard Java APIs. This JEP implements that specification. As a consequence, the javac
compiler, the HotSpot virtual machine, and the run-time libraries implement modules as a fundamental new kind of Java program component and provide for the reliable configuration and strong encapsulation of modules in all phases of development.
This JEP also changes, extends, and adds JDK-specific tools and APIs, which are outside the scope of the JSR, that are related to compilation, linking, and execution. Related changes to other tools and APIs, e.g., the javadoc
tool and the Doclet API, are the subject of separate JEPs.
This JEP assumes that the reader is familiar with the latest State of the Module System document and also the other Project Jigsaw JEPs:
- 200: The Modular JDK
- 201: Modular Source Code
- 220: Modular Run-Time Images
- 260: Encapsulate Most Internal APIs
- 282: jlink: The Java Linker
Phases
To the familiar phases of compile time (the javac
command) and run time (the java
run-time launcher) we add the notion of link time, an optional phase between the two in which a set of modules can be assembled and optimized into a custom run-time image. The linking tool, jlink
, is the subject of JEP 282; many of the new command-line options implemented by javac
and java
are also implemented by jlink
.
Module paths
The javac
, jlink
, and java
commands, as well as several others, now accept options to specify various module paths. A module path is a sequence, each element of which is either a module definition or a directory containing module definitions. Each module definition is either
-
A module artifact, i.e., a modular JAR file or a JMOD file containing a compiled module definition, or else
-
An exploded-module directory whose name is, by convention, the module's name and whose content is an "exploded" directory tree corresponding to a package hierarchy.
In the latter case the directory tree can be a compiled module definition, populated with individual class and resource files and a module-info.class
file at the root or, at compile time, a source module definition, populated with individual source files and a module-info.java
file at the root.
A module path, like other kinds of paths, is specified by a string of path names separated by the host platform's path-separator character (':'
on most platforms, ';'
on Windows).
Module paths are very different from class paths: Class paths are a means to locate definitions of individual types and resources, whereas module paths are a means to locate definitions of whole modules. Each element of a class path is a container of type and resource definitions, i.e., either a JAR file or an exploded, package-hierarchical directory tree. Each element of a module path, by contrast, is a module definition or a directory which each element in the directory is a module definition, i.e., a container of type and resource definitions, i.e., either a modular JAR file, a JMOD file, or an exploded module directory.
During the resolution process the module system locates a module by searching along several different paths, dependent upon the phase, and also by searching the compiled modules built-in to the environment, in the following order:
-
The compilation module path (specified by the command-line option
--module-source-path
) contains module definitions in source form (compile time only). -
The upgrade module path (
--upgrade-module-path
) contains compiled definitions of modules intended to be used in preference to the compiled definitions of any upgradeable modules present amongst the system modules or on the application module path (compile time and run time). -
The system modules are the compiled modules built-in to the environment (compile time and run time). These typically include Java SE and JDK modules but, in the case of a custom linked image, can also include library and application modules. At compile time the system modules can be overridden via the
--system
option, which specifies a JDK image from which to load system modules. -
The application module path (
--module-path
, or-p
for short) contains compiled definitions of library and application modules (all phases). At link time this path can also contain Java SE and JDK modules.
The module definitions present on these paths, together with the system modules, define the universe of observable modules.
When searching a module path for a module of a particular name, the module system takes the first definition of a module of that name. Version strings, if present, are ignored; if an element of a module path contains definitions of multiple modules with the same name then resolution fails and the compiler, linker, or virtual machine will report an error and exit. It is the responsibility of build tools and container applications to configure module paths so as to avoid version conflicts; it is not a goal of the module system to address the version-selection problem.
Root modules
The module system constructs a module graph by resolving the transitive closure of the dependences of a set of root modules with respect to the set of observable modules.
When the compiler compiles code in the unnamed module, or the java
launcher is invoked and the main class of the application is loaded from the class path into the unnamed module of the application class loader, then the default set of root modules for the unnamed module is computed as follows:
-
The
java.se
module is a root, if it exists. If it does not exist then everyjava.*
module on the upgrade module path or among the system modules that exports at least one package, without qualification, is a root. -
Every non-
java.*
module on the upgrade module path or among the system modules that exports at least one package, without qualification, is also a root.
Otherwise, the default set of root modules depends upon the phase:
-
At compile time it is usually the set of modules being compiled (more on this below);
-
At link time it is empty; and
-
At run time it is the application's main module, as specified via the
--module
(or-m
for short) launcher option.
It is occasionally necessary to add modules to the default root set in order to ensure that specific platform, library, or service-provider modules will be present in the resulting module graph. In any phase the option
--add-modules <module>(,<module>)*
where <module>
is a module name, adds the indicated modules to the default set of root modules. This option may be used more than once.
As a special case at run time, if <module>
is ALL-DEFAULT
then the default set of root modules for the unnamed module, as defined above, is added to the root set. This is useful when the application is a container that hosts other applications which can, in turn, depend upon modules not required by the container itself.
As a further special case at run time, if <module>
is ALL-SYSTEM
then all system modules are added to the root set, whether or not they are in the default set. This is sometimes needed by test harnesses. This option will cause many modules to be resolved; in general, ALL-DEFAULT
should be preferred.
As a final special case, at both run time and link time, if <module>
is ALL-MODULE-PATH
then all observable modules found on the relevant module paths are added to the root set. ALL-MODULE-PATH
is valid at both compile time and run time. This is provided for use by build tools such as Maven, which already ensure that all modules on the module path are needed. It is also a convenient means to add automatic modules to the root set.
Limiting the observable modules
It is sometimes useful to limit the observable modules for, e.g., debugging, or to reduce the number of modules resolved when the main module is the unnamed module defined by the application class loader for the class path. The --limit-modules
option can be used, in any phase, to do this. Its syntax is:
--limit-modules <module>(,<module>)*
where <module>
is a module name. The effect of this option is to limit the observable modules to those in the transitive closure of the named modules plus the main module, if any, plus any further modules specified via the --add-modules
option.
(The transitive closure computed for the interpretation of the --limit-modules
option is a temporary result, used only to compute the limited set of observable modules. The resolver will be invoked again in order to compute the actual module graph.)
Increasing readability
When testing and debugging it is sometimes necessary to arrange for one module to read some other module, even though the first module does not depend upon the second via a requires
clause in its module declaration. This may be needed to, e.g., enable a module under test to access the test harness itself, or to access libraries related to the harness. The --add-reads
option can be used, at both compile time and run time, to do this. Its syntax is:
--add-reads <source-module>=<target-module>
where <source-module>
and <target-module>
are module names.
The --add-reads
option can be used more than once. The effect of each instance is to add a readability edge from the source module to the target module. This is, essentially, a command-line form of a requires
clause in a module declaration, or an invocation of an unrestricted form of the Module::addReads
method. As a consequence, code in the source module will be able to access types in a package of the target module at both compile time and run time if that package is exported via an exports
clause in the source module's declaration, an invocation of the Module::addExports
method, or an instance of the --add-exports
option (defined below). Such code will, additionally, be able to access types in a package of the target module at run time if that module is declared to be open or if that package is opened via an opens
clause in the source module's declaration, an invocation of the Module::addOpens
method, or an instance of the --add-opens
option (also defined below).
If, for example, a test harness injects a white-box test class into the java.management
module, and that class extends an exported utility class in the (hypothetical) testng
module, then the access it requires can be granted via the option
--add-reads java.management=testng
As a special case, if the <target-module>
is ALL-UNNAMED
then readability edges will be added from the source module to all present and future unnamed modules, including that corresponding to the class path. This allows code in modules to be tested by test frameworks that have not, themselves, yet been converted to modular form.
Breaking encapsulation
It is sometimes necessary to violate the access-control boundaries defined by the module system, and enforced by the compiler and virtual machine, in order to allow one module to access some of the unexported types of another module. This may be desirable in order to, e.g., enable white-box testing of internal types, or to expose unsupported internal APIs to code that has come to depend upon them. The --add-exports
option can be used, at both compile time and run time, to do this. Its syntax is:
--add-exports <source-module>/<package>=<target-module>(,<target-module>)*
where <source-module>
and <target-module>
are module names and <package>
is the name of a package.
The --add-exports
option can be used more than once, but at most once for any particular combination of source module and package name. The effect of each instance is to add a qualified export of the named package from the source module to the target module. This is, essentially, a command-line form of an exports
clause in a module declaration, or an invocation of an unrestricted form of the Module::addExports
method. As a consequence, code in the target module will be able to access public types in the named package of the source module if the target module reads the source module, either via a requires
clause in its module declaration, an invocation of the Module::addReads
method, or an instance of the --add-reads
option.
If, for example, the module jmx.wbtest
contains a white-box test for the unexported com.sun.jmx.remote.internal
package of the java.management
module, then the access it requires can be granted via the option
--add-exports java.management/com.sun.jmx.remote.internal=jmx.wbtest
As a special case, if the <target-module>
is ALL-UNNAMED
then the source package will be exported to all unnamed modules, whether they exist initially or are created later on. Thus access to the sun.management
package of the java.management
module can be granted to all code on the class path via the option
--add-exports java.management/sun.management=ALL-UNNAMED
The --add-exports
option enables access to the public types of a specified package. It is sometimes necessary to go further and enable access to all non-public elements via the setAccessible
method of the core reflection API. The --add-opens
option can be used, at run time, to do this. It has the same syntax as the --add-exports
option:
--add-opens <source-module>/<package>=<target-module>(,<target-module>)*
where <source-module>
and <target-module>
are module names and <package>
is the name of a package.
The --add-opens
option can be used more than once, but at most once for any particular combination of source module and package name. The effect of each instance is to add a qualified open of the named package from the source module to the target module. This is, essentially, a command-line form of an opens
clause in a module declaration, or an invocation of an unrestricted form of the Module::addOpens
method. As a consequence, code in the target module will be able to use the core reflection API to access all types, public and otherwise, in the named package of the source module so long as the target module reads the source module.
Open packages are indistinguishable from non-exported packages at compile time, so the --add-opens
option may not be used in that phase.
The
--add-exports
and--add-opens
options must be used with great care. You can use them to gain access to an internal API of a library module, or even of the JDK itself, but you do so at your own risk: If that internal API is changed or removed then your library or application will fail.
Patching module content
When testing and debugging it is sometimes useful to replace selected class files or resources of specific modules with alternate or experimental versions, or to provide entirely new class files, resources, and even packages. This can be done via the --patch-module
option, at both compile time and run time. Its syntax is:
--patch-module <module>=<file>(<pathsep><file>)*
where <module>
is a module name, <file>
is the filesystem path name of a module definition, and <pathsep>
is the host platform's path-separator character.
The --patch-module
option can be used more than once, but at most once for any particular module name. The effect of each instance is to change how the module system searches for a type in the specified module. Before it checks the actual module, whether part of the system or defined on a module path, it first checks, in order, each module definition specified to the option. A patch path names a sequence of module definitions but it is not a module path, since it has leaky, class-path-like semantics. This allows a test harness, e.g., to inject multiple tests into the same package without having to copy all of the tests into a single directory.
The --patch-module
option cannot be used to replace module-info.class
files. If a module-info.class
file is found in a module definition on a patch path then a warning will be issued and the file will be ignored.
If a package found in a module definition on a patch path is not already exported or opened by that module then it will, still, not be exported or opened. It can be exported or opened explicitly via either the reflection API or the --add-exports
or --add-opens
options.
The --patch-module
option replaces the -Xbootclasspath:/p
option, which has been removed (see below).
The
--patch-module
option is intended only for testing and debugging. Its use in production settings is strongly discouraged.