Java 9+ modularity: The difficulties and pitfalls of migrating from Java 8 to Ja...
source link: https://developer.ibm.com/technologies/java/tutorials/java-modularity-5/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
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).
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 andexports
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 notmodule-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.
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 moduleeg.com.taman.hello
‘s dependencieseg.com.taman.hello.dot
, the description of moduleeg.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.
Create a custom runtime with jlink
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.
The problem jlink solves
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’sjmods
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, justeg.com.taman.hello
and by default, it will include the modules it depends on andjava.base
.--output
specifies the folder in which the runtime’s contents are placed; in this case, the folderHelloWorldJRE
, 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.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK