Moving a build to modules
A build that Migrating a Maven or Gradle build moved to pom. can go one step
further and declare itself in module-info., so the Java Module System describes what each module
requires and exports, and the published POM is generated from it. This is phase two of that chapter's
two phases: a change of its own, started once the pom. build compares equal
with the old one. Code without a split package, as that chapter's checks found, takes the same route directly.
Taking split packages apart
A module cannot be declared while a package is split. Phase two therefore takes the splits apart first, one package at a time, building after each:
| Split | Taken apart by |
|---|---|
| White-box tests | Moving them into packages of the test module's own, such as <package>., testing the public API where they can, and reaching the rest as the next section shows. |
| Two projects | Moving the classes so each package lives in one module, or merging the projects, with a qualified export for a package only a sibling uses. |
| A dependency | Excluding the jar that holds the package, or moving the project's classes out of it. |
| Two dependencies | Excluding one of the jars, or dropping the test that needs both. |
No module path holds a package two jars share. After a build of the pom., java SharedPackages. at
the root prints each package two jars of a test closure hold:
void main() throws IOException {
var holders = new TreeMap<String, Set<String>>();
try (var files = Files.walk(Path.of("target/build"))) {
for (var jar : files.filter(file -> file.toString().endsWith(".jar")
&& file.getParent().endsWith("resolved")
&& file.toString().contains("testmodule-")).toList()) {
try (var zip = new ZipFile(jar.toFile())) {
zip.stream().map(ZipEntry::getName)
.filter(name -> name.endsWith(".class") && name.contains("/")
&& !name.startsWith("META-INF/"))
.forEach(name -> holders.computeIfAbsent(
name.substring(0, name.lastIndexOf('/')),
_ -> new TreeSet<>()).add(jar.getFileName().toString()));
} catch (ZipException _) {
}
}
}
holders.forEach((folder, jars) -> {
if (jars.size() > 1) {
IO.println(folder.replace('/', '.') + " " + jars);
}
});
}
What the test module reaches
White-box tests reach what they need through the module system's own means rather than through a shared
package. A package the module does not export is made readable to the tests by one line, in both the
process-javac. and the process-test. of the test module:
--add-exports=demo.greeter/demo.greeter.internal=demo.greeter.test
A test module reads only what the tested module exports, when it compiles and when its tests run, so with the
line in the first file alone javac passes and the tests fail with an IllegalAccessError. The published
module-info. stays as it is. An exports <package> to <test module> there would publish the export, and
javac would warn that the test module is not found when it compiles the main module.
A package-private member of a package the module exports already is called by reflection rather than made
public, which would widen the published API. JUnit's ReflectionSupport finds and invokes it, once
--add-opens= in process-test. opens the
package to the module that makes the member accessible.
A test module-info. that names the main module itself, the --patch-module idiom Maven and Gradle use
for white-box tests, is not supported. The tests become a module of their own, as the table above takes them
apart.
What tests on the module path break
The tests then run on the module path, which breaks what read the class path:
| What breaks | What to do |
|---|---|
| A class in the unnamed package | A module holds none, so a test class there fails to load. Move it into a package; a test that needs such a class compiles it at run time. |
javac called by a test |
compile-testing and javax. compile against java., which is empty now. Hand the compiler -classpath with System., or append that property to java. before the tests run, from a LauncherSessionListener the test module provides. |
| Mocking or proxying a JDK interface | A library that mocks or proxies an interface of a JDK module, as java.'s Element, needs to read that module: --add-reads= in process-test.. |
| A jar that cannot be a module | A dependency with a class in the unnamed package fails the tests' module path with a FindException, and no dependency of a module stays on the class path alone. Move the tests that need it to a test source folder only the old build compiles. |
Declaring the build in module-info.java
Each module is the folder whose module-info. sits at the root of its sources. In a Maven tree that is
src/, so the file stays where it is. Every module-info. below the root becomes a module, so one
the old build keeps for itself, as src/, needs an empty . file in its
folder. A root pom. makes the build pick the maven layout, so pass
-Djenesis. while the old build still needs its POM. What the POM said moves
into the module declaration and the files beside it:
| In the POM | In a module-info.java build |
|---|---|
| A dependency | requires <module>, or requires static where it is only compiled against. A jar without a module name resolves by the name the Jenesis Module Index gives it, or by @jenesis.. |
| A version | @jenesis. for a required module, <groupId>/ for one a POM brings in, which pin writes; @jenesis. for a BOM. |
| The release | @jenesis.. |
| An annotation processor | @jenesis.. |
| The main class | @jenesis.. |
src/ |
Moves into src/: a file beside the sources is packaged. |
| The tests | A test module of their own, carrying @jenesis. and requiring the module and the test framework, with packages that differ from the tested module's. |
| Name and description | The first sentence and the second paragraph of the module's Javadoc. |
| The coordinate | Derived from the module name; project and artifact in project. keep a published one. |
| URL, licences, developers, SCM | project.. |
| The version | version= in project. at the root, which -Djenesis. overrides. |
| The published dependencies | What the module requires, each as the Maven artifact it resolved to (see Publishing). |
Keeping published coordinates
A module's coordinate follows from its name: the first two segments form the group ID and the whole name is
the artifact ID. A project whose artifacts were published under other coordinates keeps them. project and
artifact in project. at the root apply to every module, and a module that keeps a coordinate of
its own declares them in a project. of its own, in the META-INF/ folder beside its
sources, which wins over the root file for every key it names. Publishing lists the
other keys the file holds.
Once the pom. files are gone, so is the version they held. Without a version in project. or
the setting, what is staged is unversioned and its POM carries 0-SNAPSHOT.
Pinning again
The pins pin wrote into a pom. do not carry over: until pin runs again, the module build resolves the
newest versions. Write a bare @jenesis. for each required module whose version to
keep, and @jenesis. for one only a dependency's POM brings in, since a
module name reaches only a module that is required by it, and a module name pin for any other module changes
nothing. Then run pin, which adds the checksums and the closure. The module build resolves a conflict as a pom. does, so a version Gradle resolved higher is
pinned here as well.
While the old build remains
The old build's javadoc fails on the @jenesis tags of a module-info. as unknown, so register each tag
the module uses as disabled there. javadoc takes no wildcard: -tag jenesis. on its command line,
tags("jenesis. among Gradle's javadoc options, or a
<tag><name>jenesis. in the <tags> of the maven-javadoc-plugin.
Maven's compiler plugin compiles a src/ whatever its testExcludes or
useModulePath say, and fails on the modules it requires. Have Maven compile a copy of the tests instead: a
copy-resources execution of the maven-resources-plugin at generate-test-sources copies the *. files
of src/, all but module-info., into a folder of target/, which default-testCompile names as
its only compileSourceRoots.
What was shaded
Layers are declared in module-info., so a dependency the old build shaded to keep it private, which the
pom. build kept plain as Migrating a Maven or Gradle build describes, moves
into a module layer now.
java build/ prints this phase for a coding agent as well, beside the steps of
the first.