DEV Community

Cover image for Migrating to Java Platform Module System
Tomer Figenblat
Tomer Figenblat

Posted on • Updated on

Migrating to Java Platform Module System

JPMS Migration Playground

This tutorial presents a complex scenario of migrating a non-modular project to JPMS. Inspired by a scenario I previously faced where one of my transitive dependencies was in-accessible (and not required).

Scenario Walkthrough

Project bar is the missing artifact used by foo. It is configured to be deployed to foo's lib folder as a non-modular jar.

Project foo is the artifact we require in our project, baz. It is configured to be deployed to baz's lib folder as a non-modular jar.

To work around the obvious compliation error of trying to compile foo when bar is inaccessible, we used the flatten plugin to strip foo's pom from its dependencies so that bar will not be known at compile time to whoever uses foo.

Basically, baz depends on foo, without depending on bar and the code compiles successfully. 😁
It will, however, throw a NoClassDefFoundError exception if we were to try an access bar's classes. This is demonstrated in BazTest.java.

Pre-JPMS code is in this commit.

JPMS: foo as Automatic Module

We can get foo in our modulepath, implicitly making it an automatic module by explicitly requiring it by its unstable name. 😡
Demonstrated in this commit.

Note that when compiling, the compiler will inform us of the automatic module usage:

[INFO] Required filename-based automodules detected: [foo-0.0.1.jar]. Please don't publish this project to a public artifact repository!
Enter fullscreen mode Exit fullscreen mode

This means our module won't be a pure module. And we won't be able to make the best out of JPMS. Features like jlink don't work with automatic modules. 😞

Migrating foo

We use the moditect plugin to tweak foo. However, in our case, we can't access bar. Although our project, baz, doesn't require it, it's required by foo.

For clarification:

  • Our project in the works is baz.
  • The non-modular jar foo is given to us (without the source files).
  • The non-modular jar bar is not given to us, nor do we require it.

We aim to make foo modular while ignoring bar so that baz can truly leverage JPMS features while requiring foo.

We're using the junit-platform plugin for testing.

Create a module-info descriptor

The first step is to create a module-info descriptor for foo. This can be achieved in two ways.

From the command line (note the --ignore-missing-deps because we don't have bar):

jdeps --generate-module-info .\target\descs --ignore-missing-deps .\lib\com\example\foo\0.0.1\foo-0.0.1.jar
Enter fullscreen mode Exit fullscreen mode

This will create .\target\descs\foo\module-info.java:

module foo {
    exports com.example.foo;
}
Enter fullscreen mode Exit fullscreen mode

The same can also be achieved in build time using the moditect plugin. Better yet, we can now give the module a stable name.
By convention, let's name it com.example.foo:

<plugin>
    <groupId>org.moditect</groupId>
    <artifactId>moditect-maven-plugin</artifactId>
    <version>1.0.0.RC1</version>
    <executions>
        <execution>
            <id>generate-module-info</id>
            <phase>initialize</phase>
            <goals>
                <goal>generate-module-info</goal>
            </goals>
            <configuration>
                <outputDirectory>${project.build.directory}/descs</outputDirectory>
                <modules>
                    <module>
                        <artifact>
                            <groupId>com.example</groupId>
                            <artifactId>foo</artifactId>
                            <version>0.0.1</version>
                        </artifact>
                        <moduleInfo>
                            <name>com.example.foo</name>
                        </moduleInfo>
                    </module>
                </modules>
                <jdepsExtraArgs>--ignore-missing-deps</jdepsExtraArgs>
            </configuration>
        </execution>
    </executions>
</plugin>
Enter fullscreen mode Exit fullscreen mode

Create a modular jar

Now that we have our module-info descriptor ready, we can create a new modular jar with it.

This is easily achived with the moditect plugin:

<plugin>
    <groupId>org.moditect</groupId>
    <artifactId>moditect-maven-plugin</artifactId>
    <version>1.0.0.RC1</version>
    <executions>
        <execution>
            <id>add-module-info</id>
            <phase>initialize</phase>
            <goals>
                <goal>add-module-info</goal>
            </goals>
            <configuration>
                <modules>
                    <module>
                        <artifact>
                            <groupId>com.example</groupId>
                            <artifactId>foo</artifactId>
                        </artifact>
                        <moduleInfoFile>${project.build.directory}/descs/com.example.foo/module-info.java</moduleInfoFile>
                    </module>
                </modules>
            </configuration>
        </execution>
    </executions>
</plugin>
Enter fullscreen mode Exit fullscreen mode

This will create a modified version foo-0.0.1.jar in target\modules. The modified version will include the following module-info descriptor and will qualify as a named module.

module com.example.foo {
    exports com.example.foo;
}
Enter fullscreen mode Exit fullscreen mode

Use the modular jar

To use our new modular jar, we update the requires statement in the source files of the baz project. From foo, the automatic module unstable name, to com.example.foo, the named module stable name.

// original using an automatic module
module com.example.baz {
  requires foo;
}
// modified using the new named module
module com.example.baz {
  requires com.example.foo;
}
Enter fullscreen mode Exit fullscreen mode

A modification is also required in the test sources descriptor:

// original using an automatic module
open module com.example.baz {
  requires foo;
}
// modified using the new named module
open module com.example.baz {
  requires com.example.foo;

  requires org.junit.jupiter.api;
}
Enter fullscreen mode Exit fullscreen mode

Note that for the test descriptor, we needed to add a requires directive for reading org.junit.jupiter.api.

We didn't need to do so before because as an automatic module, foo could read all the other modules from the modulepath. So it bridged between the modules com.example.baz and org.junit.jupiter.api, so we didn't need to explicitly make com.example.baz read org.junit.jupiter.api.

Now that com.example.foo is a named module, com.example.baz needs to explicitly require org.junit.jupiter.api.

Failing to do so will result in a compilation error:

[ERROR] .../src/test/java/com/example/baz/BazTest.java:[3,32] package org.junit.jupiter.api is not visible
Enter fullscreen mode Exit fullscreen mode

Add the new module to the Modulepath

This is where it gets tricky. Typically, we would configure the compiler to include the new module like so:

<plugin>
    <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
        <compilerArgs>
            <arg>--upgrade-module-path</arg>
            <arg>${project.build.directory}/modules</arg>
        </compilerArgs>
    </configuration>
</plugin>
Enter fullscreen mode Exit fullscreen mode

This, unfortunately, will not work in our case. 😟

The original modulepath is constructed from the classpath, every monolithic jar is treated as part of the unnamed module. This means our jar will exist twice on the modulepath, as part of the unnamed module and as a named module named com.example.foo.

When trying to compile the test classes that access the module, we'll get the obvious error:

[ERROR] .../src/test/java/module-info.java:[1,6] module com.example.baz reads package com.example.foo from both com.example.foo and foo
Enter fullscreen mode Exit fullscreen mode

To avoid the above error, we cannot do upgrade-module-path. We must reconstruct the modulepath from scratch, excluding the original jar.

Create a new Classpath

First, we use the dependency plugin to create a temporary file with the classpath content excluding foo in target\fixedClasspath.txt:

<plugin>
    <artifactId>maven-dependency-plugin</artifactId>
    <executions>
    <execution>
        <id>create-classpath-file</id>
        <goals>
            <goal>build-classpath</goal>
        </goals>
        <configuration>
            <outputFile>${project.build.directory}/fixedClasspath.txt</outputFile>
            <excludeArtifactIds>foo</excludeArtifactIds><!-- every aritifact recreated with moditect should be listed here -->
        </configuration>
    </execution>
    </executions>
</plugin>
Enter fullscreen mode Exit fullscreen mode
Create a new Modulepath

We then use the gmavenplus plugin to execute a small Groovy script creating a property containing the fixed modulepath.

We use the os plugin to create the os.detected.name property.

<extension>
    <groupId>kr.motd.maven</groupId>
    <artifactId>os-maven-plugin</artifactId>
    <version>1.6.2</version>
</extension>
Enter fullscreen mode Exit fullscreen mode

And execute a Groovy script creating a property named modulePath containing the content of the fixed classpath and the named module we previously created:

<plugin>
    <groupId>org.codehaus.gmavenplus</groupId>
    <artifactId>gmavenplus-plugin</artifactId>
    <version>1.12.0</version>
    <dependencies>
        <dependency>
            <groupId>org.codehaus.groovy</groupId>
            <artifactId>groovy-ant</artifactId>
            <version>3.0.7</version>
            <scope>runtime</scope>
        </dependency>
    </dependencies>
    <executions>
        <execution>
        <phase>process-sources</phase>
        <goals>
            <goal>execute</goal>
        </goals>
        <configuration>
            <scripts>
                <script><![CDATA[
                    def delimiter = project.properties['os.detected.name'] == 'windows' ? ';' : ':'
                    def file = new File("$project.build.directory/fixedClasspath.txt")
                    project.properties.setProperty 'modulePath', file.text + delimiter + "$project.build.directory/modules"
                ]]></script>
            </scripts>
        </configuration>
        </execution>
    </executions>
</plugin>
Enter fullscreen mode Exit fullscreen mode
Patch compiler with new Modulepah

As simple as:

<plugin>
    <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
        <compilerArgs>
            <arg>--module-path</arg>
            <arg>${modulePath}</arg>
        </compilerArgs>
    </configuration>
</plugin>
Enter fullscreen mode Exit fullscreen mode
Patch testing Classpath

Next, we must exclude the original monolithic jar foo and include the modular one on the test classpath. This can be achieved with junit-platform plugin tweaks:

<plugin>
    <groupId>de.sormuras.junit</groupId>
    <artifactId>junit-platform-maven-plugin</artifactId>
    <version>1.1.0</version>
    <extensions>true</extensions>
    <configuration>
        <executor>JAVA</executor>
        <tweaks>
            <additionalTestPathElements>
                <element>${project.build.directory}/modules/foo-0.0.1.jar</element>
            </additionalTestPathElements>
            <dependencyExcludes>
                <exclude>com.example:foo</exclude>
            </dependencyExcludes>
        </tweaks>
    </configuration>
</plugin>
Enter fullscreen mode Exit fullscreen mode

That's it.
Everything now compiles, and all the tests pass.
πŸ˜€

The key added value here is that both the project baz and the local dependency foo are now named modules, although we are still missing bar, of course.
We can also use moditect to make foo stop exposing packages related to bar so we won't be able to access them by mistake.
😎

I had fun playing around with JPMS; I hope you did, too.

You can check out the code for this playground in Github.

Top comments (0)