© Fu Cheng 2018

Fu Cheng, Exploring Java 9, https://doi.org/10.1007/978-1-4842-3330-6_2

2. The Module System

Fu Cheng

(1)Auckland, New Zealand

When we’re talking about Java 9, the most important topic is Project Jigsaw ( http://openjdk.java.net/projects/jigsaw/ ) or the Java Platform Module System (JPMS), which introduces the module system into the Java platform. Project Jigsaw was supposed to be added in Java 8, but the changes were too big, so it was delayed to release with Java 9. Project Jigsaw brings significant changes to the Java platform, not only to the JDK itself, but also to Java applications running on it.

The Java SE platform and JDK are organized in modules in Java 9 so they can be customized to scale down to run on small devices. Before Java 9, the installation of JRE was all-or-nothing. The JRE contains tools, libraries, and classes that can satisfy requirements for running different applications. But some of these tools, libraries, and classes may not be required for a particular application. For example, a REST API proxy running on the JRE may never use the desktop AWT/Swing library. JPMS makes it possible to strip unnecessary libraries from the JRE to build customized images that are suitable for every unique application. This can dramatically reduce the package size making deployment faster.

The Java community has always wanted a way to build modular Java applications. OSGi ( https://en.wikipedia.org/wiki/OSGi ) is a good choice for doing this at the moment. JPMS also allows developers to create modular Java libraries and applications. Compared to using OSGi, a solution from Java platform itself is more promising.

JPMS is complicated with one JSR—JSR 376: JavaTM Platform Module System ( https://jcp.org/en/jsr/detail?id=376)—and six related JEPs.

  • 200: The Modular JDK

  • 201: Modular Source Code

  • 220: Modular Run-Time Images

  • 260: Encapsulate Most Internal APIs

  • 261: Module System

  • 282: jlink: The Java Linker

This chapter covers the most important concepts of JPMS.

Module Introduction

Take a look at this quote from a document ( http://openjdk.java.net/projects/jigsaw/spec/sotms/ ) by Mark Reinhold ( http://mreinhold.org/ ), the chief architect of the Java Platform Group at Oracle:

A module is a named, self-describing collection of code and data. Its code is organized as a set of packages containing types, i.e., Java classes and interfaces; its data includes resources and other kinds of static information.

Mark Reinhold

From this definition, we get that a module is just a set of compiled Java code and supplementary resources that are organized in a predefined structure. If you already use Maven multiple modules ( https://maven.apache.org/guides/mini/guide-multiple-modules.html ) or Gradle multiproject builds ( https://docs.gradle.org/current/userguide/multi_project_builds.html ) to organize your code, then you can easily upgrade each of these Maven modules or Gradle projects to a JPMS module.

Each JPMS module should have a name that follows the same naming convention as Java packages; that is, it should use the reverse-domain-name pattern—for example, com.mycompany.mymodule. A JPMS module is described using the file module-info.java in the root source directory, which is compiled to module-info.class. In this file, you use the new keyword module to declare a module with a name. Listing 2-1 shows the content of the file module-info.java of the module com.mycompany.mymodule with minimal information.

Listing 2-1. Minimal Module Description
module com.mycompany.mymodule {

}

Now you have successfully created a new JPMS module.

Sample Application

Let me use a sample application to demonstrate the usage of the module system. This is a simple e-commerce application with very limited features. The main objective is to demonstrate how the module system works, so the actual implementations of these modules don’t really matter. The sample application is a Maven project with modules shown in Table 2-1. The namespace of this application is io.vividcode.store, so the module name of common in Table 2-1 is actually io.vividcode.store.common.

Table 2-1. Modules of the Sample Application

Name

Description

common

Common API

common.persistence

Common persistence API

filestore

File-based persistence implementation

product

Product API

product.persistence

Product persistence implementation

runtime

Application bootstrap

Module Declaration

The module declaration file module-info.java is the first step to understanding how module system works.

requires and exports

After the introduction of modules in Java 9, you should organize Java applications as modules. A module can declare its dependencies upon other modules using the keyword requires. Requiring a module doesn’t mean that you have access to its public and protected types automatically. A module can declare which packages are accessible to other modules. Only a module’s exported packages are accessible to other modules, and by default, no packages are exported. The module declaration in Listing 2-1 exports nothing. The keyword exports is used to export packages. Public and protected types in exported packages and their public and protected members can be accessed by other modules.

Listing 2-2 shows the file module-info.java of the module io.vividcode.store.common.persistence. It uses two requires declarations to declare its dependencies upon modules slf4j.api and io.vividcode.store.common. The module slf4j.api comes from the third-party library SLF4J ( https://www.slf4j.org/ ), while io.vividcode.store.common is another module in the sample application. The module io.vividcode.store.common.persistence exports its package io.vividcode.store.common.persistence to other modules.

Listing 2-2. Module Declaration of io.vividcode.store.common.persistence
module io.vividcode.store.common.persistence {
  requires slf4j.api;
  requires io.vividcode.store.common;
  exports io.vividcode.store.common.persistence;
}

Please note, when you export a package, you only export types in this package but not types in its subpackages. For example, the declaration exports com.mycompany.mymodule only exports types like com.mycompany.mymodule.A or com.mycompany.mymodule.B, but not types like com.mycompany.mymodule.impl.C or com.mycompany.mymodule.test.demo.D. To export those subpackages, you need to use exports to explicitly declare them in the module declaration.

If a type is not accessible across module boundaries, this type is treated like a private method or field in the module. Any attempt to use this type will cause an error in the compile time, a java.lang.IllegalAccessError thrown by the JVM in the runtime, or a java.lang.IllegalAccessException thrown by the Java reflection API when you are using reflection to access this type.

Note

All modules, except for the module java.base itself, have implicit and mandatory dependency upon java.base. You don’t need to explicitly declare this dependency.

Transitive Dependencies

When module A requires module B, module A can read public and protected types exported in module B. Here we say module A reads module B. If module B also reads module C, module B can have methods that return types exported in module C.

Listing 2-3 shows the file module-info.java of module C. Module C exports the package ctest.

Listing 2-3. Module Declaration of C
module C {
  exports ctest;
}

Listing 2-4 shows the class ctest.MyC in module C. It only has one method, sayHi(), that prints out a message to the console.

Listing 2-4. Class ctest.MyC in Module C
package ctest;

public class MyC {
  public void sayHi() {
    System.out.println("Hi from module C!");
  }
}

Listing 2-5 shows the file module-info.java of module B. Module B requires module C and exports the package btest.

Listing 2-5. Module Declaration of B
module B {
  requires C;
  exports btest;
}

The method getC() of the class btest.MyB in Listing 2-6 returns a new instance of the class ctest.MyC.

Listing 2-6. Class btest.MyB in Module B
package btest;

import ctest.MyC;

public class MyB {
  public MyC getC() {
    return new MyC();
  }
}

The module A only requires module B in its file module-info.java; see Listing 2-7.

Listing 2-7. Module Declaration of B
module A {
  requires B;
}

The class atest.MyA in module A tries to use the class MyC; see Listing 2-8.

Listing 2-8. Class atest.MyA in Module A
package atest;

import btest.MyB;

public class MyA {
  public static void main(String[] args) {
    new MyB().getC().sayHi();
  }
}

Although the code in Listing 2-8 looks quite reasonable, it cannot be compiled due to the error that module A doesn’t read module C; see the error message in Listing 2-9. The module readability relationship is not transitive by default. Module B reads module C, module A reads module B, but module A doesn’t read module C.

Listing 2-9. Compile Error of Module A
/<code_path>/A/atest/MyA.java:7: error: MyC.sayHi() in package ctest is not
 accessible
  new MyB().getC().sayHi();
          ^
  (package ctest is declared in module C, but module A does not read it)
1 error

To make the code in Listing 2-8 compile, you need to add requires C to the file module-info.java of module A in Listing 2-7. This can be a tedious task when many modules depend on each other. Since this is a common usage scenario, Java 9 provides built-in support for it. The requires declaration can be extended to add the modifier transitive to declare the dependency as transitive. The transitive modules that a module depends on are readable by any module that depends upon this module. This is called implicit readability.

After the declaration of module B is changed to use the modifier transitive in Listing 2-10, module A can be compiled successfully. The transitive module C that module B depends on is readable by module A, which depends upon module B. Module A can now read module C.

Listing 2-10. Updating the Module Declaration of B to Use transitive
module B {
  requires transitive C;
  exports btest;
}

In general, if one module exports a package containing a type whose signature refers to another package in a second module, then the first module should use requires transitive to declare the dependency upon the second module. As in module B, the method getC() of class MyB references the class MyC from module C, so module B should use requires transitive C instead of requires C.

Static Dependencies

You can use requires static to specify that a module dependency is required in the compile time, but optional in the runtime; see Listing 2-11.

Listing 2-11. Example of requires static
module demo {
  requires static A;
}

Static dependencies are useful for frameworks and libraries. Suppose that you are building a library to work with different kinds of databases. The library module can use static dependencies to require different kinds of JDBC drivers. At compile time, the library’s code can access types defined in those drivers. At runtime, users of the library can add only the drivers they want to use. If the dependencies are not static, users of the library have to add all supported drivers to pass the module resolution checks.

Services

Java has its own service interfaces and providers mechanism using the class java.util.ServiceLoader. This service mechanism has been primarily used by JDK itself and third-party frameworks and libraries. A typical example of a service provider is a JDBC driver. Each JDBC driver should provide the implementation of the service interface java.sql.Driver. The driver’s JAR file should have the provider configuration file java.sql.Driver in the directory META-INF/services. For example, the file java.sql.Driver in the JAR file of Apache Derby ( https://db.apache.org/derby/ ) has this content:

org.apache.derby.jdbc.AutoloadedDriver

org.apache.derby.jdbc.AutoloadedDriver is the name of the implementation class of the service interface java.sql.Driver.

Before Java 9, ServiceLoader scanned the class path to locate provider implementations for a given service interface. In Java 9, the module descriptor module-info.java has specific declarations for service consumers and providers.

Listing 2-12 shows the service interface PersistenceService in the module io.vividcode.store.common. The interface PersistenceService has a single method save() to save Persistable objects.

Listing 2-12. Service Interface PersistenceService
package io.vividcode.store.common;

public interface PersistenceService {
  void save(final Persistable persistable) throws PersistenceException;
}

The module io.vividcode.store.common.persistence consumes this service interface. In its file module-info.java in Listing 2-13, the new keyword uses is used to declare the consumption of the service interface io.vividcode.store.common.PersistenceService.

Listing 2-13. Module Declaration of io.vividcode.store.common.persistence
module io.vividcode.store.common.persistence {
  requires slf4j.api;
  requires transitive io.vividcode.store.common;
  exports io.vividcode.store.common.persistence;
  uses io.vividcode.store.common.PersistenceService;
}

Now you can use ServiceLoader to look up providers of this service interface. In Listing 2-14, the method ServiceLoader.load() creates a new service loader for the service type PersistenceService, then you use the method findFirst() to get the first available service provider. If a service provider is found, use its method save() to save Persistable objects.

Listing 2-14. Using ServiceLoader to Look Up Providers
public class DataStore<T extends Persistable> {

  private final Optional<PersistenceService> persistenceServices;

  public DataStore() {
    this.persistenceServices = ServiceLoader
        .load(PersistenceService.class)
        .findFirst();
  }


  public void save(final T object) throws PersistenceException {
    if (this.persistenceServices.isPresent()) {
      this.persistenceServices.get().save(object);
    }
  }
}

The provider of service interface PersistenceService is in the module io.vividcode.store.filestore. In this module’s declaration of Listing 2-15, provides io.vividcode.store.common.PersistenceService with io.vividcode.store.filestore.FileStore means this module provides the implementation of the service interface PersistenceService using the class io.vividcode.store.filestore.FileStore. The implementation of FileStore is quite simple and you can check the source code for its implementation.

Listing 2-15. Module Declaration of io.vividcode.store.filestore
module io.vividcode.store.filestore {
  requires io.vividcode.store.common.persistence;
  requires slf4j.api;
  provides io.vividcode.store.common.PersistenceService
      with io.vividcode.store.filestore.FileStore;
}

Qualified Exports

When you are using exports to export a package in the module declaration, this package is visible to all modules that use requires to require it. Sometimes you may want to limit the visibility of certain packages to some modules only. Consider this example: a package was initially designed to be public to other modules, but this package was deprecated in later versions. Legacy code that uses the old version of this package should continue to work after migrating to Java 9, while new code should use the new versions. The package should only be visible to modules of the legacy code that still use the old version. This is done by using the to clause in exports to specify the names of modules that should have access.

Listing 2-16 shows the module declaration of the JDK module java.rmi. You can see that package com.sun.rmi.rmid is only visible to module java.base and package sun.rmi.server is only visible to modules jdk.management.agent, jdk.jconsole, and java.management.rmi.

Listing 2-16. Module Declaration of JDK Module java.rmi
module java.rmi {
  requires java.logging;


  exports java.rmi.activation;
  exports com.sun.rmi.rmid to java.base;
  exports sun.rmi.server to jdk.management.agent,
      jdk.jconsole, java.management.rmi;
  exports javax.rmi.ssl;
  exports java.rmi.dgc;
  exports sun.rmi.transport to jdk.management.agent,
      jdk.jconsole, java.management.rmi;
  exports java.rmi.server;
  exports sun.rmi.registry to jdk.management.agent;
  exports java.rmi.registry;
  exports java.rmi;


  uses java.rmi.server.RMIClassLoaderSpi;
}

Opening Modules and Packages

In the module declaration, you can add the modifier open before module to declare it as an open module. An open module grants compile time access to explicitly exported packages only, but it grants access to types in all its packages at runtime. It also grants reflective access to all types in all packages. All types include private types and their private members. If you use the reflection API and suppress Java language access checks—using the method setAccessible() of AccessibleObject, for example—you can access private types and members in open modules.

You can also use opens to open packages to other modules. You can access open packages using the reflection API at runtime. Just like open modules, all types in an open package and all their members can be reflected by the reflection API. You can also qualify open packages using to, which has a similar meaning in qualified exports.

The declaration of module E in Listing 2-17 marks it as an open module.

Listing 2-17. Declaration of an Open Module
open module E {
  exports etest;
}

The declaration of module F in Listing 2-18 opens two packages. It’s possible to open packages that don’t exist in the module. It’s also possible to open packages to nonexistent modules. The compiler generates warnings in these cases, which is the same as exporting packages to non-existent modules.

Listing 2-18. Module Declaration That Uses Open Packages
module F {
  opens ftest1;
  opens ftest2 to G;
}

Open modules and open packages are provided mainly for resolving backward compatibility issues. You may need to use them when migrating legacy code that relies on reflections to work.

Working with Existing Code

For developing new projects in Java 9, the concepts in the module declaration are enough. But if you need to work with existing code written before Java 9, you need to understand unnamed modules and automatic modules.

Unnamed Modules

From previous sections, you can see that Java 9 has a strict constraint on how types can be accessed across module boundaries. If you are creating a brand-new application targeting Java 9, then you should use the new module system. However, Java 9 still supports running all applications written prior to Java 9. This is done with the help of the unnamed modules.

When the module system needs to load a type whose package is not defined in any module, it will try to load it from the class path. If the type is loaded successfully, then this type is considered to be a member of a special module called the unnamed module. The unnamed module is special because it reads all other named modules and exports all of its packages.

When a type is loaded from the class path, it can access exported types of all other named modules, including built-in platform modules. For Java 8 applications, all types of this application are loaded from the class path, so they are all in the same unnamed module and have no issues accessing each other. The application can also access platform modules. That’s why Java 8 applications can run on Java 9 without changes.

The unnamed module exports all of its packages, but code in other named modules cannot access types in the unnamed module, and you cannot use requires to declare the dependency—there is no name for you to use to reference it. This constraint is necessary; otherwise we lose all the benefits of the module system and go back to the dark old days of messy class path. The unnamed module is designed purely for backward compatibility. If a package is defined in both a named and unnamed module, the package in the unnamed module is ignored. Unexpected duplicate packages in the class path won’t interfere with the code in other named modules.

Automatic Modules

Since Java 9 is backward compatible to run existing Java applications, it’s not necessary to upgrade existing applications to use modules. However, it’s recommended that you upgrade to take advantages of the new module system.

The recommended approach to migrate existing applications is to do it in a bottom-up way; that is, start with the modules that are in the bottom of the entire dependency tree. For example, in an application that has three modules/subprojects A, B, and C with a dependency tree like A -> B -> C, you start by migrating C to a module first, then B, and then A. After C is migrated to a module, A and B, which are currently in the unnamed module, can still access types in C because the unnamed module reads all named modules. Then you migrate B to a named module and declare it to require the migrated named module C. Finally, migrate A to a named module; at this point, the whole application has been migrated successfully.

It’s not always possible to do the bottom-up migration. Some libraries may be maintained by third parties and you cannot control when these libraries will be migrated to modules. But you still want to migrate modules that depend on those third-party libraries. You cannot just migrate those modules, while leaving third-party libraries in the class path, however, because named modules cannot read the unnamed module. What you can do is put those library JAR files in the module path and turn them into automatic modules.

Other than explicitly created named modules, automatic modules are created implicitly from normal JAR files. There is no module declaration in these JAR files. The name of an automatic module comes from the attribute Automatic-Module-Name in the manifest file MANIFEST.MF of the JAR file, or it is derived from the name of the JAR file. Other named modules can declare dependency upon this JAR file using the name of the automatic module. It’s recommended that you add the attribute Automatic-Module-Name in the manifest, because it is more reliable than the derived module names from JAR file names.

Automatic modules are special in many ways:

  • An automatic module reads all other named modules.

  • An automatic module exports all of its packages.

  • An automatic module reads the unnamed modules.

  • An automatic module grants transitive readability to all other automatic modules.

Automatic modules are the bridge between the class path and explicitly named modules. The goal is to migrate all existing modules/subprojects/libraries to Java 9 named modules. However, during the migrating process, you can always add them to the module path to use them as automatic modules.

In Listing 2-19, the class MyD use the class com.google.common.collect.Lists from Guava ( https://github.com/google/guava ) version 21.0.

Listing 2-19. Class MyD Using the Guava Library
package dtest;

import com.google.common.collect.Lists;

public class MyD {
  public static void main(String[] args) {
    System.out.println(Lists.newArrayList("Hello", "World"));
  }
}

At the time of writing, Guava has not yet been migrated as a Java 9 module. In the module declaration of D in Listing 2-20, you can use requires guava to declare dependency upon it. guava is the name of the automatic module for Guava, which is derived from the JAR file name.

Listing 2-20. Declaring Dependency Upon an Automatic Module
module D {
  requires guava;
}

When compiling the code in Listing 2-20, you can use the command-line option --module-path in javac to add Guava’s JAR file guava-21.0.jar in the directory ∼/libs to the module path.

$ javac --module-path ∼/libs <src_path>/*.java <src_path>/dtest/*.java

JDK Tools

After you finish the source code of a project’s modules, you need to compile and run these modules. Most of time, you’ll use IDEs for development and testing, so you can leave the compilation and execution of the project to the IDE. However, you can still use the JDK tools javac and java directly to compile and run the code, respectively. Understanding details about these JDK tools can help you to understand the whole life cycle of modules. However, it takes time for IDEs and build tools to improve their support for JDK 9. The process may be slow, so you may still need to use these JDK tools for some tasks. When migrating to Java 9, you may encounter various problems related to the module system. If you have a deep understanding of these tools, you can easily find the root cause and solve these problems.

Some of these JDK tools have been upgraded to support module-related options, while others are new in Java 9. These tools support some common command-line options related to common concepts in the module system. These tools can be used in different phases:

  • Compile time: Use javac to compile Java source code into class files.

  • Link time: The new optional phase introduced in Java 9. Use jlink to assemble and optimize a set of modules and their transitive dependencies to create a custom runtime image.

  • Runtime: Use java to launch the JVM and execute the byte code.

Other utility tools have no special phases to work with.

Module Paths

The module system uses module paths to locate modules defined in different artifacts. A module path is a sequence of paths to a module definition or directories containing module definitions. A module definition can be either a module artifact or a directory that contains exploded contents of a module. Module paths are searched based on their order in a sequence to find the first definition that defines a particular module. The module system also uses module paths to resolve dependencies. If the module system cannot find the definition for a dependent module, or if it finds two definitions in the same directory that define modules with the same name, it signals an error and exits. Module paths are separated with platform-dependent path separators: a semicolon (;) for Windows and a colon (:) for macOS and Linux.

Different types of module paths are used in different phases; see Table 2-2. As shown in this table, different command-line options for module paths can be applied to multiple phases. The order for each module path defines the search sequence when multiple module paths exist. For example, at compile time, using the javac, for example, all four types of module paths can be used. The module system checks module paths specified in --module-source-path first, then it checks --upgrade-module-path, then --system, and finally --module-path or -p.

Table 2-2. Different Types of Module Paths

Order

Name

Command-Line Option

Applied Phase

Description

1

Compilation module path

--module-source-path

Compile time

Source code of modules

2

Upgrade module path

--upgrade-module-path

Compile time and runtime

Contains compiled modules used to replace upgradeable modules in the environment

3

System modules

--system

Compile time and runtime

Compiled modules in the environment

4

Application module path

--module-path or -p

All phases

Compiled application modules

The system modules and modules definitions found on the module paths are referred to as the set of observable modules, which are very important in the module resolution process. If a module to resolve doesn’t exist in the set of observable modules, the module system signals an error and exists.

Module Version

Although there is no version-related configuration in the module declaration, it’s still possible to record version information for a module. It’s recommended that you record the module version following the scheme of semantic versioning ( http://semver.org/ ). Build tools like Maven or Gradle should record the version information automatically, so you don’t need to worry about it unless you’re using javac or jar tools directly. The important thing to know is that the module system ignores version information when searching for modules. If a module path contains multiple definitions of modules with the same name but different versions, the module system still treats this as an error. Module name is the only thing that matters when resolving modules.

You can specify module version using the option --module-version.

The Main Module

The main module can be specified using the option --module or -m. At runtime, the main module contains the main class to run. If the main class is recorded in the module’s declaration, then specifying only the module name is enough. Otherwise, you need to use <module>/<mainclass> to specify the module and main class, for example, com.mycompany.mymodule/com.mycompany.mymodule.Main.

At compile time, --module or -m specifies the only module to compile.

Root Modules

The set of observable modules defines all the possible modules that can be resolved. However, not all observable modules are required at runtime. The module system starts the resolution process from a set of root modules, and it constructs a module graph by resolving the transitive closure of the dependencies of these root modules against the set of observable modules. It’s possible that not all observable modules are resolved, and only the observable modules are resolvable.

The module system has some rules it uses to select the default root modules. When you’re compiling or running code in the unnamed module, that is, code that predates Java 9, the default set of root modules for the an unnamed module includes JDK system modules and application modules. If the java.se module exists, it will be the only JDK system module to include. Otherwise, every java.* module that unconditionally exports at least one package is a root module. Every non-java.* module that unconditionally exports at least one package is also a root module.

When you’re compiling or running Java 9 code, the default set of root modules depends on the phase:

  • At compile time, it’s the set of modules to compile.

  • At link time, it’s empty.

  • At runtime, it’s the application’s main module.

The set of root modules can be extended to include extra modules using the option --add-modules. The value of this option is a comma-separated list of module names. There are also three special values of this option.

  • ALL-DEFAULT: Add the default set of root modules for the unnamed module.

  • ALL-SYSTEM: Add all system modules.

  • ALL-MODULE-PATH: Add all observable modules found on the module paths.

Limiting the Observable Modules

It’s possible to limit the observable modules using the option --limit-modules. After you use this option, the set of observable modules is the transitive closure of those specified modules plus the main module and any modules specified via the option --add-modules. The value of this option is also a comma-separated list of module names.

Upgrading the Module Path

The option --upgrade-module- path specifies the upgrade module path. This path contains modules that you can use to upgrade modules that are built-in into the environment. This module path supersedes the existing extension mechanism ( http://docs.oracle.com/javase/8/docs/technotes/guides/extensions/index.html ).

Whether a system module is upgradable is clearly documented in the file module-info.java. For example, modules java.xml.bind and java.xml.ws are upgradable.

Increasing Readability and Breaking Encapsulation

The module system is all about encapsulation. But sometimes you’ll still w ant to break encapsulation when you’re dealing with legacy code or running tests. You can use several command-line options to break encapsulation.

  • --add-reads module=target-module(,target-module)* updates the source module to read the target module. The target module can be ALL-UNNAMED to read all unnamed modules.

  • --add-exports module/package=target-module(,target-module)* updates the source module to export the package to the target module. This will add a qualified export of the package from the source module to the target module. The target module can be ALL-UNNAMED to export to all unnamed modules.

  • --add-opens module/package=target-module(,target-module)* updates the sources module to open the package to the target module. This will add a qualified open of the package from the source module to the target module.

  • --patch-module module=file(;file)* overrides or augments a module with classes and resources in JAR files or directories. --patch-module is useful when you’re running tests that may need to temporarily replace the contents of a module.

Now that I’ve introduced the basic concepts, let’s discuss these JDK tools.

javac

javac supports the following options related to modules. Meanings of these options have been explained in earlier sections of this chapter.

  • --module or -m

  • --module-path or -p

  • --module-source-path

  • --upgrade-module-path

  • --system

  • --module-version

  • --add-modules

  • --limit-modules

  • --add-exports

  • --add-reads

  • --patch-module

I’ll now use the modules discussed in the “Transitive Dependencies” as examples of how to use javac. Let’s say you have a directory that contains the source code of these three modules. Each module has its own subdirectory. You can compile a single module as shown here. Module C has no dependencies, so it can be compiled directly.

$ javac -d ∼/Downloads/modules_output/C C/**/*.java

To compile module B, you can use -p to provide the compiled module C since module B requires module C. You should also use -p when third-party libraries are required.

$ javac -d ∼/Downloads/modules_output/B -p ∼/Downloads/modules_output B/**/*.java

Since you have source code for all the modules, you can simply compile them all.

$ javac -d ∼/Downloads/modules_output --module-source-path . **/*.java

You can also compile a single module using -m. --module-source-path is required to specify the module source path when you’re using -m.

$ javac -d ∼/Downloads/modules_output --module-source-path . -m B

jlink

To run Java applications, you need to have JRE or JDK installed first. As I mentioned earlier, before Java 9, there was no easy way to customize the JRE or JDK to include only necessary contents. Even a simple “Hello World” application requires the full-sized JRE to run. After JDK is modularized, however, it’s possible to create your own Java runtime image that only contains the system modules required by the application, which can reduce the size of the JRE image.

You can use jlink to build a custom image. If you’re given a module demo.simple that has a class test.Main, you can use it to print out Hello World!. To build your image, create the modular JAR file demo.simple-1.0.0.jar and set the main class. Listing 2-21 shows the command you can use to create a custom image using jlink. The path <module_dir> contains the artifact of the module demo.simple. <JDK_PATH>/jmods is the path to JDK modules.

Listing 2-21. Using jlink to Create Custom Images
$ jlink -p <module_dir>:<JDK_PATH>/jmods 
  --add-modules demo.simple
  --output <output_dir>
  --launcher simple=demo.simple

The jlink tool creates a new runtime image in the output directory. You can run the executable file simple in the bin directory to run your application. The size of this customized runtime image on macOS is only 36.5MB. Figure 2-1 shows the content of the custom runtime image.

A459620_1_En_2_Fig1_HTML.jpg
Figure 2-1. Contents of the custom runtime image

Table 2-3 shows the options of jlink.

Table 2-3. Options of jlink

Option

Description

--module-path or -p

See “Module Paths”.

--add-modules

See “Root Modules”.

--output

The output directory.

--launcher

The launcher command to run a module or a main class in a module. If the module is created with --main-class to specify the main class, then using the module name is enough. Otherwise, the main class can be specified using <module>/<mainclass>.

--limit-modules

See “Limiting the Observable Modules”.

--bind-services

Performs the full service binding.

--compress=<0|1|2> or -c

Enables compression. Possible values are 0, 1, and 2. 0 means no compression, 1 means constant string sharing, and 2 means using ZIP compression.

--endian

Byte order of the generated image. Possible values can be little or big. The default value is native.

--no-header-files

Excludes header files in the image.

--no-man-pages

Excludes man pages.

-G or --strip-debug

Strip debug information.

--ignore-signing-information

Suppresses the fatal error when linking signed module JAR files. The signature-related files of the signed module JAR files are not copied to the generated image.

--suggest-providers [name, ...]

Suggests providers that implement the given service types.

--include-locales=langtag[,langtag]*

Includes the list of locales. The module jdk.localedata must be added when using this option.

--exclude-files=pattern-list

Excludes specified files.

--verbose or -v

Enable verbose tracing output.

A major limitation of jlink is that it doesn’t support automatic modules, which means third-party libraries cannot be linked using jlink. For example, when you’re trying to use jlink to create an image for the sample application, it gives following error.

Error: module-info.class not found for slf4j.api module

From this error message, you know that it requires the module-info.class for the module slf4j.api, but the SLF4J library has not been migrated to Java 9 yet, so you cannot include it in the image.

java

The java command supports the followin g options related to modules:

  • --module-path or -p

  • --upgrade-module-path

  • --add-modules

  • --limit-modules

  • --list-modules: Lists the observable modules.

  • --describe-module or -d: Describe the module.

  • --validate-modules: Validates all modules. Can be used to find conflicts and errors.

  • --show-module-resolution: Shows module resolution results during start.

  • --add-exports

  • --add-reads

  • --add-opens

  • --patch-module

For the sample application, after using the Maven assembly plugin to copy all libraries and module artifacts into one directory, you can start the application using following command:

$ java -p <path> -m io.vividcode.store.runtime/io.vividcode.store.runtime.Main

If you record the main class in the module artifact, you can simply use java -p <path> -m io.vividcode.store.runtime to run the application.

The --list-modules option is very useful when you’re debugging module resolution issues, because it can list all the observable modules. For example, you can use java --list-modules to list all JDK system modules. If you use -p to add module paths, the output also includes modules found in the module paths. The option --show-module-resolution is also very useful for solving module resolution issues.

jdeps

You can use the jdepstool to analyze the dependencie s of specified modules. The following command prints out the module descriptor of io.vividcode.store.runtime, the resulting module dependencies after analysis, and the graph after transition reduction; see Listing 2-22. It also identifies any unused qualified exports.

$ jdeps --module-path <path> --check io.vividcode.store.runtime
Listing 2-22. Output of jdeps Using --check
io.vividcode.store.runtime (file:///<path>/runtime-1.0.0-SNAPSHOT.jar)
  [Module descriptor]
  requires io.vividcode.store.filestore;
  requires io.vividcode.store.product.persistence;
  requires mandated java.base (@9);
  requires slf4j.simple;
  [Suggested module descriptor for io.vividcode.store.runtime]
  requires io.vividcode.store.common;
  requires io.vividcode.store.product;
  requires io.vividcode.store.product.persistence;
  requires mandated java.base;
  [Transitive reduced graph for io.vividcode.store.runtime]
  requires io.vividcode.store.product.persistence;
  requires mandated java.base;

To use the --check option, you need to know the module name first. If you only have a JAR file, you can use the --list-deps option or --list-reduced-deps.

$ jdeps --module-path <path> --list-deps <path>/runtime-1.0.0-SNAPSHOT.jar

Listing 2-23 shows the output of this command.

Listing 2-23. Output of jdeps Using --list-deps
io.vividcode.store.common
io.vividcode.store.product
io.vividcode.store.product.persistence
java.base

The difference between --list-deps and --list-reduced-deps is that the result of using the --list-reduced-deps option doesn’t include implicit read edges in the module graph.

$ jdeps --module-path <path> --list-reduced-deps <path>/runtime-1.0.0-SNAPSHOT.jar

Listing 2-24 shows the output of this command.

Listing 2-24. Output of jdeps Using --list-reduced-deps
io.vividcode.store.product.persistence

Another handy feature of jdeps is that it can generate graphviz ( http://graphviz.org/ ) DOT files to visualize module dependencies in graphs.

$ jdeps --module-path <module_path> 
    --dot-output <dot_output_path> -m io.vividcode.store.runtime

The output directory contains a DOT file summary.dot for all modules and a DOT file io.vividcode.store.runtime.dot for the module. Then you can turn DOT files into PNG files using the following command. Make sure you have graphviz installed first.

$ dot -Tpng <dot_output_path>/summary.dot -o <dot_png_output_path>/summary.png

You can also use jdeps to process multiple JAR files to generate the module dependencies diagram of the whole project. The generated DOT file summary.dot contains all the modules. For the sample application, use Maven to copy all module artifacts and third-party libraries into different directories. You can do this easily using Maven’s dependency plugin. Then you can generate the DOT files using following command. (<third_party-libs-path> is the path of third-party libraries, while <modules_path> is the path of module artifacts.)

$jdeps --module-path <third_party-libs-path> 
    --dot-output <dot_output_path>
  <modules_path>/*.jar

You can then turn the file summary.dot into a PNG file, see Figure 2-2 for the generated module dependencies diagram.

A459620_1_En_2_Fig2_HTML.jpg
Figure 2-2. Generated module dependencies diagram

jdeps can also generate the module declaration file module-info.java from JAR files. The options --generate-module-info and --generate-open-module can be used for normal modules and open modules, respectively. For example, you can use the following command to generate the file module-info.java for jackson-core-2.8.7.jar.

$ jdeps --generate-module-info <output_dir> <path>/jackson-core-2.8.7.jar

The generated module-info.java is shown in Listing 2-25. It simply exports all packages and adds service providers. You can use the generated module-info.java file as a starting point when migrating legacy JAR files to Java 9 modules.

Listing 2-25. Generated module-info.java of jackson-core-2.8.7.jar
module jackson.core {
  exports com.fasterxml.jackson.core;
  exports com.fasterxml.jackson.core.base;
  exports com.fasterxml.jackson.core.filter;
  exports com.fasterxml.jackson.core.format;
  exports com.fasterxml.jackson.core.io;
  exports com.fasterxml.jackson.core.json;
  exports com.fasterxml.jackson.core.sym;
  exports com.fasterxml.jackson.core.type;
  exports com.fasterxml.jackson.core.util;


  provides com.fasterxml.jackson.core.JsonFactory with
    com.fasterxml.jackson.core.JsonFactory;


}

Module Java API

When the JVM starts, it resolves the application’s main module against the observable modules. The result of this resolution process is a readability graph, which is a directed graph with nodes representing resolved modules and edges representing the readability among the modules. Then the JVM creates a module layer, which consists of runtime modules defined from those resolved modules. The module system has Java API for applications to interact with it.

ModuleFinder

Let’s start with the module resolution process. To resolve a module, you first need to find the module definition. The interface ModuleFinder is responsible for locating modules. There are three different ways to create instances of ModuleFinder with static methods in ModuleFinder; see Table 2-4.

Table 2-4. Static Methods of ModuleFinder

Method

Description

ofSystem()

Creates a ModuleFinder that locates system modules

of(Path… entries)

Creates a ModuleFinder that locates modules on the file system by searching a sequence of paths specified by entries

compose(ModuleFinder... finders)

Creates a ModuleFinder by composing a sequence of zero or more ModuleFinders

Once a ModuleFinder is created, you can use its method Optional<ModuleReference> find(String name) to find a module by name. The class ModuleReference is a reference to the module’s contents. The method Set<ModuleReference> findAll() returns a set that contains all ModuleReferences that can be located by this ModuleFinder object. Normally you don’t need to use ModuleFinders directly, but you should use them to create configurations for readability graphs.

The modules located by ModuleFinders ar e r epresented as ModuleReference. Table 2-5 shows the methods of ModuleReference.

Table 2-5. Methods of ModuleReference

Method

Description

ModuleDescriptor descriptor()

Returns the ModuleDescriptor that describes the module

Optional<URI> location()

Returns the location of the module’s content as a URI

ModuleReader open()

Opens the module’s content for reading

Listing 2-26 shows a helper class to create ModuleFinder and ModuleReference objects. The ModuleFinder object finds modules in the resource path /modules. This path contains module artifacts of the sample application. The method getModuleReference() uses the ModuleFinder to find a ModuleReference by name. Since I am sure that the module to find must exist, here I simply use the method get() of the returned Optional<ModuleReference> to retrieve the ModuleReference directly.

Listing 2-26. Support Class for Module Tests
public class ModuleTestSupport {

  public static final String MODULE_NAME = "io.vividcode.store.common";

  public static ModuleFinder getModuleFinder() throws URISyntaxException {
    return ModuleFinder.of(
      Paths.get(
        ModuleDescriptorTest.class.getResource("/modules").toURI()));
  }


  public static ModuleReference getModuleReference() throws URISyntaxException {
    return getModuleFinder().find(MODULE_NAME).get();
  }
}

Listing 2-27 is the test of ModuleFinder. Here I verify the results of methods find() and findAll().

Listing 2-27. ModuleFinder Test
public class ModuleFinderTest {

  @Test
  public void testFindModule() throws Exception {
    final ModuleFinder moduleFinder = ModuleTestSupport.getModuleFinder();
    assertTrue(moduleFinder.find(ModuleTestSupport.MODULE_NAME).isPresent());
    final Set<ModuleReference> allModules = moduleFinder.findAll();
    assertEquals(4, allModules.size());
  }
}

ModuleDescriptor

From the ModuleReference, you can get the ModuleDescriptorthat describes modules. Table 2-6 shows the methods of ModuleDescriptor.

Table 2-6. Methods of ModuleDescriptor

Method

Description

Set<ModuleDescriptor.Exports> exports()

Returns the set of ModuleDescriptor.Exports that represent the exported packages

Set<ModuleDescriptor.Opens> opens()

Returns the set of ModuleDescriptor.Opens that represent the open packages.

Set<ModuleDescriptor.Requires> requires()

Returns the set of ModuleDescriptor.Requires that represent the module’s dependencies

Set<ModuleDescriptor.Provides> provides()

Returns the set of ModuleDescriptor.Provides that represent services provided by this module

Set<String> uses()

Returns the set of services this module uses

String name()

Returns the module’s name

Optional<String> mainClass()

Returns the module’s main class

Set<ModuleDescriptor.Modifier> modifiers()

Returns the set of ModuleDescriptor.Modifier that represent module modifiers

boolean isAutomatic()

Checks if the module is automatic

boolean isOpen()

Checks if the module is open

Optional<ModuleDescriptor.Version> version()

Returns the module’s version

In Listing 2-28, I use a test to verify various information retrieved from the ModuleDescriptor.

Listing 2-28. ModuleDescriptor Test
public class ModuleDescriptorTest {

  @Test
  public void testModuleDescriptor() throws Exception {
    final ModuleReference reference = ModuleTestSupport.getModuleReference();
    assertNotNull(reference);
    final ModuleDescriptor descriptor = reference.descriptor();
    assertEquals(ModuleTestSupport.MODULE_NAME, descriptor.name());
    assertFalse(descriptor.isAutomatic());
    assertFalse(descriptor.isOpen());
    assertEquals(1, descriptor.exports().size());
    assertEquals(2, descriptor.packages().size());
    assertTrue(descriptor.requires().stream().map(Requires::name)
      .anyMatch(Predicate.isEqual("jackson.core")));
    assertTrue(descriptor.uses().isEmpty());
    assertTrue(descriptor.provides().isEmpty());
  }
}

The methods in Table 2-6 are used to query information of existing ModuleDescriptor objects. To create ModuleDescriptor objects, you can either use the builder class ModuleDescriptor.Builder, or read from the binary form of module descriptors. The static methods newModule(String name), newOpenModule(String name), and newAutomaticModule(String name) can create ModuleDescriptor.Builder objects to build a normal, open, and automatic module, respectively. ModuleDescriptor.Builder has different methods to programmatically configure various components of a module descriptor. Listing 2-29 shows a simple test of ModuleDescriptor.Builder.

The static method read() has different overloads to read the binary form of a module declaration from an InputStream or a ByteBuffer. It’s possible that the binary form doesn’t include a set of packages in the module, if it doesn’t, you can pass an extra argument of type Supplier<Set<String>> to provide the packages.

Listing 2-29. ModuleDescriptor.Builder Test
@Test
public void testModuleDescriptorBuilder() {
  final ModuleDescriptor descriptor = ModuleDescriptor.newModule("demo")
      .exports("demo.api")
      .exports("demo.common")
      .mainClass("demo.Main")
      .version("1.0.0")
      .build();
  assertEquals(2, descriptor.exports().size());
  assertTrue(descriptor.mainClass().isPresent());
  assertEquals("demo.Main", descriptor.mainClass().get());
  assertTrue(descriptor.version().isPresent());
  assertEquals("1.0.0", descriptor.version().get().toString());
}

ModuleReader returned by ModuleReference.open() reads resources in a module. A resource is identified by a path string separated by /, for example, com/mycompany/mymodule/Main.class. Table 2-7 shows the methods of ModuleReader.

Table 2-7. Methods of ModuleReader

Method

Description

Stream<String> list()

Lists the names of all resources in the module

Optional<URI> find(String name)

Finds a resource by its name

Optional<InputStream> open(String name)

Opens a resource for reading

Optional<ByteBuffer> read(String name)

Reads a resource’s contents as a ByteBuffer

void release(ByteBuffer bb)

Releases a ByteBuffer

void close()

Closes this ModuleReader

In Listing 2-30, I use ModuleReader to read the content of module-info.class into a ByteBuffer and then create a new ModuleDescriptor from this ByteBuffer.

Listing 2-30. ModuleReader Test
public class ModuleReaderTest {

  @Test
  public void testModuleReader() throws Exception {
    final ModuleReference reference = ModuleTestSupport.getModuleReference();
    assertNotNull(reference);
    final ModuleReader reader = reference.open();
    final Optional<ByteBuffer> byteBuffer = reader.read("module-info.class");
    assertTrue(byteBuffer.isPresent());
    final ModuleDescriptor descriptor = ModuleDescriptor.read(byteBuffer.get());
    assertEquals(ModuleTestSupport.MODULE_NAME, descriptor.name());
  }
}

Configuration

The readability graph is represented as an object of the class java.lang.module.Configuration. A configuration may have parent configurations. Configurations can be organized in a hierarchy. Configuration has methods to create and query the readability graph.

Configurations are created using the methods resolve() and resolveAndBind(). Both methods return a new Configuration object representing the result of module resolution. Configuration also has static versions of these two methods. For static methods, you need to provide the parameter of type List<Configuration> as the parent configurations of the created Configuration. For instance methods, the current Configuration is the only parent configuration of the created Configuration. The following are the methods you need to create Configurations:

  • Configuration resolve(ModuleFinder before, ModuleFinder after, Collection<String> roots)

  • Configuration resolveAndBind(ModuleFinder before, ModuleFinder after, Collection<String> roots)

  • static Configuration resolve(ModuleFinder before, List<Configuration> parents, ModuleFinder after, Collection<String> roots)

  • static Configuration resolveAndBind(ModuleFinder before, List<Configuration> parents, ModuleFinder after, Collection<String> roots)

The method resolveAndBind() takes the same parameters as the method resolve(). The difference is that the resolved modules of resolveAndBind() also includes modules introduced by service dependencies.

The following are the possible parameters of the preceding methods:

  • roots: The list of root module names.

  • parents: The list of parent configurations.

  • before and after: Two ModuleFinders to locate modules. The ModuleFinder before is used to locate root modules. If a module cannot be found, parent configurations are used to locate it using the method findModule(). If the module still cannot be found, then the ModuleFinder after is used to locate it. Transitive dependencies are located following the same search order.

When a root module or transitive dependency is located in a parent configuration, the resolution process ends for this module and the module is not included in the resulting configuration.

Listing 2-31 shows the example of creating a new Configuration by finding modules in the given path.

Listing 2-31. Create New Configuration Objects Using resolve()
public Configuration resolve(final Path path) {
  return Configuration.resolve(
    ModuleFinder.of(path),
    List.of(Configuration.empty()),
    ModuleFinder.ofSystem(),
    List.of("io.vividcode.store.runtime")
  );
}

The module resolution process may fail for various reasons. If a root module or a direct or transitive dependency, is not found, or any error occurs when trying to find a module, the methods resolve() and resolveAndBind() throw the FindException. After all modules have been found, the readability graph is computed, and the consistency of module exports and service use is checked. Consistency checks include checking for cyclic module dependencies, duplicate module reads, duplicate package exports, and invalid service use. ResolutionException is thrown when any consistency check fails.

Table 2-8 shows other methods of Configuration.

Table 2-8. Methods of Configuration

Method

Description

static Configuration empty()

Returns the empty configuration

List<Configuration> parents()

Returns the list of this configuration’s parent configurations, in the search order

Optional<ResolvedModule> findModule(String name)

Finds a resolved module by name

Set<ResolvedModule> modules()

Returns the set of all resolved modules in this configuration

For each ResolvedModule, the reads() method returns a set of ResolvedModules that it reads; see Table 2-9 for other methods.

Table 2-9. Methods of ResolvedModule

Method

Description

Configuration configuration()

Returns the Configuration that this resolved module belongs to

String name()

Returns the module name

Set<ResolvedModule> reads()

Returns the set of ResolvedModules that this resolved module reads

ModuleReference reference()

Returns the ModuleReference to this module’s content

Listing 2-32 shows how to print out the readability graph. The method sortedModules() sorts ResolvedModules by name. In the method printLayer(), I get all ResolvedModules in the configuration and print out their names. For each ResolvedModule, I also print out the ResolvedModules it reads.

Listing 2-32. Printing Out the Readability Graph
public class ConfigurationPrinter {

  public void printLayer(final Configuration configuration) {
    sortedModules(configuration.modules()).forEach(module -> {
      System.out.println(module.name());
      printModule(module);
      System.out.println("");
    });
  }


  private void printModule(final ResolvedModule module) {
    sortedModules(module.reads())
      .forEach(m -> System.out.println("|--" + m.name()));
  }


  private List<ResolvedModule> sortedModules(final Set<ResolvedModule> modules) {
    return modules
      .stream()
      .sorted(Comparator.comparing(ResolvedModule::name))
      .collect(Collectors.toList());
  }


  public static void main(final String[] args) {
    final Configuration configuration = Configuration.resolve(
      ModuleFinder.of(Paths.get(args[0])),
      List.of(Configuration.empty()),
      ModuleFinder.ofSystem(),
      List.of("io.vividcode.store.runtime")
    );
    new ConfigurationPrinter().printLayer(configuration);
  }
}

Listing 2-33 shows the output of Listing 2-32. It shows all modules of the sample application.

Listing 2-33. Output of the Readability Graph
io.vividcode.store.common
|--guava
|--jackson.annotations
|--jackson.core
|--jackson.databind
|--java.base
|--slf4j.api
|--slf4j.simple


io.vividcode.store.common.persistence
|--guava
|--io.vividcode.store.common
|--jackson.annotations
|--jackson.core
|--jackson.databind
|--java.base
|--slf4j.api
|--slf4j.simple


io.vividcode.store.filestore
|--guava
|--io.vividcode.store.common
|--io.vividcode.store.common.persistence
|--jackson.annotations
|--jackson.core
|--jackson.databind
|--java.base
|--slf4j.api
|--slf4j.simple


io.vividcode.store.product
|--io.vividcode.store.common
|--java.base


io.vividcode.store.product.persistence
|--io.vividcode.store.common
|--io.vividcode.store.common.persistence
|--io.vividcode.store.product
|--java.base


io.vividcode.store.runtime
|--guava
|--io.vividcode.store.common
|--io.vividcode.store.common.persistence
|--io.vividcode.store.filestore
|--io.vividcode.store.product
|--io.vividcode.store.product.persistence
|--jackson.annotations
|--jackson.core
|--jackson.databind
|--java.base
|--slf4j.api
|--slf4j.simple

The Module Layers

Module layers are used to organize modules. Module layers are represented as the class java.lang.ModuleLayer.

A layer is created from the readability graph in a Configuration object by mapping each resolved module to a class loader that is responsible for loading types defined in this module. The JVM has at least one nonempty layer, the boot layer, which is created when the JVM starts. Most applications will only use this boot layer. Additional layers can be created to support advanced use cases. A layer can have multiple parent layers. Modules in a layer can also read modules in the layer’s parent layers. When a layer is created, a java.lang.Module object is created for each ResolvedModule in the configuration.

The static method boot() of ModuleLayer returns the boot layer. There are three different ways to create a new layer from an existing ModuleLayer; see Table 2-10. The existing ModuleLayer will be the parent layer of the newly created layer.

Table 2-10. Methods for Creating New ModuleLayers

Method

Description

defineModules(Configuration cf, Function<String, ClassLoader> clf)

Creates a new ModuleLayer from the Configuration. For each module, the function returns the mapped ClassLoader.

defineModulesWithOneLoader(Configuration cf, ClassLoader parentLoader)

Creates a new ModuleLayer from the Configuration. All modules use the same ClassLoader with parentLoader as the parent class loader.

defineModulesWithManyLoaders(Configuration cf, ClassLoader parentLoader)

Creates a new ModuleLayer from the Configuration. Each module has its own ClassLoader with parentLoader as the parent class loader.

ModuleLayer also has the static methods defineModules(), defineModulesWithOneLoader(), and defineModulesWithManyLoaders() to create new layers. These methods require to provide the list of parent layers. Compared to those instance methods, these static methods return ModuleLayer.Controller instead of ModuleLayer. ModuleLayer.Controller has methods for modifying the modules in the layer; see Table 2-11.

Table 2-11. Methods of ModuleLayer.Controller

Method

Description

addReads(Module source, Module target)

Updates the module source to read the module target

ModuleLayer.Controller addExports(Module source, String pn, Module target)

Updates the module source to export the package pn to the module target

ModuleLayer.Controller addOpens(Module source, String pn, Module target)

Updates the module source to open the package pn to the module target

ModuleLayer layer()

Returns the ModuleLayer object

ModuleLayer also has other methods; see Table 2-12.

Table 2-12. Methods of ModuleLayer

Method

Description

static ModuleLayer empty()

Returns the empty layer.

Configuration configuration()

Returns the configuration for this layer.

List<ModuleLayer> parents()

Returns the list of this layer’s parents.

Set<Module> modules()

Returns the set of modules in this layer.

Optional<Module> findModule(String name)

Returns the module with the given name. Parent layers are also searched recursively until the module is found.

ClassLoader findLoader(String name)

Returns the ClassLoader of the module with the given name. Parent layers are also searched in the same way as in findModule().

The class Module represents a runtime module. It can be used to retrieve information about the module and modify it when required. Table 2-13 shows methods of Module.

Table 2-13. Methods of Module

Method

Description

String getName()

Returns the name of this module, null for an unnamed module

Set<String> getPackages()

Returns the set of package names in this module

ModuleDescriptor getDescriptor()

Returns the ModuleDescriptor object that describes this module, null for an unnamed module

boolean isNamed()

Checks if the module is named

boolean isExported(String pn)

Checks if the module exports a package pn unconditionally

boolean isExported(String pn, Module other)

Checks if the module exports a package pn to the given module other

boolean isOpen(String pn)

Checks if the module opens a package pn unconditionally

boolean isOpen(String pn, Module other)

Checks if the module opens a package pn to the given module other

boolean canRead(Module other)

Checks if the module reads the given module other

boolean canUse(Class<?> service)

Checks if the module uses the given service class

Module addExports(String pn, Module other)

Updates this module to export the given package pn to the given module other

Module addOpens(String pn, Module other)

Updates this module to open the given package pn to the given module other

Module addUses(Class<?> service)

Updates this module to add a service dependency on the given service type

Module addReads(Module other)

Updates this module to read the given module other

ClassLoader getClassLoader()

Gets the class loader of this module

ModuleLayer getLayer()

Returns the module layer that contains this module

InputStream getResourceAsStream(String name)

Returns an input stream to read a resource in the module

When using getResourceAsStream() to read the resources in a module, the resource may be encapsulated so that it cannot be located by code in other modules. Class files are not encapsulated. For other resources, the package name is derived from the resource name first. If the package name is in the module, that is, it is in the set of package names returned by getPackages(), then the caller code needs to be in a module that the package is open to.

Now I’ll use an example to show you how to use ModuleLayers to have multiple versions of the same module coexist in the same application. In the module demo, I have a simple Java Version class that has the static method getVersion() to return the version string; see Listing 2-34. The version string is 1.0.0 for the module version 1.0.0 and is updated to 2.0.0 for the module version 2.0.0.

Listing 2-34. Version Class in the Module demo
package io.vividcode.demo.version;

public class Version {
  public static String getVersion() {
    return "1.0.0";
  }
}

I have another runtime module that requires the module demo. Listing 2-35 shows the Main class that outputs the version string.

Listing 2-35. Main Class in the Module runtime
package io.vividcode.demo.runtime;

import io.vividcode.demo.version.Version;

public class Main {
  public static void main(final String[] args) {
    System.out.println(Version.getVersion());
  }
}

The module runtime and two versions of the module demo are packaged as JAR files. If you put these three JAR files in the same directory dist and run the module runtime using following command, you’ll find out that the command cannot run.

$ java -p dist -m runtime

The command fails with following error. This is because two versions of the same module demo appear in the same directory of the module path.

Error occurred during initialization of boot layer
java.lang.module.FindException: Two versions of module demo found in dist (demo-2.0.0.jar and demo-1.0.0.jar)

If you put demo-1.0.0.jar and demo-2.0.0.jar into separate directories and update the module path as shown here, the output is 1.0.0 because the demo-1.0.0.jar is found first.

$ java -p dist:dist/v1:dist/v2 -m runtime

If you change the order of dist/v1 and dist/v2 in the module path as shown here, the output is 2.0.0, because demo-2.0.0.jar is found first.

$ java -p dist:dist/v2:dist/v1 -m runtime

If you want to have both versions running in the same JVM, you can use OSGi or create custom class loaders. In Java 9, you can use ModuleLayers. Listing 2-35 shows the updated version of the Main class using ModuleLayers. In the method createLayer(), path is the path of the directory that contains the artifact of module demo. In this code, I create a Configuration that uses the ModuleFinder to only search in this path. The parent Configuration comes from the boot layer. I then use defineModulesWithOneLoader() to create the module layer from the Configuration and use the system class loader as the parent class loader for the only ClassLoader object. The created ModuleLayer uses the boot layer as the parent.

Once I have created the module layer, the method showVersion() uses the method findLoader() to find the ClassLoader for the module and the use reflections API to load the class and invoke the method getVersion(); see Listing 2-36. If you run the class Main, you can see that both version strings are displayed.

Listing 2-36. Using ModuleLayer to Load Modules
import java.lang.module.Configuration;
import java.lang.module.ModuleFinder;
import java.lang.reflect.Method;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Set;


public class Main {
  private static final String MODULE_NAME = "demo";
  private static final String CLASS_NAME = "io.vividcode.demo.version.Version";
  private static final String METHOD_NAME = "getVersion";


  public static void main(final String[] args) {
    final Main main = new Main();
    main.load(Paths.get("dist", "v1"));
    main.load(Paths.get("dist", "v2"));
  }


  public void load(final Path path) {
    showVersion(createLayer(path));
  }


  private void showVersion(final ModuleLayer moduleLayer) {
    try {
      final Class<?> clazz = moduleLayer.findLoader(MODULE_NAME)
          .loadClass(CLASS_NAME);
      final Method method = clazz.getDeclaredMethod(METHOD_NAME);
      System.out.println(method.invoke(null));
    } catch (final Exception e) {
      e.printStackTrace();
    }
  }


  private ModuleLayer createLayer(final Path path) {
    final ModuleLayer parent = ModuleLayer.boot();
    final Configuration configuration = parent.configuration().resolve(
        ModuleFinder.of(path),
        ModuleFinder.of(),
        Set.of(MODULE_NAME)
    );
    return parent.defineModulesWithOneLoader(configuration,
        ClassLoader.getSystemClassLoader());
  }
}

Class Loaders

OSGi uses a complicated class loader hierarchy to allow different versions of the same bundle to coexist at runtime. JPMS uses a simple class loading strategy. A module has its own class loader that is responsible for loading all types in this module. A class loader can load types from one module or from many modules. All of the types in one module should be loaded by the same class loader.

Before Java 9, Java runtime has three built-in class loaders:

  • Bootstrap class loader: JVM built-in class loader, typically represented as null. It has no parent.

  • Extension class loader: Loads classes from the extension directory. It’s a result of the extension mechanism introduced in JDK 1.2. Its parent is the bootstrap class loader.

  • System or application class loader: Loads classes from the application’s class path. Its parent is the extension class loader.

The code in Listing 2-37 outputs the class loader hierarchy.

Listing 2-37. Outputing the Class Loader Hierarchy
ClassLoader classLoader = ClassLoaderMain.class.getClassLoader();
while (classLoader != null) {
  System.out.println(classLoader);
  classLoader = classLoader.getParent();
}

When you’re running the code in Listing 2-37 on Java 8, the following output is shown. AppClassLoader and ExtClassLoader are the classes of the system class loader and the extension class loader, respectively. The bootstrap class loader is represented as null, so it’s not shown.

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@60e53b93

In Java 9, the extension mechanism has been superseded by the upgrade module path. The extension class loader is still kept for backward compatibility, but it is renamed to platform class loader. It can be retrieved using the method getPlatformClassLoader() of ClassLoader. The following are the built-in class loaders in Java 9:

  • Bootstrap class loader: Defines core Java SE and JDK modules

  • Platform class loader: Defines selected Java SE and JDK modules

  • System or application class loader: Defines classes on the class path and modules in the module path

In Java 9, the platform class loader and system class loader are no longer instances of URLClassLoader. This affects a popular trick ( https://stackoverflow.com/a/7884406 ) to dynamically add new entries to the search path of the system class loader. As shown in Listing 2-38, this hack converts the system class loader to URLClassLoader and invokes its method addURL(). This no longer works in Java 9 as the cast to URLClassLoader fails.

Listing 2-38. Adding a Path to URLClassLoader
public static void addPath(String s) throws Exception {
  File f = new File(s);
  URL u = f.toURL();
  URLClassLoader urlClassLoader = (URLClassLoader)
      ClassLoader.getSystemClassLoader();
  Class urlClass = URLClassLoader.class;
  Method method = urlClass.getDeclaredMethod("addURL", new Class[]{URL.class});
  method.setAccessible(true);
  method.invoke(urlClassLoader, new Object[]{u});
}

Every class loader has its own unnamed module that can be retrieved using the method getUnnamedModule() of ClassLoader. If a class loader loads a type that is not defined in a named module, this type is considered to be in the class loader’s unnamed module. The unnamed module we discussed earlier is actually the unnamed module of the application class loader.

In Java 9, class loaders have names. The name is specified in the constructor and can be retrieved using the method getName(). The name of the platform class loader is platform, whereas the name of application class loader is app. The new class loader name can be useful when you’re debugging class loader–related issues.

Listing 2-39. Testing the Class Loader Names
@Test
public void testClassLoaderName() {
  ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
  final List<String> names = Lists.newArrayList();
  while (classLoader != null) {
    names.add(classLoader.getName());
    classLoader = classLoader.getParent();
  }
  assertEquals(2, names.size());
  assertEquals("app", names.get(0));
  assertEquals("platform", names.get(1));
}

The new method Class<?> findClass(String moduleName, String name) finds the class with a given binary name in a module defined to this class loader. If the module name is null, it finds the class in the unnamed module of this class loader. If the class cannot be found, this method returns null instead of throwing a ClassNotFoundException. The new method URL findResource(String moduleName, String name) returns a URL to the resource with a given name in a module that is defined for this class loader. The method Module.getResourceAsStream() actually uses this method of the module’s class loader to get the URL first and then open the input stream. The methods findClass() and findResource() are both protected and should be overridden by class loader implementations.

ClassLoader already has the method Enumeration<URL> getResources(String name) to find all resources with the given name. The new method Stream<URL> resources(String name) has the same functionality, but it returns a Stream<URL>.

Java 9 adds a new restriction when accessing the resources that matches the access control rules enforced by module declaration. Resources in the named modules are subject to the encapsulation rules specified in the method Module.getResourceAsStream(). For nonclass resources, the related methods in ClassLoader can only find resources in packages when the package is open unconditionally.

For example, the module io.vividcode.store.common has a properties file application.properties in the directory config. The package name of this resource is config. If the package config is not declared as open in the module declaration, it cannot be located using those resource-related methods.

In Listing 2-40, I get the Module object of the module io.vividcode.store.common first, then I get its class loader and use the method resources() to list all resources with name config/application.properties. Here the package config must be open, otherwise the method resources() returns an empty stream.

Listing 2-40. Testing ClassLoader.resources()
public class ResourceTest {

  @Test
  public void testResources() throws URISyntaxException {
    final Optional<Module> moduleOpt = ModuleTestSupport.getModule();
    assertTrue(moduleOpt.isPresent());
    final Module module = moduleOpt.get();
    assertTrue(module.isOpen("config"));
    assertTrue(module
        .getClassLoader()
        .resources("config/application.properties")
        .count() > 0
    );
  }
}

The new method Package[] getDefinedPackages() returns all of the Packages defined by this class loader, while Package getDefinedPackage(String name) returns the Package of the given name defined by this class loader. In Listing 2-41, the class loader to test is actually the system class loader, so it defines application packages, but not packages like java.lang.

Listing 2-41. Testing ClassLoader.getDefinedPackages()
@Test
public void testGetDefinedPackages() {
  final ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
  final Package[] packages = classLoader.getDefinedPackages();
  assertTrue(Stream.of(packages)
      .map(Package::getName)
      .noneMatch(Predicate.isEqual("java.lang")));
  assertTrue(Stream.of(packages)
      .map(Package::getName)
      .anyMatch(Predicate.isEqual("io.vividcode.feature9.module")));
  assertNull(classLoader.getDefinedPackage("java.lang"));
  assertNotNull(
    classLoader.getDefinedPackage("io.vividcode.feature9.module"));
}

Class

With the introduction of modules, Class has new methods related to modules. Class<?> forName(Module module, String name) returns the Class with the given binary name in the given module. This method delegates to the module’s class loader for class loading. Similar to the method findClass() of ClassLoader, forName() returns null on failure rather than throwing a ClassNotFoundException. Since a class loader can define classes for multiple modules, it’s possible that the defined class actually comes from a different module. In this case, the method also returns null after the class is loaded.

Since every type is now in a module, Class has a new method, getModule(), to return the Module object representing the module it belongs to. If this class represents an array type, then this method returns the Module object for the element type. If this class represents a primitive type or void, then the Module object for the module java.base is returned. If this class is in an unnamed module, then the result of getUnnamedModule() of the class loader for this class is returned. Listing 2-42 shows the test related to Class.getModule().

Listing 2-42. Testing Class.getModule()
@Test
public void testGetModule() {
  assertEquals("java.sql", Driver.class.getModule().getName());
  assertEquals("java.base", String[].class.getModule().getName());
  assertEquals("java.base", int.class.getModule().getName());
  assertEquals("java.base", void.class.getModule().getName());
}

The new method String getPackageName() returns the fully qualified package name of a class. If this class represents an array type, then this method returns the package name of the element type. If this class represents a primitive type or void, then the package name java.lang is returned. Otherwise, the package name is derived from the class name returned by Class.getName() by keeping the characters before the last dot (.). Listing 2-43 shows the test related to Class.getPackageName().

Listing 2-43. Testing Class.getPackageName()
@Test
public void testGetPackageName() {
  assertEquals("java.sql", Driver.class.getPackageName());
  assertEquals("java.lang", String[].class.getPackageName());
  assertEquals("java.lang", int.class.getPackageName());
  assertEquals("java.lang", void.class.getPackageName());
}

Reflection

In the previous section, I showed you how to use ServiceLoader to load provider classes of service interfaces. Many frameworks use the similar pattern and reflection API to dynamically load classes by name and instantiate them. A client can provide the class name to the framework through configuration. The framework uses Class.forName() to load the class and instantiate it using the method newInstance() of the Class object.

This kind of pattern may have issues in the module system. The actual class may be loaded from the unnamed module or another named module. If the actual class is loaded from the unnamed module, then the instantiation fails because the framework’s module cannot read the unnamed module. If the actual class is loaded from another named module, it’s not possible for the framework’s module to know of the existence of client modules and declare dependencies upon them, which actually reverses the dependencies. To make this work, the reflection API has an implicit convention in which it assumes any code that reflects a type is in a module that reads the module that defines this type. The reflection API has the access to the type.

In Listing 2-44, I use the reflection API in a named module to load the Guava class com.google.common.eventbus.EventBus and instantiate a new instance. The Guava library is put into the class path, so it’s in the unnamed module. The output of clazz.getModule() is something similar to unnamed module @1623b78d, which confirms that the class is in the unnamed module. The instantiation is successful and you can see output like EventBus{default}.

Listing 2-44. Using the Reflection API to Instantiate an Instance
try {
  final Class<?> clazz = Class.forName("com.google.common.eventbus.EventBus");
  System.out.println(clazz.getModule());
  final Object instance = clazz.getDeclaredConstructor().newInstance();
  System.out.println(instance);
} catch (final Exception e) {
  e.printStackTrace();
}
Note

The method Class.newInstance() is deprecated in Java 9. In Listing 2-44, I use clazz.getDeclaredConstructor().newInstance() to get the constructor and then use it to instantiate new instances.

Automatic Module Names

I mentioned names of automatic modules earlier. If no attribute Automatic-Module-Name is found in the manifest of a JAR file, the module name is derived from the file name. The module name is determined using following steps.

  1. Remove the suffix .jar.

  2. Attempt to match the file name to the regular expression pattern -(\d+(\.|$)). If the pattern matches, then the substring before the matching position is treated as the candidate of module name, while the substring after the dash (-) is parsed as the version. If the version cannot be parsed, then it’s ignored. If the pattern doesn’t match, the whole file name is treated as the candidate of module name.

  3. Clean up the candidate of module name to create a valid module name. All nonalphanumeric characters ([^A-Za-z0-9]) in the module name are replaced with a dot (.), all repeating dots are replaced with one dot, and all leading and trailing dots are removed.

If you are interested in knowing the actual implementation of these steps, check the method ModuleDescriptor deriveModuleDescriptor(JarFile jf) of the class jdk.internal.module.ModulePath in the module java.base. The parse of the version string is done using the static method Version parse(String v) of ModuleDescriptor.Version. The code in Listing 2-45 also implements the same algorithm.

Table 2-14 shows some examples of deriving automatic module names from the JAR file names.

Table 2-14. Example of Deriving Automatic Module Names for JAR File Names

File Name

Module Name

Version

mylib.jar

mylib

null

slf4j-api-1.7.25.jar

slf4j.api

1.7.25

hibernate-jpa-2.1-api-1.0.0.Final.jar

hibernate.jpa

2.1-api-1.0.0.Final

spring-context-support-4.3.6.RELEASE.jar

spring.context.support

4.3.6.RELEASE

Listing 2-45 shows the code to derive the automatic module names shown in Table 2-14.

Listing 2-45. Deriving Automatic Module Names
public class DeriveAutomaticModuleName {

  static final Pattern DASH_VERSION = Pattern.compile("-(\d+(\.|$))");
  static final Pattern NON_ALPHANUM = Pattern.compile("[^A-Za-z0-9]");
  static final Pattern REPEATING_DOTS = Pattern.compile("(\.)(\1)+");
  static final Pattern LEADING_DOTS = Pattern.compile("^\.");
  static final Pattern TRAILING_DOTS = Pattern.compile("\.$");


  public Tuple2<String, String> deriveModuleName(final String fileName) {
    Objects.requireNonNull(fileName);
    String name = fileName;
    String version = null;
    if (fileName.endsWith(".jar")) {
      name = fileName.substring(0, fileName.length() - 4);
    }


    final Matcher matcher = DASH_VERSION.matcher(name);
    if (matcher.find()) {
      final int start = matcher.start();


      try {
        final String tail = name.substring(start + 1);
        ModuleDescriptor.Version.parse(tail);
        version = tail;
      } catch (final IllegalArgumentException ignore) {
      }


      name = name.substring(0, start);
    }
    return Tuple.of(cleanModuleName(name), version);
  }


  public void displayModuleName(final String fileName) {
    final Tuple2<String, String> result = deriveModuleName(fileName);
    System.out.printf("%s => %s [%s]%n", fileName, result._1, result._2);
  }


  private String cleanModuleName(String mn) {
    // replace non-alphanumeric
    mn = NON_ALPHANUM.matcher(mn).replaceAll(".");


    // collapse repeating dots
    mn = REPEATING_DOTS.matcher(mn).replaceAll(".");


    // drop leading dots
    if (mn.length() > 0 && mn.charAt(0) == '.') {
      mn = LEADING_DOTS.matcher(mn).replaceAll("");
    }


    // drop trailing dots
    final int len = mn.length();
    if (len > 0 && mn.charAt(len - 1) == '.') {
      mn = TRAILING_DOTS.matcher(mn).replaceAll("");
    }


    return mn;
  }


  public static void main(final String[] args) {
    final DeriveAutomaticModuleName moduleName = new DeriveAutomaticModuleName();
    moduleName.displayModuleName("mylib.jar");
    moduleName.displayModuleName("slf4j-api-1.7.25.jar");
    moduleName.displayModuleName("hibernate-jpa-2.1-api-1.0.0.Final.jar");
    moduleName.displayModuleName("spring-context-support-4.3.6.RELEASE.jar");
  }
}

If the module name is specified using the manifest attribute Automatic-Module-Name, then the name is used as is. If the specified module name is invalid, then a FindException is thrown when the module is loaded.

Module Artifacts

Modules are packaged as module artifacts. There are two types of module artifacts, JAR files and JMOD files.

JAR Files

A modular JAR file is just a JAR file with the file module-info.class in its root directory, so you can use the existing jar tool to create modular JAR files. The jar tool also adds new options to insert additional information into module descriptors.

  • -e, --main-class=CLASSNAME records the entry point class in the file module-info.class. This is actually an old option that records the main class in the manifest file.

  • --module-version=VERSION records the VERSION in the module-info.class file as the module’s version.

  • --hash-modules=PATTERN records hashes of modules that depend upon this module in the module-info.class file. Hashes are only recorded for modules whose names match the regular expression specified with PATTERN.

  • -d, --describe-module prints the module descriptor or the name of an automatic module.

You can use the command in Listing 2-46 to create a modular JAR file for the module io.vividcode.store.runtime. Here I specified the module version and main class.

Listing 2-46. Using jar to Create Module Artifacts
$ jar --create --file target/runtime-1.0.0.jar 
  --main-class io.vividcode.store.runtime.Main
  --module-version 1.0.0
  -C target/classes .

Then you can print out the details of the created JAR file.

$ jar -d -f target/runtime.jar

Listing 2-47 shows the output of this command.

Listing 2-47. Output of jar -d
module [email protected] (module-info.class)
  requires io.vividcode.store.filestore
  requires io.vividcode.store.product.persistence
  requires mandated java.base
  requires slf4j.simple
  contains io.vividcode.store.runtime
  main-class io.vividcode.store.runtime.Main

To record module hashes using --hash-modules, you need to also provide the module path using -p or --module-path for the jar tool to locate modules.

JMOD Files

JMOD files are introduced in Java 9 to package JDK modules. Compared to JAR files, JMOD files can contain native code, configuration files, and other kinds of data. Figure 2-3 shows the JMOD files in the jmods directory of the JDK.

A459620_1_En_2_Fig3_HTML.jpg
Figure 2-3. JDK JMOD files

Developers can also use JMOD files to package module files using the new jmod tool.

You can use the following jmod list command to list the names of all entries in the JMOD file of the module java.base.

$ jmod list <JDK_PATH>/jmods/java.base.jmod

This JMOD file contains directories listed in Table 2-15.

Table 2-15. Directories in JMOD Files

Directory Name

Files

classes/

Java class files

conf/

Configuration files

include/

Header files

legal/

Legal files

bin/

Executable binaries

lib/

Native libraries

Other modules, for example, java.sql and java.xml, may only contain classes and legal directories.

The command jmod describe prints out the module details, which are similar to jar -d.

$ jmod describe <JDK_PATH>/jmods/java.base.jmod

The command jmod extract extracts all the files to the target directory.

$ jmod extract <JDK_PATH>/jmods/java.sql.jmod --dir <output_dir>

The command jmod create creates JMOD files. You can use different options to provide the path for various kinds of files; see Table 2-16.

Table 2-16. Options of jmod

Option

Description

--class-path

JAR files or directories containing Java class files

--cmds

Path of native commands

--config

Path of configuration files

--header-files

Path of header files

--legal-notices

Path of legal files

--libs

Path of native libraries

--man-pages

Path of man pages

jmod create also supports the options --main-class and --module-version. Options --os-arch and --os-name can be used to specify the operating system architecture and name. Listing 2-48 shows how to use jmod to create a JMOD file.

Listing 2-48. Using jmod to Create Module Artifacts
$ jmod create --class-path target/classes 
  --main-class io.vividcode.store.runtime.Main
  --module-version 1.0.0
  --os-arch x86_64
  --os-name "Mac OS X"
  target/runtime-1.0.0.jmod

JDK Modules

In Java 9, JDK itself is organized as multiple modules. There are currently 94 modules in JDK. These modules have four different prefixes to indicate their groups; see Table 2-17.

Table 2-17. Groups of JDK Modules

Group

Description

java.

Standard Java modules, for example, java.base, java.logging, java.sql and java.xml

javafx.

JavaFX modules, for example, javafx.base, javafx.deploy and javafx.media

jdk.

Part of JDK implementation, for example, jdk.compiler, jdk.charsets, jdk.accessibility and jdk.management

oracle.

Oracle modules, for example, oracle.desktop and oracle.net

Common Issues

With the introduction of the module system, some common issues may occur. You’ve already seen a potential issue—that the platform and application class loaders are no longer instances of URLClassLoader. There are other potential issues you may need to be aware of.

The modifier public no longer guarantees that a program element is accessible everywhere. Accessibility now depends on various conditions related to the module system, including whether the package is exported or open and whether its module is readable by the module containing the code that is attempting to access it.

If a package is defined in both a named module and on the class path, then the package on the class path is ignored. For example, the package javax.transaction is defined in the module java.transaction. If the module java.transaction is in the module path, and the JAR file javax.transaction-api-1.2.jar that contains the same package javax.transaction is also on the class path, the classes in the JAR file are ignored. That’s why you may get the error java.lang.ClassNotFoundException: javax.transaction.SystemException when you’re running Spring JPA applications. The solution is to either remove the module java.transaction from the module path or put the JAR file in the upgrade module path to override built-in modules.

Modules that define Java EE APIs, or APIs primarily of interest to Java EE applications, have been deprecated and will be removed in a future release. These modules are not resolved by default for code on the class path. The default set of root modules for the unnamed module is based upon the java.se module. The java.se module only requires modules shown in Listing 2-49.

Listing 2-49. Modules Required by the java.se Module
java.compiler
java.datatransfer
java.desktop
java.instrument
java.logging
java.management
java.management.rmi
java.naming
java.prefs
java.rmi
java.scripting
java.security.jgss
java.security.sasl
java.sql
java.sql.rowset
java.xml
java.xml.crypto

The modules shown in Listing 2-50 are required in the java.se.ee module, so by default, code in the unnamed module doesn’t have access to APIs defined in these modules.

Listing 2-50. Modules Required by the java.se.ee Module
java.activation
java.corba
java.transaction
java.xml.bind
java.xml.ws
java.xml.ws.annotation

If the project uses classes from the modules in Listing 2-50, the code cannot compile. You need to update the Java compiler option to add --add-modules java.se.ee to include the java.se.ee module.

Most of JDK’s internal APIs are inaccessible by default at compile time. Some of these internal APIs, especially the sun.misc package, have been used by some popular libraries. In JDK 9, noncritical internal APIs, for example sun.misc.BASE64Decoder, are encapsulated and inaccessible by default. Critical internal APIs, for example, sun.misc.Unsafe and sun.reflect.Reflection, are not encapsulated. These APIs are defined in the JDK specific module jdk.unsupported. If your project requires you to use these packages, you can make your module require jdk.unsupported. For those encapsulated internal APIs, they can be made accessible using --add-exports and --add-opens when necessary.

The options -Xbootclasspath and -Xbootclasspath/p have been removed. The JDK-specific system property sun.boot.class.path has also been removed. This is because the bootstrap class path is empty by default, since bootstrap classes are loaded from modules. If the code depends on the bootstrap class path, it may not run using JDK 9. For example, the class org.elasticsearch.monitor.jvm.JvmInfo in Elasticsearch 1.7.1 tries to get the bootstrap class path from the JMX bean java.lang.management.RuntimeMXBean using the method getBootClassPath(). This method in Java 9 simply throws UnsupportedOperationException. This causes Elasticsearch 1.7.1 to fail to start when running on JDK 9. The correct implementation should check the method boolean isBootClassPathSupported() first before calling getBootClassPath() .

Migration in Action

Now that you understand all the important concepts in the Java 9 module system, you have a clear path to migrate existing applications to use modules.

Building the Project Using Java 9

Before migrating the code to Java 9, you should build and run the project on Java 9 first. This requires configuration changes to your existing build tools. For Maven, you need to upgrade the Maven compiler plugin to the latest version and set the source and target configuration to 9. For Gradle, you need to update the properties sourceCompatibility and targetCompatibility of the Java plugin to 9. You should also upgrade Maven or Gradle plugins to the latest version for better support of Java 9.

After updating the configuration, try to run the build. It’s possible you’ll encounter issues with build tools, especially since Java 9 has just been released and it takes time for the build tools to provide the support for it. At the time of writing, Gradle 4.2 doesn’t have first-class support for Java 9 modules yet. You may need to find workarounds for various issues. Or you can wait until the support of build tools is mature before you start the migration. As I mentioned before, Java 9 has no issues running applications written before it.

When building the project using Java 9, you may encounter some errors. Most of these errors are apt to be related to class access and reflections. This is because modules have stricter access control. The default value of the option --illegal-access is permit, so Java 9 already allows code on the class path to perform illegal access. With this default mode, most of reflection-related issues can be resolved. JVM outputs warning messages about illegal access, but the application can be started.

The Migration Path

After you can su ccessfully build the project using JDK 9, you can start the migration process. The following are the basic steps for migrating an existing application to modules.

  1. Add existing third-party libraries into the module path. If you are using build tools like Maven or Gradle, you may not need to do anything. These tools should already handle this for you. If you’re using JDK tools, you can specify the module path using --module-path or -p, and then point to the directory of libraries. Putting libraries into the module path is the first step to moving from class path to module paths. These libraries become automatic modules and you don’t need to migrate them.

  2. List the dependency tree. You can do this using the task dependency:tree of the Maven dependency plugin or the Gradle command gradle dependencies. The dependency tree is used to understand dependencies between a project’s modules or subprojects. This step is not necessary if you already know about the dependencies. As I mentioned before, you should do a bottom-up migration, so you need the project’s module dependencies tree to figure out the migration order.

  3. Start from the subprojects in the bottom of the dependency tree and add module declaration files for each subproject. Third-party libraries are also declared in the module-info.java files as automatic modules. As a start, simply export all packages for each module. If your code already uses conventions to distinguish between public and internal packages, you should leverage it in the module declaration. For example, you can export packages with names that end with api but not packages that end with impl or internal. If service providers are used in the application, add them in the module declaration.

  4. Try to compile the code and fix compilation errors. The IDE can help you to auto-fix most of the issues.

  5. Run all unit tests and integration tests to make sure all are passed.

  6. Improve the module declarations to remove unnecessary exports and fix issues found during tests.

These steps can be simplified with the help of the jdeps tool. The jdeps tool can generate module declaration files.

BioJava

To demonstrate the migration of Maven projects, I’ll use a real-world example. Let’s take a look at the BioJava ( http://biojava.org/ ) project. BioJava is an open-source project ( https://github.com/biojava/biojava ) dedicated to providing a Java framework for processing biological data. It’s a Maven project with 13 modules, so it’s a good candidate for demonstrating the migration process.

The first step is to build the project using Java 9. Before building the project, you need to make sure Maven is using Java 9. You can use mvn -v to verify the JVM that Maven is using. If Java 9 is the default JVM on your local machine, then you are good to go. If you have multiple JDKs installed, you need to make sure the system property JAVA_HOME points to JDK 9. Now you can use mvn to build the project. For Intellij IDEA users, you can configure the JRE to run Maven in PreferencesBuild, Execution, DeploymentBuild ToolsMavenRunner.

Now you should configure the source and target level of the Maven compiler plugin. This can be done by updating the property jdk.version to 9. You also need to update the plugins to the latest version. This is necessary to avoid issues when using these plugins in Java 9. Table 2-18 shows the latest versions of these plugins.

Table 2-18. Versions of Maven Plugins

Plugin

Version

maven-compiler-plugin

3.7.0

maven-jar-plugin

3.0.2

maven-assembly-plugin

3.1.0

maven-surefire-plugin

2.20.1

maven-enforcer-plugin

3.0.0-M1

When you first start the Maven build, you encounter the first issue with Maven enforcer plugin; see Listing 2-51. This is because the Maven enforcer plugin version 1.2 uses an older version (2.3) of Apache Commons Lang, which doesn’t support the new version scheme of Java 9. Upgrading the enforcer plugin to version 3.0.0-M1 solves this issue.

Listing 2-51. Issue with Maven Enforcer Plugin
Caused by: java.lang.ExceptionInInitializerError
  at org.apache.maven.plugins.enforcer.RequireJavaVersion.execute(RequireJavaVersion.java:52)
  at org.apache.maven.plugins.enforcer.EnforceMojo.execute(EnforceMojo.java:178)
  at org.apache.maven.plugin.DefaultBuildPluginManager.executeMojo(DefaultBuildPluginManager.java:134)
  ... 22 more
Caused by: java.lang.StringIndexOutOfBoundsException: begin 0, end 3, length 1
  at java.base/java.lang.String.checkBoundsBeginEnd(String.java:3116)
  at java.base/java.lang.String.substring(String.java:1885)
  at org.apache.commons.lang.SystemUtils.getJavaVersionAsFloat(SystemUtils.java:1122)
  at org.apache.commons.lang.SystemUtils.<clinit>(SystemUtils.java:818)
  ... 25 more

When compiling the source code, you’ll encounter the second issue java.lang.ClassNotFoundException: javax.xml.bind.JAXBException. This is because BioJava uses JAXB. The module java.xml.bind is not in the default set of root modules for the unnamed module, so its classes cannot be found. To fix this issue, you need to update the Java compiler option to add --add-modules java.se.ee. You can do this by configuring the Maven compiler plugin; see Listing 2-52.

Listing 2-52. Configuring the Maven Compiler Plugin to Add Extra Modules
<plugin>
  <artifactId>maven-compiler-plugin</artifactId>
  <version>3.7.0</version>
  <configuration>
    <source>${jdk.version}</source>
    <target>${jdk.version}</target>
    <compilerArgs>
      <arg>--add-modules</arg>
      <arg>java.se.ee</arg>
    </compilerArgs>
  </configuration>
</plugin>

The same option should also be added to the Maven surefire plugin using the configuration argLine. After you add this option, you can compile the project successfully.

Now you should run the unit tests. During the unit test, you’ll encounter the third issue that causes some tests to fail; see Listing 2-53.

Listing 2-53. Unit Test Error
org.biojava.nbio.structure.align.ce.CeCPMainTest
testCECP1(org.biojava.nbio.structure.align.ce.CeCPMainTest)  Time elapsed: 1.394 sec  <<< ERROR!
java.lang.ExceptionInInitializerError
  at org.biojava.nbio.structure.align.ce.CeCPMainTest.testCECP1(CeCPMainTest.java:415)
Caused by: java.lang.RuntimeException: Could not initialize JAXB context
  at org.biojava.nbio.structure.align.ce.CeCPMainTest.testCECP1(CeCPMainTest.java:415)
Caused by: javax.xml.bind.JAXBException: Package java.util with JAXB class java.util.TreeSet defined in a module java.base must be open to at least java.xml.bind module.
  at org.biojava.nbio.structure.align.ce.CeCPMainTest.testCECP1(CeCPMainTest.java:415)

After enabling error stacktrace, it turns out that it’s caused by the code in Listing 2-54 of class org.biojava.nbio.structure.scop.server.XMLUtil. The error message is very clear—the package java.util in the module java.base needs to be open to the module java.xml.bind.

Listing 2-54. Code with Issues in XMLUtil
static JAXBContext jaxbContextDomains;
static {
  try {
    jaxbContextDomains= JAXBContext.newInstance(TreeSet.class);
  } catch (JAXBException e){
    throw new RuntimeException("Could not initialize JAXB context", e);
  }
}

You can use the option --add-opens java.base/java.util=java.xml.bind to fix the issue. This option needs to be added to configuration argLine of the Maven surefire plugin.

Now all the unit tests can pass successfully and you can move on to the migration. Let’s use jdeps to generate module declaration files for all modules. To do this, you need to copy all the project’s artifacts and third-party libraries to a directory. You can use the task dependency:copy-dependencies to copy dependencies for all modules and the task dependency:copy to copy artifacts of modules. Listing 2-55 shows the Maven configuration to copy the artifacts and dependencies during the install phase.

Listing 2-55. Copying the Artifacts and Dependencies
<plugin>
  <artifactId>maven-dependency-plugin</artifactId>
  <executions>
    <execution>
      <phase>install</phase>
      <id>copy-jar</id>
      <goals>
        <goal>copy</goal>
      </goals>
      <configuration>
        <artifactItems>
          <artifactItem>
            <groupId>${project.groupId}</groupId>
            <artifactId>${project.artifactId}</artifactId>
            <version>${project.version}</version>
            <type>${project.packaging}</type>
          </artifactItem>
        </artifactItems>
        <outputDirectory>${targetDirectory}</outputDirectory>
      </configuration>
    </execution>
    <execution>
      <phase>install</phase>
      <id>copy-dependencies</id>
      <goals>
        <goal>copy-dependencies</goal>
      </goals>
      <configuration>
        <outputDirectory>${targetDirectory}</outputDirectory>
      </configuration>
    </execution>
  </executions>
</plugin>

After running these two tasks, the target directory contains all the JAR files you need to work with. There are two versions of commons-lang in the directory; you need to remove the commons-lang-2.4.jar. Now let’s generate module declaration files. In the following command, the <output_dir> is the output directory for the generated module declaration files; <biojava_assembly_dir> is the directory contains all the artifacts.

$ jdeps --generate-module-info <output_dir> <biojava_assembly_dir>

Errors occur when generating module declaration files, because jdeps also tries to generate module declaration files for those third-party libraries. Since you only care about project’s modules, you can simply ignore these errors. In the output directory, you can find the module-info.java files for each module. Listing 2-56 shows the module-info.java for the module biojava.core.

Listing 2-56. Generated module-info.java for biojava.core
module biojava.core {
  requires java.logging;
  requires java.rmi;
  requires slf4j.api;


  requires transitive java.xml;

  exports org.biojava.nbio.core.alignment;
  exports org.biojava.nbio.core.alignment.matrices;
  exports org.biojava.nbio.core.alignment.template;
  exports org.biojava.nbio.core.exceptions;
  exports org.biojava.nbio.core.search.io;
  exports org.biojava.nbio.core.search.io.blast;
  exports org.biojava.nbio.core.sequence;
  exports org.biojava.nbio.core.sequence.compound;
  exports org.biojava.nbio.core.sequence.edits;
  exports org.biojava.nbio.core.sequence.features;
  exports org.biojava.nbio.core.sequence.io;
  exports org.biojava.nbio.core.sequence.io.template;
  exports org.biojava.nbio.core.sequence.io.util;
  exports org.biojava.nbio.core.sequence.loader;
  exports org.biojava.nbio.core.sequence.location;
  exports org.biojava.nbio.core.sequence.location.template;
  exports org.biojava.nbio.core.sequence.reference;
  exports org.biojava.nbio.core.sequence.storage;
  exports org.biojava.nbio.core.sequence.template;
  exports org.biojava.nbio.core.sequence.transcription;
  exports org.biojava.nbio.core.sequence.views;
  exports org.biojava.nbio.core.util;


  provides org.biojava.nbio.core.search.io.ResultFactory with
      org.biojava.nbio.core.search.io.blast.BlastXMLParser,
      org.biojava.nbio.core.search.io.blast.BlastTabularParser;
}

You copy these module-info.java files into the src/main/java directory of the corresponding module. Now you can successfully migrate them to JPMS modules. You can try to build the project and when you do, you’ll encounter the errors in Listing 2-57.

Listing 2-57. Compilation Errors After Adding module-info.java Files
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.7.0:compile (default-compile) on project biojava-structure-gui: Compilation failure: Compilation failure:
[ERROR] /Users/fucheng/git/biojava/biojava-structure-gui/src/main/java/demo/ShowStructureInJmol.java:[21,1] package exists in another module: jcolorbrewer
[ERROR] /Users/fucheng/git/biojava/biojava-structure-gui/src/main/java/demo/DemoMultipleMC.java:[21,1] package exists in another module: jcolorbrewer
[ERROR] /Users/fucheng/git/biojava/biojava-structure-gui/src/main/java/demo/DemoCeSymm.java:[21,1] package exists in another module: jcolorbrewer

This is because the package demo in module biojava-structure-gui conflicts with the same package in the third-party library jcolorbrewer. Since the package demo is not important, you can exclude it in Maven compiler plugin; see Listing 2-58.

Listing 2-58. Excluding demo Packages
<plugin>
  <artifactId>maven-compiler-plugin</artifactId>
  <version>3.7.0</version>
  <configuration>
    <excludes>
      <exclude>demo/**</exclude>
    </excludes>
  </configuration>
</plugin>

Now you can build the project successfully. Once you migrate to JPMS modules, you can remove the --add-modules java.se.ee in the configuration of the Maven compiler and the surefire plugins.

Summary

As the most important new feature in Java 9, JPMS has an impact on different aspects of the Java platform. This chapter started with the module declarations, and then went on to discuss unnamed modules and automatic modules. I also covered JDK tools and related concepts. In addition, you learned that with the module-related API, you can interact with the module system in Java programs. Finally, this chapter provided information about the migration path and a concrete example of how to migrate to Java 9 modules. In next chapter, we’ll discuss the REPL shell in Java 9—jshell.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.15.22.160