TABLE OF CONTENTS (HIDE)

Java Module System (JDK 9)

JDK 9 introduces the Java Module System.

References:
  1. Project Jigsaw: Module System Quick-Start Guide @ http://openjdk.java.net/projects/jigsaw/quick-start.
  2. Project Jigsaw @ http://openjdk.java.net/projects/jigsaw/.
  3. JSR 376: Java Platform Module System @ http://openjdk.java.net/projects/jigsaw/spec/.
  4. JEP 200: The Modular JDK @ http://openjdk.java.net/jeps/200.
  5. JEP 201: Modular Source Code @ http://openjdk.java.net/jeps/201.
  6. JEP 220: Modular Run-Time Images @ http://openjdk.java.net/jeps/220.
  7. JEP 260: Encapsulate Most Internal APIs @ http://openjdk.java.net/jeps/260.
  8. JEP 261: Modular System @ http://openjdk.java.net/jeps/261.
  9. JEP 281: jlink: The Java Linker @ http://openjdk.java.net/jeps/281.
  10. JDK 9 Modules @ https://www.journaldev.com/13106/java-9-modules.

 

The most drastic change in JDK 9 is the Java Module System, which is carried out through the Project Jigsaw @ http://openjdk.java.net/projects/jigsaw/.

The current Java System suffers from these problems:

  • The JDK is too big, with many legacy codes. This also impedes on the performance, test and maintenance.
  • JAR files like rt.jar are too big to be used in small devices.
  • There is no strong encapsulation. The public access is too open. Some internal non-critical APIs like sun.*, *.internal.*, are accessible, as they are public, which poses security issues.

I shall assume that you are familiar with Java package and CLASSPATH. Otherwise, read "Package and CLASSPATH".

Why module?

A module is a set of related packages and resources (code and data) that is designed for reuse. It adds a higher level of aggregation above the package. Each module has a module descriptor called module-info.java, which is to be compiled into module-info.class.

Before JDK 9, public classes are accessible to ALL, resulted in many APIs got exposed unnecessarily. With modules, you can conceal packages for internal use only; or export packages to be used by other modules. You can further specify which classes within a package are to be shared with which modules, or you can set them to be accessible by any modules. In other words, you can fine-tune the public access restriction of packages and classes to improve security with strong encapsulation and stability with reliable dependencies. "public" on a class declaration no longer means accessible to everyone.

Module Dependency by Examples

Example 1: Hello Module - The module Statement

I shall assume that our project's base directory is "c:\myProject", with a sub-directory "src" for the source files and a sub-directory "mods" for the compiled modules (instead of "classes" for compiled classes). Create the directory structure.

Let's write a module called "com.hello". By convention, the module's directory name is the same as the module's name. Create a sub-directory "com.hello" under "src".

Each module has a module descriptor file called "module-info.java" under the top level directory of a module. Create the module descriptor "module-info.java" under "src\com.hello", as follows:

// src\com.hello\module-info.java
module com.hello {
}

This module com.hello contains a package com.hellopack, with a class Hello (i.e., com.hellopack.Hello). Recall that the dot in a package corresponds to a sub-directory, i,e, package com.hellopack has a directory structure of "com\hellopack". Create the directory structure "src\com.hello\com\hellopack" for the package com.hellopack under the module.

Write the main class com.hellopack.Hello in "src\com.hello\com\hellopack\Hello.java", as follows:

// src\com.hello\com\hellopack\Hello.java
package com.hellopack;
public class Hello {
   public static void main(String[] args) {
      System.out.println("hello, module");
   }
}

We shall keep our compiled module in a sub-directory called mods under our project base directory (c:\myProject). Our compiled module com.hello shall be kept in directory mods\com.hello.

Compile the module as follows, where -d option specifies the directory of the generated class file, which will be created automatically.

> c:
> cd \myProject
> javac -d mods/com.hello src/com.hello/module-info.java src/com.hello/com/hellopack/Hello.java
   // Note: You can use either forward slash (Unix and macOS) or 
   //       backward slash (Windows) as path separator in the javac command

The mods\com.hello\module-info.class and mods\com.hello\com\hellopack\Hello.class shall be generated, as shown:

> tree /f /a
+---mods
|   +---com.hello
|       |   module-info.class
|       |
|       \---com
|           \---hellopack
|                   Hello.class
\---src
    +---com.hello
        |   module-info.java
        |
        \---com
            \---hellopack
                    Hello.java

You can run the main class as follows, where -p (or --module-path) specifies the module path, -m specifies the main class. Both -p and -m are new options in JDK 9:

> c:
> cd \myProject
> java -p mods -m com.hello/com.hellopack.Hello
hello, module

Notes:

  • JDK 9 introduces module-path for locating module, similar to classpath for locating classes prior to JDK 9.
  • In this example, I named the module and package differently for academic purpose. In practice, the module and package could be given the same name.

Example 2: Module Dependency - Directives "exports <package>" and "requires <module>"

Let's create two modules: com.hi and com.world, such that com.hi depends on (or reads) com.world.

We need to start with the independent module com.world. Create the module directory "com.world" under "src". Create the module descriptor module-info.java under "src\com.world", as follows:

// src\com.world\module-info.java
module com.world {
   exports com.world;  // This package can be used by ALL other modules
}

The com.world module contains a package called com.world (having the same name as the module), which is exported to be used by ALL other modules.

The package com.world contains a class called World (i.e., com.world.World), as follows:

// src\com.world\com\world\World.java
package com.world;
public class World {
   public static String getWorld() {
      return "world";
   }
}

Compile the module com.world as follows:

> cd <project-base-path>
> javac -d mods/com.world src/com.world/module-info.java src/com.world/com/world/World.java

Now, let us write the dependent module com.hi, Create the module directory "com.hi" under "src". Create the module descriptor module-info.java under "src\com.hi", as follows:

// src\com.hi\module-info.java
module com.hi {
   requires com.world;   // Depends on this module
                         // "requires" is analogous to "import"
}

The module com.hi contains a package com.hi (having the same name as the module), with a class com.hi.Hello. The Hello class invokes method in the World class (of the module com.world):

// src\com.hi\com\hi\Hello.java
package com.hi;
import com.world.World;  // from the "requires" module
public class Hello {
   public static void main(String[] args) {
      System.out.println("hello, " + World.getWorld());
   }
}

Compile the module com.hi as follows, where -p (or --module-path) specifies the module-path for locating the dependent module:

> cd <project-base-path>
> javac -p mods -d mods/com.hi src/com.hi/module-info.java src/com.hi/com/hi/Hello.java

Run the Hello class:

> java -p mods -m com.hi/com.hi.Hello
hello, world

The directory/file structure is as follows:

> tree /f /a
+---mods
|   |
| +---com.hi
| | | module-info.class
| | |
| | \---com
| | \---hi
| | Hello.class
| |
| \---com.world
| | module-info.class
| |
| \---com
| \---world
| World.class
|
\---src |
+---com.hi
| | module-info.java
| |
| \---com
| \---hi
| Hello.java
|
\---com.world
| module-info.java
|
\---com
\---world
World.java
Missing "requires <module>" Directive

Comment out the requires directive in com.hi's module descriptor src\com.hi\module-info.java; and re-compile:

> javac -p mods -d mods/com.hi src/com.hi/module-info.java src/com.hi/com/hi/Hello.java
src\com.hi\com\hi\Hello.java:4: error: package com.world is not visible
import com.world.World;
          ^
  (package com.world is declared in module com.world, but module com.hi does not read it)
Missing "exports <package>"Directive

Now comment out the exports directive in com.world's module descriptor src\com.world\module-info.java; and re-compile:

> javac -d mods/com.world src/com.world/module-info.java src/com.world/com/world/World.java

> javac -p mods -d mods/com.hi src/com.hi/module-info.java src/com.hi/com/hi/Hello.java
src\com.hi\com\hi\Hello.java:4: error: package com.world is not visible
import com.world.World;
          ^
  (package com.world is declared in module com.world, which does not export it)

Example 3: Packaging in Modular JAR Files

For deployment, we can package a module as a Modular JAR File, which is a regular JAR file with a module descriptor module-info.class in its top-level directory.

We shall keep our modular JAR files in a sub-directory "mlib". Create a sub-directory "mlib" under "c:\myProject".

Create modular JAR file for module com.world as follows, where -c (or --create) for creating new JAR file, -f (or --file) specifies the name of the JAR file, -C to change the directory:

> cd <project-base-path>
> jar -c -f mlib/com.world.jar -C mods/com.world .

Create modular JAR file for module com.hi as follows, where --main-class specifies the main entry class of the module:

> jar -c -f mlib/com.hi.jar --main-class com.hi.Hello -C mods/com.hi .

You can run the modular JAR file directly as follows, where -p (or --module-path) specifies the module path, -m (or --module) specifies the module to be executed (both -p and -m are new options in JDK 9).

> java -p mlib -m com.hi
hello, world
CLASSPATH vs MODULEPATH

A CLASSPATH is a sequence of classes' base paths and JAR files for the Java Compiler or JVM to locate the classes used in the application.

Similarly, in JDK 9, a MODULEPATH is a sequence of modules' base paths and Modular JAR files for the Java Compiler or JVM to locate the modules used in the application.

Module's Directives

A module adds a higher level of aggregation above package. It is a set of related packages, as well as resources (such as images and XML files). By convention, a module is kept in a directory with the same name as the module name. Each module contains a module descriptor module-info. The descriptor contains directives (such as requires, exports, uses, provides...with, opens, to and transitive) to describe:

  • the module's name
  • the module's dependencies (via "requires <module>", "requires transitive <module>")
  • the packages it explicitly makes available to other modules (via "exports <package>", "exports <package> to <modules>")
  • the services it provides (via "provides <service> with <implementationClass>")
  • the services it consumes (via "uses <service>")
  • to what other modules it allows reflection (via "opens <package>")
// moduleName\module-info.java
module moduleName {
   ......  // module directives
}
requires <module>

A requires directive specifies that this module depends on another module. When module A requires module B, module A is said to read module B; and module B is read by module A.

requires transitive <module>

Other modules that read this module also read this dependency (known as implied readability). For example, if module A requires transitive module B, and module C requires module A. Module C can read module B without explicitly requires module B.

exports <package>

Specify a package (of this module) whose public types are accessible to ALL other modules.

exports <package> to <modules>

Further specify, in a comma-separated list, which module's code can access the exported package. For example, [TODO]

uses <service>

Specify a service used by this module - making this module a service consumer. A service is an instance of a class that implements the interface or extends the abstract class specified in the uses directive.

provides <service> with <implementationClass>

Specify that a module provides a service implementation - making this module a service provider.

opens <package>

Before JDK 9, reflection could be used to learn about all types in a package and all members of a type - even its private members. In JDK 9, the opens packageName specifies that the package’s public types are accessible to code in ALL other modules at runtime only. Also, all the types in the specified package (and all of the types’ members) are accessible via reflection.

opens <package> to <modules>

To the specified modules (commas-separated list) only.

Service Consumer and Service Provider Modules By Example

Reference: JDK 9 Modules - Creating Services and Service Providers @ https://www.logicbig.com/tutorials/core-java-tutorial/modules/services.html.

 

Services allow for loose coupling between service consumer modules and service provider modules.

A service consumer module declares that it uses one or more interfaces whose implementations will be provided at runtime by some service provider module. A service provider module declares what implementations of service interface it provides...with.

The Service Consumer Module

Let's call our service consumer module com.hello.consumer. It contains a package com.hello.consumer, and consume a service defined in interface com.hello.consumer.HelloService (to be provided by a service provider).

// Module "com.hello.consumer" Module Descriptor
// src\com.hello.consumer\module-info.java
module com.hello.consumer {
   exports com.hello.consumer;
   uses com.hello.consumer.HelloService;  // service's interface
}
// Service Interface
// src\com.hello.consumer\com\hello\consumer\HelloService.java
package com.hello.consumer;

import java.util.Iterator;
import java.util.ArrayList;
import java.util.List;
import java.util.ServiceLoader;

public interface HelloService {

   // Return a List of implementations for this service.
   public static List<HelloService> getInstances() {
      ServiceLoader<HelloService> services = ServiceLoader.load(HelloService.class);
      Iterator<HelloService> iter = services.iterator();
      if (!iter.hasNext()) throw new RuntimeException("No service providers found!");
      List<HelloService> lst = new ArrayList<>();
      iter.forEachRemaining(lst::add);
      return lst;
   }

   void sayHello(String msg);  // (public abstract) to be provided by the implementation class
}
// Compile the module from "src" into "mods"
> cd <project-base-directory>
> javac -d mods/com.hello.consumer src/com.hello.consumer/module-info.java src/com.hello.consumer/com/hello/consumer/HelloService.java

// Create Modular JAR file in "mlib"
> jar --create --file mlib/com.hello.consumer.jar -C mods/com.hello.consumer .
How does ServiceLoader find a class?

The ServiceLoader can find and instantiate classes that implement a specific interface. When we call the static method ServiceLoader.load(interfaceName), it returns a list of classes that implement this interface. The JDK 9's module declares the services that a ServiceLoader can load, and a module can also declare what services it may need to load via the ServiceLoader.

The Service Provider Module

Our service provider module called com.hello.provider, provides an implementation for the service.

// Module "com.hello.provider" Module Descriptor
// src\com.hello.provider\module-info.java
module com.hello.provider {
   requires com.hello.consumer;
   provides com.hello.consumer.HelloService with com.hello.provider.HelloServiceImpl;
      // provides an implementation for the service.
}
// Implementation class of the service
// src\com.hello.provider\com\hello\provider\HelloServiceImpl.java
package com.hello.provider;
import com.hello.consumer.HelloService;

public class HelloServiceImpl implements HelloService {
   @Override
   public void sayHello(String msg) {
      System.out.println("hello, " + msg);
   }
}
// Compile the module from "src" into "mods"
> cd <project-base-directory>
> javac -p mlib -d mods/com.hello.provider src/com.hello.provider/module-info.java src/com.hello.provider/com/hello/provider/HelloServiceImpl.java

// Create Modular JAR file in "mlib"
> jar --create --file mlib/com.hello.provider.jar -C mods/com.hello.provider .
Service Client Module

Now, write a client module that invokes the service.

// Module "com.hello.client" Module Descriptor
// src\com.hello.client\module-info.java
module com.hello.client {
   requires com.hello.consumer;
}
// src\com.hello.client\com\hello\client\Main.java
package com.hello.client;
import com.hello.consumer.HelloService;
import java.util.List;

public class Main {
   public static void main(String[] args) {
      List<HelloService> services = HelloService.getInstances();
      for (HelloService service : services) {
         System.out.println(service.getClass().getName());  // print the implementation classname
         service.sayHello("service");
      }
   }
}
// Compile the module
> javac -p mlib -d mods/com.hello.client src/com.hello.client/module-info.java src/com.hello.client/com/hello/client/Main.java

// Create JAR
> jar --create --file mlib/com.hello.client.jar --main-class=com.hello.client.Main -C mods/com.hello.client .

// Run
> java -p mlib -m com.hello.client
com.hello.provider.HelloServiceImpl
hello, service

New Tools to Support Modules

jlink - The Java Linker (JEP 282)

The goal of JEP 282 is to "create a tool that can assemble and optimize a set of modules and their dependencies into a custom run-time image". jlink is the linker tool and can be used to link a set of modules, along with their transitive dependence, to create a custom modular run-time image.

The following example creates a run-time image that contains the module com.hello.client and its transitive dependences:

// For Windows
> cd <project-base-path>
> jlink --module-path "%JAVA_HOME%/jmods";mlib --add-modules com.hello.client --output helloapp

// For Unix and macOS
$ cd <project-base-path>
$ jlink --module-path $JAVA_HOME/jmods:mlib --add-modules com.hello.client --output helloapp

Notes:

  • The module path "%JAVA_HOME%/jmod" is the directory containing java.base.jmod and other standard and JDK modules.
  • The module path "mlib" is the directory containing our module com.hello.client.
  • The module com.hello.client shall be packaged in modular JAR or JMOD format (in the module path).

The output "helloapp" is a directory (36 MB), as follows:

> tree /a helloapp
HELLOAPP
+---bin
|   \---server
+---conf
|   \---security
|       \---policy
|           +---limited
|           \---unlimited
+---include
|   \---win32
+---legal
|   \---java.base
\---lib
    +---security
    \---server

The modules are kept under "helloapp\lib\modules" (22 MB)?!

To run the Main class:

// Run the "Main" class
> cd <project-base-path>
> helloapp\bin\java -m com.hello.client/com.hello.client.Main
Exception in thread "main" java.lang.RuntimeException: No service providers found!

// Re-link with provider module
> jlink --module-path "%JAVA_HOME%/jmods";mlib --add-modules com.hello.client,com.hello.provider --output helloapp

// Run the "Main" class again
> helloapp\bin\java -m com.hello.client/com.hello.client.Main
com.hello.provider.HelloServiceImpl
hello, service

JDK 9 Introduces a new optional phase, link time, which is in-between compile time and run time, during which a set of modules can be assembled and optimized into a custom runtime image.

jmod Tool and jmod Files

JDK 9 Introduces the JMOD format, which is a packaging format similar to JAR except it can include native code and configuration files. It also introduces a jmod tool for creating jmod files.

[TODO]

The Modular JDK

JDK 9 makes the following changes to support modular JDK.

The Modular JDK (JEP 200)

JDK has grown over the years, with massive amount of legacy codes. But the Java platform has primarily been a monolithic one-size-fits-all solution.

The goal is to divide the JDK into a set of modules that can be combined at compile time, build time and run time into a variety of configurations.

The modular JDK implements these design principles:

  1. Standard modules, whose specifications are governed by the JCP (Java Community Process), start with "java.".
    • Base Module java.base: defines the foundational APIs of the Java SE Platform, including core JDK packages such as java.lang, java.util, java.io, java.nio, java.net, javax.net, java.text, java.security, javax.cryptio.
      The Base Module is an independent module and does NOT depends on any other modules. By default, all other modules depends on this module.
      The Base Module is also the default module for all JDK modules and user-defined modules.
    • Module java.desktop: Defines the AWT and Swing user interface toolkits, plus APIs for accessibility, audio, imaging, printing, and JavaBeans, including packages java.awt, java.awt.event, java.awt.*, java.beans, javax.imageio, javax.print, javax.swing, javax.swing.*, javax.sound.*.
    • Module java.sql: defines the JDBC API, including packages java.sql, javax.sql.
    • Others
  2. All other modules that are part of JDK, start with "jdk.".
  3. JavaFX modules, starts with "javafx.".

A new option --list-modules is provided in java tool to list all the modules, for example,

> java --list-modules
java.activation@10.0.1
java.base@10.0.1
......

JEP 201: Modular Source Code (JEP 201)

The current source code JAR files are too big, especially rt.jar. The goal of this JEP is to "reorganize the JDK source code into modules, enhance the build system to compile modules, and enforce module boundaries at build time".

Modular Run-Time Images (JEP 220)

The main goal of this JEP is to "restructure the JDK and JRE run-time images to accommodate modules and to improve performance, security, and maintainability".

The JDK 9 does not have a "jre" sub-directory. But it has a "jmods" sub-directory, which contains the Java modules (such as java.activation.jmod, java.base.jmod, etc.). In other words, In JDK 9, JRE is separated into a separate distribution jmods.

JDK 9 removes rt.jar or tools.jar.

Encapsulate Most Internal APIs (JEP 260)

The main goal of this JEP is to "encapsulate most of the JDK's internal APIs by default so that they are inaccessible at compile time, and prepare for a future release in which they will be inaccessible at run time".

 

REFERENCES & RESOURCES