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.