This repository contains recommendations and tests for the Java Platform Module System (JPMS) for JBoss middleware projects.
Jars and directories on the classpath(–class-path -classpath -cp) are treated as a single, monolithic collection of classes in an unnamed module. The modulepath (–module-path -p) specifies directoires that contain exploded module roots and/or modular jars or jmod (compile time) files.
-
requires : specifies a dependency on another module
-
requires static : specifies a dependency on another module at compile time, but optional at runtime (optional dependency)
-
requires transitive : specifies a dependency on another module and makes it available to other modules that depend on the current module (transitive dependency)
-
exports : makes a package public and protected types available to other modules
-
exports X to A,B,…: makes a package public and protected types of package X available to specific modules A,B,… (qualified export)
-
uses : specifies a service used by the current module. It is an interface or abstract class that some other module must define a provides directive for.
-
provides X with Y : specifies a service implementation provided by the current module. X is an interface or extends an abstract class defined the service contract, and Y specifies the name of the service provider class that implements the interface or extends the abstract class.
-
opens : makes a package accessible to reflection at runtime
-
opens X to A,B,…: makes package X accessible to reflection at runtime to specific modules A,B,… (qualified open)
-
open module M …: opens all packages in module M to reflection at runtime
To run in module mode with Java 21 you need 3.5.0+. Earlier versions run with Java 21, but silentfly fall back to classpath mode when the org.objectweb.asm.ClassReader fails to read a module-info.class file. While the asm issue is fixed with versions post 3.3.1, other issues show up unless the later versions are used.
To run with JPMS enabled, you need to create a src/test/java/module-info.java descriptor for your test module. This describes the required modules, service and exports test packages to the test framework. You can explicitly disable the use of modules
The service-loader/pom.xml includes executions of tests using the classpath and modulepath. This is the maven-surefire-plugin setup:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.2</version>
<executions>
<execution> (1)
<id>default-test</id>
<configuration>
<skip>true</skip>
</configuration>
</execution>
<execution> (2)
<id>cp</id>
<goals>
<goal>test</goal>
</goals>
<configuration>
<useModulePath>false</useModulePath> (3)
<includes>
<include>cp/*Test.java</include>
</includes>
</configuration>
</execution>
<execution> (4)
<id>mp</id>
<goals>
<goal>test</goal>
</goals>
<configuration>
<includes>
<include>mp/*Test.java</include>
</includes>
</configuration>
</execution>
</executions>
</plugin>-
Disable the default-test execution mode which would attempt to run all tests using modules since there is a src/test/java/module-info.java
-
A classpath execution configuration that forces classpath mode via <3>.
-
Disable modulepath mode via the
useModulePathconfig element.4 -
A modulepath execution configuration that relies on
useModulePathdefaulting to true.
You can also just force this behavior from the commandline by specifying -Dsurefire.useModulePath=false:
starksm@Scotts-Mac-Studio basic % mvn -Dsurefire.useModulePath=false -Dtest=test.resource.ResourceLoadingTest test
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running test.resource.ResourceLoadingTest
testLoadResourceCL: unnamed module @bd8db5a (1)
configPropsURL = file:/Users/starksm/Dev/JBoss/module-tests/basic/target/classes/config.properties-
Test is in the unamed module rather than "test.tag.jboss.basic"
Every test package should be exported in the src/test/java/module-info.java descriptor. I have seen where you add a new test package, forget to export it and see the test run fine from the maven command line, but fail when run in the with a message like:
java.lang.reflect.InaccessibleObjectException: Unable to make public test.resource.ResourceLoadingTest() accessible: module basic.test does not "exports test.resource" to module org.junit.platform.commons
at java.base/java.lang.reflect.AccessibleObject.throwInaccessibleObjectException(AccessibleObject.java:388)
at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:364)
at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:312)
at java.base/java.lang.reflect.Constructor.checkCanSetAccessible(Constructor.java:194)
at java.base/java.lang.reflect.Constructor.setAccessible(Constructor.java:187)
at java.base/java.util.Optional.orElseGet(Optional.java:364)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1597)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1597)I’m guessing surefire is adding an export of any package it finds a test in while the IDE is just relying on the test module to have this correct. In earlier versions it was also running the test on the classpath.
With the latest 3.5.2 maven-surefire-plugin it reports the same error as the IDE if the test package is not exported:
[ERROR] Errors:
[ERROR] ResourceLoadingTest.testLoadResourceCL » InaccessibleObject Unable to make public test.resource.ResourceLoadingTest() accessible: module basic.test does not "exports test.resource" to module org.junit.platform.commonsI have seen this error:
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.14.0:testCompile (default-testCompile) on project jakartaee-bom-test: Execution default-testCompile of goal org.apache.maven.plugins:maven-compiler-plugin:3.14.0:testCompile failed: Can't compile test sources when main sources are missing a module descriptor -> [Help 1]for a project that was a test only module, it was inherriting a build/resources configuration that was placing a license file into target/classes/META-INF directory. This was causing the compiler to look for a module-info.class file in the main sources directory. The solution was to add the following configuration to the submodule to override the behavior of the maven-resources-plugin:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<executions>
<execution>
<id>default-resources</id>
<phase>process-resources</phase>
<goals>
<goal>resources</goal>
</goals>
<configuration>
<resources>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>You can see what dependencies are modularized with the maven-dependency-plugin dependency:list command. For example, if you go into the basic module and run mvn dependency:list you will see the dependencies with the module type:
starksm@Scotts-Mac-Studio basic % mvn dependency:list
[INFO] Scanning for projects...
[INFO]
[INFO] ----------------------< jboss.tag.modules:basic >-----------------------
[INFO] Building Basic Module Tests 1.0.0-SNAPSHOT
[INFO] from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- dependency:3.7.0:list (default-cli) @ basic ---
[INFO]
[INFO] The following files have been resolved:
[INFO] org.junit.jupiter:junit-jupiter-engine:jar:5.10.3:test -- module org.junit.jupiter.engine
[INFO] org.junit.platform:junit-platform-engine:jar:1.10.3:test -- module org.junit.platform.engine
[INFO] org.apiguardian:apiguardian-api:jar:1.1.2:test -- module org.apiguardian.api
[INFO] org.junit.jupiter:junit-jupiter-api:jar:5.10.3:test -- module org.junit.jupiter.api
[INFO] org.opentest4j:opentest4j:jar:1.3.0:test -- module org.opentest4j
[INFO] org.junit.platform:junit-platform-commons:jar:1.10.3:test -- module org.junit.platform.commons
[INFO] jboss.tag.modules:open:jar:1.0.0-SNAPSHOT:test -- module tag.jboss.open
[INFO] jboss.tag.modules:auto:jar:1.0.0-SNAPSHOT:test -- module tag.jboss.auto [auto] (1)
[INFO] jboss.tag.modules:legacy:jar:1.0.0-SNAPSHOT:test -- module legacy (auto) (2)-
[auto] is in black indicating a specified Automatic-Module-Name header value.
-
(auto) is in yellow indicating a generated automatic module name derived from the artifact name.
The jdeps tool analyzes class files and JAR files to determine package-level or class-level dependencies. It can generate a module descriptor for a JAR file.
The jmod tool creates JMOD files, which are a packaging format for the Java runtime system. A JMOD file is a compressed file that contains a set of directories and files. It can contain class files, native libraries, configuration files, and other resources.
The jlink tool links a set of modules and their dependencies to create a custom runtime image. The custom runtime image contains only the modules that are required to run the application. The custom runtime image can be smaller than the full JDK distribution.
DML module-info (https://github.com/dmlloyd/module-info)
This is a maven plugin/CLI utility for generating module-info.class files from any JDK version (including 8). The module-info.class file is generated by reading a source YAML file, optionally merging in values from your project and/or build system, and then producing the class file from the result.
-
https://github.com/moditect/moditect - This is another maven plugin that can generate module-info files. Currently the following tasks are supported:
-
Generating module-info.java descriptors for given artifacts (Maven dependencies or local JAR files)
-
Adding module descriptors to your project’s JAR as well as existing JAR files (dependencies)
-
Creating module runtime images
-
-
https://github.com/moditect/layrry - Layrry is a launcher and Java API for executing modularized Java applications. It allows to assemble modularized applications based on Maven artifact coordinates of the (modular) JARs to include. Layrry utilizes the Java Module System’s notion of module layers, allowing multiple versions of one module to be used within an application at the same time, as well as dynamically adding and removing modules at application runtime.
The sun.misc.Unsafe class is a part of the Java Platform API, but it is not part of the public API. It is used by the Java runtime system and some libraries to perform low-level operations. The sun.misc.Unsafe class is not guaranteed to be present in all Java implementations, and it is not recommended to use it in application code.
JEPs are coming out to replace sun.misc.Unsafe class.
-
JEP 471: Deprecate the Memory-Access Methods in sun.misc.Unsafe - This JEP proposes to deprecate the memory-access methods in sun.misc.Unsafe. It provides a complete list of the memory-access methods and their standard replacements.
-
JEP 498: Warn upon Use of Memory-Access Methods in sun.misc.Unsafe - This JEP introduces a runtime warning the first time any memory-access method in sun.misc.Unsafe is invoked, alerting developers to the risks of using these methods.
-
JEP 454: Foreign Function & Memory API - In adddition to interop with data and code outside the JVM, it has off-heap memory replacements for allocateMemory/reallocateMemory.
When wanting to load a module, you should first check ModuleFinder.find(String) to see if the module exists before calling ModuleFinder.of(Path).
Similarly, when creating a ModuleLayer, the Configuration instance used should have a before ModuleFinder from the ModuleFinder::ofSystem static method to avoid creating a duplicate module that is unable to read itself. This would cause this code to fail to find a service via ServiceLoader:
Path modulePath = Path.of("../basic/target/classes");
ModuleLayer basicLayer = createModuleLayer(modulePath); (1)
Optional<Module> basicModule = basicLayer.findModule("tag.jboss.basic"); (2)
ClassLoader basicLoader = basicLayer.findLoader("tag.jboss.basic");
AService aServiceBL = ServiceLoader
.load(AService.class, basicLoader) (3)
.findFirst()
.orElseThrow(() -> new IllegalStateException("AService not found"));-
This code uses the
ModuleFinder.of(Path)as the before finder which results in a duplicate "tag.jboss.basic" module being created. -
This "tag.jboss.basic" module fails to equate to ModuleLayer.boot().findModule("tag.jboss.basic") which was used to load the
AServiceinterface class. -
The
load()call returns nothing because a check of whether the module from <2> can reated the module of theAService.classfails because there are two "tag.jboss.basic" Module instances.
A non-modular jar that specifies and automatic module name of tag.jboss.auto using the Automatic-Module-Name manifest attribute. It contains SPI and API interfaces and implementations:
-
tag.jboss.auto.spi.LegacyService
-
tag.jboss.auto.provider.ProviderOfLegacyService
-
-
tag.jboss.auto.api.AutoPublicApi
-
tag.jboss.auto.impl.ImplOfAutoPublicAPI
-
The java command describe output is:
starksm@Scotts-Mac-Studio auto % java -p target/auto-1.0.0-SNAPSHOT.jar -d tag.jboss.auto
[email protected] file:///Users/starksm/Dev/JBoss/module-tests/auto/target/auto-1.0.0-SNAPSHOT.jar automatic
requires java.base mandated
contains tag.jboss.auto.api
contains tag.jboss.auto.impl
contains tag.jboss.auto.provider
contains tag.jboss.auto.spiA transformation of the auto non-modular jar into a modular jar using the maven-dependency-plugin and the module-info plugin. The src/main/java/module-info.yml is:
name: tag.jboss.auto
exports:
- package: tag.jboss.auto.api
- package: tag.jboss.auto.spi
provides:
- serviceType: tag.jboss.auto.api.AutoPublicApi
with:
- tag.jboss.auto.impl.ImplOfAutoPublicAPIThe java command describe output is:
starksm@Scotts-Mac-Studio autoinfo % java -p target/autoinfo-1.0.0-SNAPSHOT.jar -d tag.jboss.auto
[email protected] file:///Users/starksm/Dev/JBoss/module-tests/autoinfo/target/autoinfo-1.0.0-SNAPSHOT.jar
exports tag.jboss.auto.api
exports tag.jboss.auto.spi
requires java.base mandated
provides tag.jboss.auto.api.AutoPublicApi with tag.jboss.auto.impl.ImplOfAutoPublicAPI
contains tag.jboss.auto.impl
contains tag.jboss.auto.providerThis uses a local 2.2-SNAPSHOT build of module-info to work with Java SE 21 target artifacts. The earlier 2.1 release uses a version of asm that was complaining about not supporting the Java SE 21 class file version.
A transformation of the auto non-modular jar into a modular jar using the maven dependency plugin and a local src/main/java/module-info.java. While this does work by setting the maven-dependency-plugin phase to process-sources, it is wonky because in the IDE the module-info.java shows errors complaining about the packages not being read. More hacking with generated-sources/generated-classes may work, but I was trying to see if a module-info.java could easily be added to existing jar without source.
A basic modular jar with no dependencies that exports an API, data and SPI package, and provides an implementation of the exported tag.jboss.modules.basic.spi.AService SPI:
module tag.jboss.basic {
exports tag.jboss.modules.basic.api;
exports tag.jboss.modules.basic.data;
exports tag.jboss.modules.basic.spi;
provides tag.jboss.modules.basic.spi.AService with tag.jboss.modules.basic.provider.ProviderOfAService;
}An open module is a module that has all of its packages open for reflection. This means that any code can access the classes and members of the packages in the module, even if they are not public.
/**
* This module exports all module and opens them for reflection.
*/
open module tag.jboss.open {
exports tag.jboss.modules.open.api;
exports tag.jboss.modules.open.spi;
exports tag.jboss.modules.open.impl;
provides tag.jboss.modules.open.spi.SerializationService with tag.jboss.modules.open.impl.ProviderOfSerializationService;
}Tests related to reflection issues that require opening packages to frameworks using reflection.