What is JPMS? Introducing the Java Platform Module System

The module system introduced in Java 9 makes it easier to organize your code. Here’s a brief guide to working with modules in Java

What is JPMS? Introducing the Java Platform Module System
Thinkstock

Until Java 9, Java’s top-level code organization element had been the package. Starting with Java 9 that changed: above the package now is the module. The module collects related packages together.

The Java Platform Module System (JPMS) is a code-level structure, so it doesn’t change the fact that we package Java into JAR files. Ultimately, everything is still bundled together in JAR files. The module system adds a new, higher-level descriptor that JARs can use, by incorporating the module-info.java file.

Large-scale apps and organizations will take advantage of modules to better organize code. But everyone will be consuming modules, as the JDK and its classes are now modularized.

Why Java needs modules

JPMS is the outcome of project Jigsaw, which was undertaken with the following stated aims: 

  • Make it easier for developers to organize large apps and libraries
  • Improve the stucture and security of the platform and JDK itself
  • Improve app performance
  • Better handle decomposition of the platform for smaller devices

It’s worth noting that the JPMS is a SE (Standard Edition) feature, and therefore effects every aspect of Java from the ground up. Having said that, the change is designed to allow most code to function without modification when moving from Java 8 to Java 9. There are some exceptions to this, and we’ll note them later in this overview.

The chief idea behind a module is to allow the collection of related packages that are visible to the module, while hiding elements from external consumers of the module. In other words, a module allows for another level of encapsulation.

Class path vs. module path

In Java until now the class path has been the bottom line for what is available to a running application. Although the class path serves this purpose and is well understood, it ends up being a big, undifferentiated bucket into which all dependencies are placed.

The module path adds a level above the class path. It serves as a container for packages and determines what packages are available to the application.

Modules in the JDK

The JDK itself is composed of modules now. Let’s start by looking at the nuts and bolts of JPMS there.

If you have a JDK on your system, you also have the source. If you are unfamiliar with the JDK and how to obtain it, take a look at this InfoWorld article.

Inside your JDK install directory is a /lib directory. Inside that directory is a src.zip file. Unzip that into a /src directory.

Look inside the /src directory, and navigate to the /java.base directory. There you will find the module-info.java file. Open it up.

After the Javadoc comments at the head, you’ll find a section named module java.base followed by a series of exports lines. We won’t dwell on the format here, as it becomes fairly esoteric. The details can be found here.

You can see that many of the familiar packages from Java, like java.io, are exported from the java.base module. This is the essence of a module gathering together the packages.

The flip side of exports is the requires instruction. This allows a module to be required by the module being defined.

When running the Java compiler against modules, you specify the module path in similar fashion to the class path. This allows the depedencies to be resolved.

Creating a modular Java project

Let’s take a look at how a modulized Java project is structured.

We’re going to make a small program that has two modules, one that supplies a dependency and the other that uses that dependency and exports an executable main class.

Create a new directory somewhere convenient on your file system. Call it /com.javaworld.mod1. By convention, Java modules live in a directory that has the same name as the module.

Now, inside this directory, create a module-info.java file. Inside, add the content from Listing 1.

Listing 1: com.javaworld.mod1/module-info.java


module com.javaworld.mod1 { exports com.javaworld.package1; }

Notice that the module and the package it exports are different names. We are defining a module that exports a package.

Now create a file on this path, inside the directory that contains the module-info.java file: /com.javaworld.mod1/com/javaworld/package1. Name the file Name.java. Put the contents of Listing 2 inside it.

Listing 2: Name.java


package com.javaworld.package1;

public class Name {
  public String getIt() {
    return "Java World";
  }
}

Listing 2 will become a class, package, and module upon which we depend.

Now let’s create another directory parallel to /com.javaworld.mod1 and call it /com.javaworld.mod2. In this directory, let’s create a module-info.java module definition that imports the module we already created, as in Listing 3.

Listing 3: com.javaworld.mod2/module-info.java


module com.javaworld.mod2 {
  requires com.javaworld.mod1;
}

Listing 3 is pretty self-explanatory. It defines the com.javaworld.mod2 module and requires com.javaworld.mod1.

Inside the /com.javaworld.mod2 directory, create a class path like so: /com.javaworld.mod2/com/javaworld/package2.

Now add a file inside called Hello.java, with the code provided in Listing 4.

Listing 4: Hello.java


package com.javaworld.package2;

import com.javaworld.package1.Name;

public class Hello {
  public static void main(String[] args) {
    Name name = new Name();
    System.out.println("Hello " + name.getIt());
  }
}

In Listing 4, we begin by defining the package, then importing the com.javawolrd.package1.Name class. Take note that these elements function just as they always have. The modules have changed how the packages are made available at the file structure level, not the code level.

Similarly, the code itself should be familiar to you. It simply creates a class and calls a method on it to create a classic “hello world” example.

Running the modular Java example

The first step is to create directories to receive the output of the compiler. Create a directory called /target at the root of the project. Inside, create a directory for each module: /target/com.javaworld.mod1 and /target/com.javaworld.mod2.

Step 2 is to compile the dependency module, outputting it to the /target directory. At the root of the project, enter the command in Listing 5. (This assumes the JDK is installed.)

Listing 5: Building Module 1


javac -d target/com.javaworld.mod1 com.javaworld.mod1/module-info.java com.javaworld.mod1/com/javaworld/package1/Name.java

This will cause the source to be built along with its module information.

Step 3 is to generate the dependent module. Enter the command shown in Listing 6.

Listing 6: Building Module 2


javac --module-path target -d target/com.javaworld.mod2 com.javaworld.mod2/module-info.java com.javaworld.mod2/com/javaworld/package2/Hello.java

Let’s take a look at Listing 6 in detail. It introduces the module-path argument to javac. This allows us to define the module path in similar fashion to the --class-path switch. In this example, we are passing in the target directory, because that is where Listing 5 outputs Module 1.

Next, Listing 6 defines (via the -d switch) the output directory for Module 2. Finally, the actual subjects of compilation are given, as the module-info.java file and class contained in Module 2.

To run, use the command shown in Listing 7.

Listing 7: Executing the module main class


java --module-path target -m com.javaworld.mod2/com.javaworld.package2.Hello

The --module-path switch tells Java to use /target directory as the module root, i.e., where to search for the modules. The -m switch is where we tell Java what our main class is. Notice that we preface the fully qualified class name with its module.

You will be greeted with the output Hello Java World.

Backward compatibility 

You may well be wondering how you can run Java programs written in pre-module versions in the post Java 9 world, given that the previous codebase knows nothing of the module path. The answer is that Java 9 is designed to be backwards compatible. However, the new module system is such a big change that you may run into issues, especially in large codebases.

When running a pre-9 codebase against Java 9, you may run into two kinds of errors: those that stem from your codebase, and those that stem from your dependencies.

For errors that stem from your codebase, the following command can be helpful: jdeps. This command when pointed at a class or directory will scan for what dependencies are there, and what modules those dependencies rely on.

For errors that stem from your dependencies, you can hope that the package you are depending on will have an updated Java 9 compatible build. If not you may have to search for alternatives.

One common error is this one:

How to resolve java.lang.NoClassDefFoundError: javax/xml/bind/JAXBException

This is Java complaining that a class is not found, because it has migrated to a module without visibility to the consuming code. There are a couple of solutions of varying complexity and permanency, described here.

Again, if you discover such errors with a dependency, check with the project. They may have a Java 9 build for you to use.

JPMS is a fairly sweeping change and it will take time to adopt. Fortunately, there is no urgent rush, since Java 8 is a long-term support release.

That being said, in the long run, older projects will need to migrate, and new ones will need to use modules intelligently, hopefully capitalizing on some of the promised benefits.

This story, "What is JPMS? Introducing the Java Platform Module System" was originally published by JavaWorld.

Copyright © 2020 IDG Communications, Inc.