Getting Started with the Java Platform Module System Part 3
Sep 25, 2018 • 7 Minute Read
Introduction
In the previous guide, Getting Started with the Java Platform Module System Part 2, I covered examples for working with modules and how to use ServiceLoader with modules. In the final guide of this series, I'll talk about how Maven works with modules and some final thoughts.
What About Maven?
Here's a commonly asked question: Is there an overlap between Maven and JPMS?
The answer is no: they complement each other.
While modularization is more about encapsulation and visibility (i.e. deciding which packages can be seen outside a module/JAR), Maven is about dependency management and compiling code into artifacts. These two things work at different levels.
On the topic of dependency management, every Maven artifact has three parts:
- A group ID, which uniquely identifies the project it belongs
- An artifact ID, which is its name
- A version
The JPMS does not know about versions and, depending on naming conventions, the name of a Java module can be the same as the artifact ID, the union of the group and artifact ID, or something completely different.
The bottom line is that, because of this, we cannot directly associate a Java module with a Maven POM dependency/artifact. This means:
- Maven cannot generate the module-info.java file.
- In a modular application, adding a dependency involves two steps now:
- Adding the dependency to the POM file as always. This dependency can be modularized or not, it doesn't matter. Even if it is not modularized, it becomes a module automatically.
- The dependency's module is added to the module-info.java file of the project.
For example, let's assume our project consists of two modules, one for the API and one for the GUI, which depends on the API module.
We can structure our project with Maven modules. At the high-level, the concept is basically the same. This is what the parent pom.xml would look like:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>my-parent</artifactId>
<packaging>pom</packaging>
<version>1.0</version>
<name>Parent project</name>
<modules>
<module>api</module>
<module>gui</module>
</modules>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<source>9</source>
<target>9</target>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
<!--
...
-->
</project>
Nothing is different from what we are used to, except maybe for the maven-compiler-plugin configuration. Make sure to use at least version 3.6.1.
The directory structure of the project can be similar to the following:
|─ api
| |─ pom.xml
| |─ src
| |─ main
| |─ java
| |─ module-info.java
| |─ my
| |─ example
| |─ api
|─ gui
| |─ pom.xml
| |─ src
| |─ main
| |─ java
| |─ module-info.java
| |─ my
| |─ example
| |─ gui
|- pom.xml
For the API module, the pom.xml will look like this:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.example</groupId>
<artifactId>my-parent</artifactId>
<version>1.0</version>
</parent>
<artifactId>api</artifactId>
<packaging>jar</packaging>
<name>API project</name>
<dependencies>
<!--
...
-->
</dependencies>
<!--
...
-->
</project>
This would be the content of the module-info.java file:
module my.company.api {
exports my.company.api;
// ...
}
Now, in the GUI module, we have to add the dependecy to the API project to its pom.xml:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.example</groupId>
<artifactId>my-parent</artifactId>
<version>1.0</version>
</parent>
<artifactId>gui</artifactId>
<packaging>jar</packaging>
<name>GUI project</name>
<dependencies>
<dependency>
<groupId>org.example</groupId>
<artifactId>api</artifactId>
<version>1.0</version>
</dependency>
<!--
...
-->
</dependencies>
<!--
...
-->
</project>
As well as in module-info.java:
module my.company.webapp {
requires my.company.api;
// ...
}
This way, when it's time to build the project (for example, with mvn package), the Maven compiler plug-in will set up the module path so that the Java compiler can work correctly.
Conclusion
The module system has been introduced to Java to promote stronger encapsulation and better design, while making Java a more flexible and future-proof language. There are features like incubator modules that will bring new APIs to us while they progress towards either finalization or removal in a future release, giving us the chance to provide input. This also fits pretty well with the proposed fast release cycle for Java.
In this guide, you have learned the basics of the JPMS; including the benefits of modules, how they work, and how they integrate with a tool like Maven.
Of course, there are several more things to learn, like custom runtime images, how to migrate classpath projects to modules, or how to unit test a modular project.
The best book I have read about JPMS is Java 9 Modularity by Sander Mak and Paul Bakker. I totally recommended it. Also, The Java Module System by Nicolai Parlog is a book that, at the time of writing this guide, is currently in progress yet looks very promising. Definitely look to these resources if you find yourself using Java 9 and JPMS in the future.
You can find the code of the sample JavaFX with ServiceLoader and Maven apps on this GitHub repository.