Kubernetes with OpenShift World Tour: Get hands-on experience and build applications fast! Find a workshop!

Java 9+ modularity: The difficulties and pitfalls of migrating from Java 8 to Java 9+

It’s been a long journey through Java releases

Throughout my humbling, almost 20-year career in programming, I’ve worked with many languages — Visual Basic, C++, C#, you name it — but since 2005, my specialization has been the Java programming language and ecosystem in all its incarnations, beginning with version 1.2. Those were lovely days when you got your Professional Java Programmer certificate directly from Sun Microsystems.

Around 2009, I started giving back to the community by contributing to the platform development cycle, first as a community member, and then as a full JCP member, expert for some of the JSRs, Java champion, and Oracle Groundbreaker Ambassador. It’s been a rewarding experience.

What I’ve loved about Java, from 1.0 to 8, is that a program written early in the evolution runs quite well in a later release because the language has always put backwards compatibility high on the priorities list.

This attribute came with a cost.

The JDK grew. APIs and tools became more difficult to maintain, older technologies harder to deprecate, security issues more challenging to resolve. Plus, it wasn’t as easy to integrate new innovations (like cloud or containers) or cope with the faster development cycles.

But the excellent Java community of developers and companies rose to these challenges and the Java Platform Module System (JPMS) and the new six-month release schedule is the result. The first two tutorials in this series, “The theory and motivations behind modularity” and “Module basics and rules,” describe the motivations that drove the community to modularity. The next two tutorials — “How to design packages and create modules, Part 1” and “How to design packages and create modules, Part 2” — discuss the key concepts of Java 9 modularity: encapsulation and reliable configuration.

Prepare for migration

Now, you move on to look at compatibility and code migration issues that arise when moving to Java SE 9 from a previous version. In this part of the journey, you’ll explore techniques to help you migrate your current code base and will gain a deep understanding of the unnamed and root modules, as well as learn about automatic modules. And, I show you how to use jdeps, the Java dependency analysis tool to help you determine module dependencies and therefore be able to migrate your code more easily.

Before you start, you need the exercise files used in this tutorial; you can find them here.

The following steps are a great outline for successfully migrating your application:

  • Download the latest JDK.
  • Run your application before recompiling.
  • Update your application third-party library dependencies.
  • Compile your application.
  • Run jdeps on your code.
  • Run jdeprscan on your code.

Download the latest JDK

Download and install the latest JDK release.

Run your application before recompiling

Try running your application on the latest JDK release. Most code and libraries should work on JDK 12+ without any changes, but there might be some libraries that need to be upgraded. Look for any warnings from the JVM about obsolete VM options. If the VM fails to start, then look for removed GC options.

The most important task is to make your code work on the latest JDK release, and to do that, you must understand the new features and changes, so here’s a list of the detailed information about the new features and changes from Java 9 to Java 13:

Update your application third-party library dependencies

For every tool and third-party library that you use, you might need to have an updated version that supports the latest JDK release.

Compile your application

Compiling your code with the latest JDK compiler eases migration to future releases because the code might depend on APIs and features that have been identified as problematic, especially with JDK 10, 11, and 12. However, it is not strictly necessary.

Run jdeps on your code

Run the jdeps tool on your application to see what packages and classes your applications and libraries depend on. If you use internal APIs, then jdeps might suggest replacements to help you to update your code.

Keep in mind that jdeps is a static analysis tool; it might not provide a complete list of dependencies. If the code uses reflection to call an internal API, then jdeps won’t warn you.

Run jdeprscan on your code

You use the jdeprscan tool as a static analysis tool that scans a JAR file (or some other aggregation of class files) for usages of deprecated API elements. Learn more from the full documentation on jdeprscan.

Ready, set, migrate code to Java 9

Let’s start this section by discussing code migration considerations, focusing on internal API access. You’ll cover different compilation modes, the various modules, and how to encapsulate resources in modules.

Code migration

Many apps before Java 9 run unaltered on the platform. In Java 9, all programs are compiled and executed using the module system and the language strongly encapsulates types that are not exported by modules, so it is possible that some apps will fail to compile because types that were accessible to them prior to Java 9 are no longer available.

For example, there are many earlier internal APIs that were not meant for use outside of the JDK, but were in fact used outside of the JDK. Many of these APIs are not exported in Java 9 and thus are inaccessible. If your code uses such internal APIs directly or indirectly, it will fail to compile.

Internal API access

Modularity enables strong encapsulation: Code that is not exported cannot be accessed by other modules. Some internal APIs considered critically important are still available in Java 9, but various JEPs referenced by JSR 379 define new public APIs that replace a lot of those internal APIs. Eventually, they will be removed.

You can use the jdeps tool released with Java 8 to locate a type’s dependencies or the dependencies for all types in a JAR file. In Java 9, the tool also supports modules. The jdeps option --JDK-internals or -jdkinternals specifically identifies the usage of JDK internal APIs in code. Like with the following example:

$ jdeps--jdk-internals Sample.class

Some Java 8 and earlier internal APIs have been placed into packages that are exported in Java 9 and are now strongly encapsulated. For each internal API that jdeps locates, you can review JEP 260 and update your code accordingly.

Java is more than two decades old, so there are vast amounts of legacy Java code that you might want to migrate to Java 9. The module system provides mechanisms that can automatically place your code in modules to help you with migration.

Now, let’s have a look at the different compilation modes provided by Java 9.

Compilation modes for Java 9 modules

Let’s focus on the different compilation modes provided by Java 9 that maintain backward compatibility for the legacy codes, as well as working with the new module system. The compiler operates in one of three modes (as specified in JEP 261):

  • Legacy mode
  • Single module mode
  • Multi-module mode

Legacy mode

Legacy mode is enabled when the compilation environment, as defined by the --source, --target, and --release options, is less than or equal to 8. None of the modular options can be used. Here, the compiler behaves in essentially the same way as it does in JDK 8 (or before), where you use traditional options (classpath, etc.) rather than any of the modules-related options (like --module-path). This mode still works in Java 9.

In this mode, our code runs as the unnamed module during runtime. I am going to explain unnamed modules in a separate section.

Single module mode

Single module mode is enabled when the compilation environment is 9 or later and the --module-source-path option is not used. In this mode, the code is structured in a traditional package-hierarchical directory tree. The code has a module-info.java file and runs on modulepath rather than on classpath.

In this structure, you place your module-info.java file directly under the src directory. You cannot have multiple module-info.java in the same directory tree, that’s why it is called single module mode.

Multi-module mode

Multi-module mode is enabled when the compilation environment is 9 or later and the --module-source-path option is used. In this mode, you can place multiple modules under the same source directory. During compile time this main source directory should be specified with the --module-source-path option. The source tree for each individual module is placed in its own sub-directory under the main source directory.

The unnamed module

In Java 9, all code is required to be placed in modules. When you execute code that is not in a module, the code is loaded from the classpath and placed in the unnamed module. You can run some non-modularized code in the modularized JDK, but unfortunately it won’t receive the benefits of modularization.

The unnamed module implicitly exports all the package’s types and reads all other modules; however, because the module is unnamed, there is no way to refer to it in a requires directive from a named module, so a named module cannot depend on the unnamed module.

Automatic modules

There are enormous numbers of preexisting libraries that you can use in your apps. Many of these are not yet modularized, but to facilitate migration, you can add any library’s JAR file to an app’s module path then use the packages in that JAR. When you do, the JAR file implicitly becomes an automatic module and can be specified in the module declaration’s requires directive.

The JAR’s file name (minus the .jar extension) becomes its module name, which must be a valid Java identifier for use in a requires directive.

An automatic module:

  • Implicitly exports all the package’s types so any module that reads the automatic module (including the unnamed module) has to access to the public types in the automatic module’s packages
  • Implicitly reads (requires) all other modules, including other automatic modules and the unnamed module, so an automatic module has access to all the public types exposed by the system’s other modules

Resources in modules

When the types in a module require resources, such as images, videos, XML documents, and more, those resources should be packaged with the module to ensure that they’re available when the module’s types are used at execution time.

By convention, resources typically are placed in a folder named res under the module root directory along with the module-info.java file and this is known as resource encapsulation (shown below).

Resource encapsulation

For more information on resource encapsulation, explore Java Platform Module System Requirements.

Now, let’s deep dive into the unnamed module concept and I’ll demonstrate what it is, what modules it requires, what packages it exports, and how it helps the old code keep working in Java 9.

The unnamed module

A class that is not a member of a named module is a member of a special module known as the unnamed module. The unnamed module concept is like the unnamed package (the default package).

The unnamed module is not a real module. It is like a default module that does not have a name. All classes compiled in older versions of the language that are not yet migrated to modules (or will never be) belong to the unnamed module when running in the Java 9+ environment.

The modules it requires

The unnamed module requires every other named module automatically. That means all classes within the unnamed module can read all another named or unnamed modules without an explicit declaration of any kind.

That also means that older classes written prior to Java 9 can read all modules defined by the new module system, so all such classes will continue to compile and run in Java SE 9 without any modification.

The packages it exports

The unnamed module exports all its packages. That means different JAR applications which do not contain a module or which are compiled in the older versions will continue to use each other’s dependencies.

The packages exported by the unnamed module can only be read by another unnamed module. It is not possible that a named module can read (requires) the unnamed module.

Of course, to explicitly use the requires clause in a module-info.java declaration or use a command line option to add the module, you will need a module name.

Let’s create one

Let’s create a very simple project without a module. (Look at the exercise files under Java-SE-Code-Examples/12/modularity/migration and open the project called Unnamed-Module. It is an old-fashioned Java project with the project JDK set to Java 9 or higher.) Here are some code snippets from it:

        Module module = UnnamedModuleApp.class.getModule();

out.printf("Module: %s%nName: %s%nisNamed: %b%nDescriptor: %s%n",
                module,
isNull(module.getName())? "Unnamed": module.getName(),
module.isNamed(),
isNull(module.getDescriptor())?
                       "Unnamed modules, doesn't have a Module descriptor" :
module.getDescriptor());

In this example, I’ve used the new method java.lang.Class#getModule(), which returns an instance of java.lang.Module (introduced in Java 9). The returned instance of the module represents a module that this class is a member of.

The method Module#getDescriptor() returns a ModuleDescriptor object, which typically represents details of module-info.class. The other methods I used should be self-explanatory.

Let’s use javac/java and other tools to learn about unnamed modules’ behavior.

Compile the application:

$ javac -d out/production/Unnamed-Module src/eg/com/taman/UnnamedModuleApp.java

Run the application:

$ java -cp out/production/Unnamed-Module eg.com.taman.UnnamedModuleApp

The output is self-explanatory:

Module: unnamed module @2f92e0f4
Name: Unnamed
isNamed: false
Descriptor: Unnamed modules, doesn't have a Module descriptor

Note that in this example, I used the classpath instead of the module-path. If you want to run your old code without migrating to a new module system, then you should run it using classpath only. But it begs the question: In this example, can you use the module-path to run your main class?

Consider the following possible command:

$ java --module-path out --module moduleName/eg.com.taman.UnnamedModuleApp

What are you going to use for moduleName? You don’t have a name in this case. That means, you cannot run unnamed modules on module-path.

Let’s use the jdeps tool; this tool was introduced in Java 8. It is a class/JAR dependency analyzer and has been enhanced for Java 9 modules. Use it on the out/UnnamedModuleApp class folder:

$ jdeps -s out/production/Unnamed-Module/eg/com/taman/UnnamedModuleApp.class
UnnamedModuleApp.class ->java.base

The code requires the only java.base. As mentioned earlier, the unnamed module requires every other named module automatically, so why don’t you see all the named modules of Java SE 9 in the above output?

That’s because they are imported on demand, only when the code uses them.

Take another class UnnamedModule2App.java which only uses the java.base module.

$ javac -d out/production/Unnamed-Module src/eg/com/taman/UnnamedModule2App.java
$ jdeps -s out/production/Unnamed-Module/eg/com/taman/UnnamedModule2App.class
UnnamedModule2App.class ->java.base
UnnamedModule2App.class ->java.logging

This time, the compiled class uses the java.logging module as well.

Using –describe-module

--describe-module <moduleName> is a Java command option which describes a module’s details. Since the unnamed module does not have a name, you cannot use this option. The jar command also has a --describe-module option which does not require a module name. It describes the module in the specified JAR. Let’s create a JAR of the previous example and use this option.

Create the mlib directory:

$ mkdirmlib

$ jar --create --file mlib/unnamed.module.test.jar -C out/production/Unnamed-Module .

Run the main class first to see if the JAR is working properly:

$ java -cpmlib/unnamed.module.test.jar eg.com.taman.UnnamedModuleApp

Now, use the --describe-module option:

$ jar --file mlib/unnamed.module.test.jar --describe-module

The output says “No module descriptor found.” An unnamed module does not have a descriptor.

You saw an example of this in the first example output. An unnamed module does not have a module-info.java, so there’s no ModuleDescriptor object for it.

The output also says “Derived automatic module.” An automatic module is a different type of module; this part of the description is applied in a different context. Here, the context refers to whether you run the JAR either using classpath or using the module-path.

This automatic module description is applied when you use the module-path. You will learn about the automatic module later.

To recap

What have you learned about unnamed modules?

  • The unnamed module does not have module-info.java, or in other words, the classes that do not have modules are promoted to the unnamed module.
  • The unnamed module requires all other modules and exports all its packages.
  • The classes that were written in the older versions but now running in Java 9 environment will continue to work because they become the members of the unnamed module.
  • If you want to run old code in the Java 9 environment without migrating them to the module system, you should run them using classpath (and not module-path).

The root modules

In JDK 9, there are a couple of modules that contain only module-info.java and no packages or Java code. The sole purpose of these modules is to require other modules (called root modules) and make them visible outside. java.se is one of these modules. Let’s see how it is declared.

Open the root-module project Unnamed-module-jee from the exercise folder.

From the IDE, expand the Java 12 JDK external libraries, point to java.se, expand it, double-click the module-info.class, and it will open in the right pane.

The root module

So what is this requires transitive clause you’re seeing? The effect of transitive is that the target module is not only required by this module (java.se in this case), but will also be readable to other modules that are reading this module.

For example, requires transitive java.logging will make java.logging available to the module that requires the java.se module. At first requires transitive seems to be same as exports but it is different in that exports is used to make packages visible, whereas requires transitive is used to make imported modules visible in the outside world.

How are root modules used?

When the unnamed module is being compiled or loaded, one of the sets of root modules should be accessible to the unnamed module so that a non-modular application will continue to work. The default set of root modules is implementation specific; in the JDK implementation, it is the module java.se.

What if you want to use a class coming from a module that is not in the set of the default root modules? To illustrate, I’ll create a simple project without a module in a JDK 9 or 10 environment. (By the way, this example does not work in Java 11 because module java.se.ee was deprecated and removed. For the Java 11 environment, you need to download the JAXB lib from Maven Central and add it to your class or module path. For more on what’s missing from previous versions of Java, see “Explore new Java SE 11 & 12 APIs and language features.”)

This example uses the JAXB API to un-marshal an XML String to a person object.

But first, take a look at the application from the terminal to see the compiler and JVM options to compile and run such programs. Here is the project structure:

$tree /F /A

Open the files and explore them:

$cat src/eg/com/taman/Person.java
$cat src/eg/com/taman/PersonConverter.java

Compiling using javac:

$javac -d out/production/UnnamedModule-and-jeesrc/eg/com/taman/*.java

The exception messages are clear: The JAXB-related classes are not visible to the unnamed module. The reason is that the default set of root modules in java.se does not have the code requires transitive java.xml.bind. To fix the exception, you add the module by using the --add-modules java.xml.bind option, and it will compile successfully.

$javac --add-modules java.xml.bind -d out/production/UnnamedModule-and-jeesrc/eg/com/taman/*.java

It compiled this time. Now run the main class by using java command:

$java -cp out/production/UnnamedModule-and-jeeeg.com.taman.PersonConverter

This exception occurs because of the same reason: The module java.xml.bind has not been imported during runtime. In this case, however, the exception is less intuitive than it was during the compilation.

To fix the exception, you use the --add-modules option with the java command:

$java --add-modules java.xml.bind -cp out\production\UnnamedModule-and-jeeeg.com.taman.PersonConverter

Now it runs successfully and prints the message.

How did this work before Java 9?

In JDK 8 and older versions, JAXB worked without any extra configuration. This API and other Java EE APIs have been bundled with Java SE since version 6. In Java 9, the Java EE APIs are separated from Java SE via the module system, which reduces the size of the JRE so that it can run in smaller devices.

Currently, these APIs are disabled by default. Also, the modules that exported the Java EE API (like java.xml.bind) have been individually deprecated for removal in a future release.

The separate and stand-alone platforms of such modules will be released in the future, so the applications and libraries using these APIs can eventually migrate to those platforms.

Automatic modules

Probably the paramount question in your mind about automatic modules is how they can help you migrate and use non-modular third-party libraries in your modular applications without waiting for the libraries to migrate. You might also want to know which packages an automatic module exports and which modules it requires.

The anatomy of an automatic module

They are named modules that are automatically created for a non-modular JAR. It happens when the JAR is placed on the module-path (as a dependency) of a modular application. In other words, no modular JAR files become modular (or an automatic module) when used by a modular application.

Say you have a modular application called app (containing the module-info.class) that wants to use a third-party non-modular JAR file called lib.jar (that does not have the module-info.class). If you run the main class com.app.Main.class using the following commandthen lib.jar automatically becomes modular for app:

java --module-path appClasses;lib --module app/com.app.Main

This module has the same name as the JAR name (extension is dropped, hyphens, if any, are replaced by dots and the version part, if any, is also dropped). The module app must still explicitly import the JAR module by its name using the requires clause in module-info.java.

In this command, appClasses is the folder where app has its compiled classes and lib is the folder where you have the third-party lib.jar.

Here’s an example to make it clear. Open the project Automatic-Modules, which contains two applications. The first application represents a non-modular third-party library (a JAR file) and the second one is a modular application that will use the first library. Open your project in the IDE.

The non-modular library is named easy-calc; it is an old Java 8 application and contains a Calculator class with some basic mathematical operations.

Create a JAR file from this module as you did in “How to design packages and create modules, Part 2.” The exported JAR file name will be easy-calc.jar.

The second modular application is called calc.app and it contains module-info.java. To use the non-modular JAR file in the application, you add it as a dependency so it is available to the modular application module-path.

Use the easy-calc.jar packages to import the Calculator.java by requiring the JAR name into the modular application module-info.java module descriptor:

module calc.app {
    requires easy.calc;
}

Run the application. It will compile and run successfully. Try to omit the requires statement and see the error.

Let’s analyze the modular application using the jdeps tool:

$ jdeps --module-path mlib;out/production/calc.app -s --module calc.app

Use the --describe-module option for the easy-calc library:

$ jar --file mlib/easy-calc.jar --describe-module

and for the client:

$ java --module-path mlib;out/production/calc.app --describe-module calc.app

As an exercise, use the new Module API to analyze the modules. Open the class ModuleAnalyzer, explore it, run the file, and check the output.

Fine-tune the non-modular JAR file for modularity

If you are a tools, library, or framework maintainer, or even if you develop libraries in your work, here’s a piece of advice: Think about Java 9+ modularity even when your development efforts don’t depend on all or some of your code being modular.

By explicitly naming automatic Java modules (JAR files), you are heading in the right direction to eventually implement Java 9+ modularization. It is much more efficient to pick an explicit name for your JAR file than to depend on the ModuleFinder-based name derivation algorithm.

public interface ModuleFinder
A ModuleFinder finds modules during resolution or service binding. It can only find one module with a given name and it locates the first occurrence of a module of a given name and will ignore other modules of that name that appear in directories later in the sequence.

The naming of an automatic module is significant because later changes to that name will cause backward incompatibilities for the library.

It is such an easy step, but it can have a great impact on your library on the future, so just pick a module name and add it as an Automatic-Module-Name:<module name> entry to the library’s MANIFEST.MF. That’s it.

With this step, you make your library usable as Java module without moving the library itself to Java 9 or creating a module descriptor for the library (module-info.java).

The jdeps tool: Java dependency analysis

Now you can learn more about the Java dependency analysis tool jdeps.

What is the jdeps tool?

The jdeps command — introduced in Java 8 to help determine class and package dependencies — is another tool to help you migrate your code to Java 9. Another use of jdeps is to locate dependencies on internal APIs in prior versions that have become strongly encapsulated in Java 9.

To determine whether a class has any such dependencies, use the following command on your compiled Java 8 code:

jdeps --jdk-internals YourClassName.class

If you have many classes in a JAR file, use:

jdeps --jdk-internals YourJARName.jar

If this command produces no output, then your class or set of classes does not have any dependence on JDK internal APIs that are no longer accessible.

As an exercise (and a good practice), check every older compiled class or JAR file with the jdeps command to ensure that your code does not depend on JDK internal APIs.

Determining the modules you need

You can also discover module dependencies in Java 9 code, for instance, when you’re preparing to create a custom runtime, by using jdeps to determine your app’s module dependencies. For example, the Hello World module you developed in a previous tutorial depends only on java.base. You can confirm that by executing the following command from the /12/modularity/migration/HelloWorld-Module folder, which checks the eg.com.taman.hello module’s dependencies:

$ jdeps --module-path mlib -m eg.com.taman.hello

This produces the following output, showing the packages and modules that are used by the application:

eg.com.taman.hello
[file:///Users/mohamed_taman/Google%20Drive/Projects/JavaSE/Java-SE-Code-Examples/12/modularity/migration/HelloWorld-Module/mlib/eg.com.taman.hello.jar]
requires java.base (@9.0.4)
eg.com.taman.hello ->java.base
eg.com.taman.hello      -> java.io                   java.base
eg.com.taman.hello      ->java.langjava.base

The output shows that module eg.com.taman.hello depends on the java.base module and that the module specifically uses types from the java.base module’s java.io and java.lang packages.

The preceding command can also be written as:

jdepsmlib/eg.com.taman.hello.jar

In addition, you can use jdeps on a specific .class file, as in:

$ jdeps mods/eg.com.taman.hello/eg/com/taman/hello/HelloWorldApp.class

This produces the following output:

HelloWorldApp.class ->java.base
eg.com.taman.hello             -> java.io           java.base
eg.com.taman.hello             ->java.langjava.base

Now, what if you need more information about the application’s dependencies?

Make your jdeps output more verbose

If you would like more details, you can specify the -v (verbose) option as in the following command:

jdeps -v mlib/eg.com.taman.hello.jar

It produces the following:

........
requires java.base (@9.0.4)
eg.com.taman.hello ->java.base
eg.com.taman.hello.HelloWorldApp      ->java.io.PrintStreamjava.base
eg.com.taman.hello.HelloWorldApp      ->java.lang.Objectjava.base
eg.com.taman.hello.HelloWorldApp      ->java.lang.Stringjava.base
eg.com.taman.hello.HelloWorldApp      ->java.lang.Systemjava.base

This shows you precisely which packages, types, and modules the application uses. Knowing that the application requires only java.base, you can then use jlink to create a custom runtime containing only that module (example to come).

What if you need to visualize the application dependencies?

Use jdeps to produce DOT files for graphing tools

By using graphing tools (such as Graphviz download/Graphviz web-based), you can produce module dependency graphs using the DOT graph description language, which specifies a graph’s nodes and edges.

The jdeps tool can create DOT files with the --dot-output option:

$ jdeps --dot-output graphs jars/ eg.com.taman.hello.jar

This produces two .dot files in the current DOT folder:

  • summary.dot, the description of module eg.com.taman.hello‘s dependencies
  • eg.com.taman.hello.dot, the description of module eg.com.taman.hello‘s specific package dependencies

Like this:

$cat graphs\summary.dot

This is the content of the graph you produced by opening summary.dot in a text editor and copying and pasting its contents into the textbox on the web-based Graphviz site and clicking Generate Graph:

digraph "summary" {
  " eg.com.taman.hello" -> "java.base (java.base)";
}

The .dot extension is also used by Microsoft Word document templates. On systems with Microsoft Word installed, open the jdeps-produced .dot files directly from a text editor.

You can learn more about the DOT graph description language. You can also explore a complete list of jdeps options for Windows, macOS, and Linux.

I’ve mentioned the concept of reliable configuration as a foundational element of Java 9 throughout this series, so now I’d like to demonstrate the Java linker tool jlink that helps to build those configurations by making it easier to craft smaller custom runtimes and use them to execute applications.

I’ll show you how to use it to build custom runtime images, customizable JREs that can be used for microservices containers, embedded systems, IoT and other resource-limited devices, and Docker images.

Specifically, I’m going to touch on the following in this section:

  • Why the tool was created and what the jlink command does
  • How to find JRE modules
  • How to create a custom runtime for the sample Hello World application

So first, let’s discuss the problems we had previously that lead to creating such a great tool.

Usually, you run a program using the default JRE, but if you needed to create your own JRE, then jlink is the answer.

But why would you need to build your own JRE? Look at the following example. Suppose you have a simple Hello World program like this:

class HelloWorld{
    public static void main(String[]args) {
System.out.prinltn("Hello World");
    }
}

If you want to run this small program on your system, you need to install a default JRE. After installing the default JRE, you can happily run the small Hello World application. To execute this simple application, you require the following .class files:

  • HelloWorld.class
  • String.class
  • System.class
  • Object.class

These four .class files are enough to run the app.

The default JRE provided by Oracle contains about 4,300 predefined Java .class files. Let that sink in.

If you execute this application with the default JRE, then all the predefined class files will be executed. But if you only need three or four class files, why should you have to expense the overhead of all these outer class files?

The default JRE, which weighs in at about 203MB, executes all predefined class files whether you want to or not. This costs you 203MB to execute a simple 1KB of code. Talk about a waste of storage space, not to mention memory and the performance hit you take.

Using the default JRE for this type of application execution is like swatting a flea with a cannon. The size of the default JRE means that you will not be able to develop microservices or applications for resource-limited devices or environments, such as IoT sensors and the like.

This is the way Java 8 and previous versions kept the language from being an optimal choice to develop microservices and tiny device applications. Java 9 solved the problem with jlink. You can easily build a JRE that contains only the relevant classes. If you’re building an app that doesn’t support a GUI, you can build a runtime without the corresponding GUI modules that support Swing and JavaFX (which becomes a stand-alone SDK in JDK 11).

The deprecated JRE
Starting with Java SE 10, the JRE is deprecated and with the Java 11 release, it is totally removed. Now you build your own JRE, with just the resources your project requires, using jlink.

How to list and find the JRE’s modules

With modularization, the JRE is now a proper subset of the JDK. If you run the following command from the JRE’s bin folder, the result contains only the JRE’s 73 modules rather than the full listing of the JDK’s 95 modules:

$ java --list-modules

This number will change as Java evolves. (When you run this command from the Java 11 or 12 bin folder, it will return 71 modules because Java EE, JavaFX, and other irrelevant-to-JDK modules have been removed.)

In the next section, I’ll show you how to list JRE modules on a custom runtime that you produce with the jlink command. You’ll only see the module bundled with the new runtime.

Now, let’s build our custom JRE image, just enough for our application.

Create a custom runtime

Start your terminal and change the folder to the jre/HelloWorld-Module application developed earlier. List the folder structure of this project, using the following command:

$ tree -A -F

The program is in a module named eg.com.taman.hello. You can compile the module-based application in Java 9 by using the following command:

$ javac --module-source-path src -d mods -m eg.com.taman.hello

After compiling successfully, a folder with a HelloWorldApp.class file will be created. If you run this module-based application using the default JRE, you can use the command:

$ java --module-path mods -m eg.com.taman.hello/eg.com.taman.hello.HelloWorldApp

Hello world to Java SE 12 Platform Module System!

As discussed earlier, the Hello World application requires only a few class files:

  • String.class
  • System.class
  • Object.class

These class files are part of the java.lang package, which is part of the java.base module. So, if you want to run the HelloWorld program, only two modules are required:

  • eg.com.taman.hello
  • java.base module

With these two modules, you can create your own customized JRE to run this application. Build the JRE with the command:

$ jlink --module-path mods:"$JAVA_HOME"/jmods --add-modules eg.com.taman.hello--output HelloWorldJRE

The command options work like this:

  • --module-path specifies one or more folders in which to locate the modules that will be included in the runtime; in this case, the JDK’s jmods folder, which contains the modular JAR files for all the JDK’s modules.
  • --add-modules specifies which modules to include in the runtime; in this case, just eg.com.taman.hello and by default, it will include the modules it depends on and java.base.
  • --output specifies the folder in which the runtime’s contents are placed; in this case, the folder HelloWorldJRE, which will be placed in the folder from which you execute the preceding command (unless you specify additional path information) — if the folder already exists, an error occurs.

The JAVA_HOME environment variable must refer to JDK 9+’s installation folder on your system.

After executing this command successfully, you will find there is a HelloWorldJRE folder created, which is nothing but your customized JRE. You just follow a few steps to execute your program by using the customized JRE.

First navigate to the newly created folder:

$ cd HelloWorldJRE\bin

Run the new Hello World from within the custom runtime images just created:

$ ./java -m eg.com.taman.hello/eg.com.taman.hello.HelloWorldApp

Hello world to Java SE 12 Platform Module System!

By executing this command, you can happily run your Hello World application.

One last thing. Check the modules included in your custom runtime by executing the following command:

$ ./java --list-modules
eg.com.taman.hello
java.base@12

I hope that provided you a clear picture of how to use jlink to make your own JRE.

Summary

In this tutorial, you explored the compatibility and code migration tools and techniques available in Java SE 9, including:

  • The different techniques available in Java SE 9 to help you migrate your current code base
  • An understanding of the unnamed, root, and automatic modules
  • How to use the jdeps Java dependency analysis tool to migrate code to Java 9+
  • Why the jlink Java linker tool was invented and how to use it

You’ve been provided a hands-on experience of creating a custom runtime image for your application.

I hope the experience has provided you with a strong starting point to help modularize your existing and future Java code. Just remember two concepts and you’ll be a modularization guru in your development — reliable configuration and strong encapsulation.

Mohamed Taman