Migrating to Java Platform Module System
Tags: java, jpms, jigsaw, maven
Published: 2021-01-12 22:38:22

---
title: Migrating to Java Platform Module System
published: true
description: Migrating non-modular modules to modular ones.
tags: ["java", "jpms", "jigsaw", "maven"]
cover_image: https://source.unsplash.com/B-x4VaIriRc
---

## 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][1] is the missing artifact used by [foo][2]. It is configured to be deployed to [foo's lib folder][3] as a non-modular jar.

Project [foo][2] is the artifact we require in our project, [baz][0]. It is configured to be deployed to [baz's lib folder][4] 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][5] to strip [foo's pom][6] from its dependencies so that [bar][1] will not be known at compile time to whoever uses [foo][2].

Basically, [baz][0] depends on [foo][2], without depending on [bar][1] and the code compiles successfully. :grin:</br>
It will, however, throw a `NoClassDefFoundError` exception if we were to try an access [bar][1]'s classes. This is demonstrated in [BazTest.java][7].

> Pre-JPMS code is in [this commit][8].

### 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. :dizzy_face:
Demonstrated in [this commit][9].

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

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

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`. :disappointed:

### Migrating foo

We use the [moditect plugin][10] 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][0].
- The non-modular jar [foo][2] is given to us (without the source files).
- The non-modular jar [bar][1] 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][11] 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`):

```shell
jdeps --generate-module-info .\target\descs --ignore-missing-deps .\lib\com\example\foo\0.0.1\foo-0.0.1.jar
```

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

```java
module foo {
    exports com.example.foo;
}
```

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

```xml
<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>
```

#### 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:

```xml
<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>
```

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.`

```java
module com.example.foo {
    exports com.example.foo;
}
```

#### 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.

```java
// 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;
}
```

A modification is also required in the test sources descriptor:

```java
// 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;
}
```

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:

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

#### 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:

```xml
<plugin>
    <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
        <compilerArgs>
            <arg>--upgrade-module-path</arg>
            <arg>${project.build.directory}/modules</arg>
        </compilerArgs>
    </configuration>
</plugin>
```

This, unfortunately, will not work in our case. :worried:

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:

```shell
[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
```

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][12] to create a temporary file with the classpath content excluding `foo` in _target\fixedClasspath.txt_:

```xml
<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>
```

##### Create a new Modulepath

We then use the [gmavenplus plugin][13] to execute a small _Groovy_ script creating a property containing the fixed modulepath.

We use the [os plugin][14] to create the `os.detected.name` property.

```xml
<extension>
    <groupId>kr.motd.maven</groupId>
    <artifactId>os-maven-plugin</artifactId>
    <version>1.6.2</version>
</extension>
```

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

```xml
<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>
```

##### Patch compiler with new Modulepah

As simple as:

```xml
<plugin>
    <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
        <compilerArgs>
            <arg>--module-path</arg>
            <arg>${modulePath}</arg>
        </compilerArgs>
    </configuration>
</plugin>
```

##### 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][11] tweaks:

```xml
<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>
```

That's it.</br>
Everything now compiles, and all the tests pass.</br>
:grinning:

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.</br>
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.</br>
:sunglasses:

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

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

[0]: https://github.com/TomerFi/jpms-migration-playground
[1]: https://github.com/TomerFi/jpms-migration-playground/tree/master/bar
[2]: https://github.com/TomerFi/jpms-migration-playground/tree/master/foo
[3]: https://github.com/TomerFi/jpms-migration-playground/tree/master/foo/lib/com/example/bar
[4]: https://github.com/TomerFi/jpms-migration-playground/tree/master/lib/com/example/foo
[5]: https://www.mojohaus.org/flatten-maven-plugin/
[6]: https://github.com/TomerFi/jpms-migration-playground/blob/master/foo/.flattened-pom.xml
[7]: https://github.com/TomerFi/jpms-migration-playground/blob/master/src/test/java/com/example/baz/BazTest.java
[8]: https://github.com/TomerFi/jpms-migration-playground/tree/07ed726f752545f5c72a0d853ebe1feb47e18320
[9]: https://github.com/TomerFi/jpms-migration-playground/tree/7755eec38d055dd98a003747d1dbc5d37a4f799d
[10]: https://github.com/moditect/moditect
[11]: https://github.com/sormuras/junit-platform-maven-plugin
[12]: https://maven.apache.org/plugins/maven-dependency-plugin
[13]: https://groovy.github.io/GMavenPlus
[14]: https://github.com/trustin/os-maven-plugin