© Alexandru Jecan  2017

Alexandru Jecan, Java 9 Modularity Revealed, https://doi.org/10.1007/978-1-4842-2713-8_8

8. Migration

Alexandru Jecan

(1)Munich, Germany

This chapter covers key concepts and tools used to ease migration to JDK 9. It covers common issues that can occur when we migrate existing Java applications to JDK 9 and suggests solutions and tips for solving migration problems.

First, why do we need migration? The answer is obvious: without migrating to JDK 9, we can’t use the powerful features introduced by Jigsaw, nor can we use the other features introduced in Java 9 by the other JEPs, like the following:

  • The Java Shell

  • The updates in the Process API

  • The HTTP 2 client

  • The Stack-Walking API

  • The Platform Logging API

  • The multi-release JAR files

By looking back in the history of Java, every time a new version of Java SE has been released, there were some changes that caused incompatibilities with the previous versions of Java. The supreme scope of Oracle has always been to provide backward compatibility as much as possible. Hence, the modularization of the JDK is such a disruptive change that backward compatibility can’t be 100 percent assured. Oracle struggled to offer the highest possible degree of backward compatibility, but there are some breaking changes that can affect the backward compatibility. This all depends on how our code is structured. Before starting talking about any compatibility issues, we must outline two very important things:

  • Code that uses internal JDK APIs might not work in JDK 9. Some changes may be necessary.

  • Code that uses only official Java SE platform APIs and supported JDK-specific APIs works in JDK 9 without any necessary change.

When deciding to migrate to Java 9 , it’s important to know the outcome we want to achieve:

  • We want our existing Java application to simply run on JDK 9, but we don’t want to have any modules defined inside our code .

  • We want to modularize only a part of our application and keep the other part not modularized.

  • We want to modularize the entire application.

We’ll explain each case in detail. For each case, suppose we have a Java application written in a version lower or equal to Java 8 and that we want to compile and run it using Java 9.

The first case involves only assuring that our application works on Java 9, without creating any modules. This means we stay on the class path and don’t use the newly introduced module path at all. Start by setting the JAVA_HOME environment variable to point to a JDK 9 installation and then compile and run our application without performing any changes inside the code. Our application most likely works on Java 9 if it’s not using JDK internal APIs. Most of the JDK internal APIs have been encapsulated in Java 9 and therefore can’t be accessed. A small number of the JDK internal APIs, the ones from the module jdk.unsupported, are still accessible, but all the other are inaccessible. When we talk about JDK internal APIs, we refer to both our application code and library code. It doesn’t matter if our application doesn’t make use of internal JDK APIs because if one of our libraries that we’re using inside our application makes use of JDK internal APIs, then our application will break anyway. Nevertheless, there are a couple of changes performed in JDK 9 that can eventually break our application, like the new versioning scheme or the new structure of the JDK and the JRE. These changes will be covered in detail throughout this chapter. The biggest disadvantage on relying just on the class path is that we can’t use two of the most important features brought by JDK 9: reliable configuration and strong encapsulation. That means we can’t, for instance, declare dependencies on other modules, we can’t hide the internals of parts of our application, and we also can’t create custom runtime images.

The second case involves modularizing only a part of our application and keeping the other part not modularized. This means we combine the module path with the class path: the part of the code that contains modules is on the module path, and the non-modularized part of the code is on the class path. A big problem is that by default, code from the module path can’t access types from the class path! Fortunately, there are at least two solutions for this. One solution is to take the code from the class path and turn it into automatic modules. This is useful especially for third-party JAR files that may not yet have been modularized by their maintainers. Another solution would be to use a new command-line option called --add-exports that exports our packages so they can be accessed from other modules or from the class path.

The third case involves completely modularizing our application. As a result, the class path won’t be used anymore. The entire code lies only on the module path. Each piece of code is included in a module defined by a module descriptor. No piece of code is residing outside of a module. This approach brings many advantages because we can use all the features that the Java Platform Module System offers, including strong encapsulation, reliable configuration, improved security, maintainability, reusability, scalability, and so on. We recommend following this approach and modularizing the entire application.

Note

The class path wasn’t removed in Java 9. It can still be used standalone or in combination with the module path.

There are three situations that we can have in Java 9 in correlation to the three use cases discussed earlier:

  • Only the class path is used: The module path isn’t used. Corresponds to the first use case mentioned earlier.

  • Both the class path and the module path are used: Corresponds to the second use case mentioned earlier .

  • Only the module path is used: The class path isn’t used. Corresponds to the third use case mentioned earlier.

We’ll start to learn some key concepts in order to be able later to migrate an application to Java 9. Many topics are covered in this chapter, because the migration topic is quite comprehensive. We introduce the key concepts that you need to know. Let’s start by presenting the new concept of automatic modules, which are a very important component in the migration to Java 9 ecosystem.

Automatic Modules

Automatic modules are a special type of modules used to ease migration to Java 9 and to accomplish backward-compatibility. An automatic module is a named module created after placing a JAR file onto the module path. An automatic module isn’t directly declared by the Java Platform Module System or by us—it’s generated automatically for a JAR file that we place on the module path.

Automatic modules bring a great benefit in the modularization landscape . They permit us to start modularizing our own code without needing to wait for all the needed libraries and frameworks to be modularized. It would have been extremely bad to have to wait until each maintainer of each third-party library or framework modularizes their work.

An automatic module is created by deriving a JAR file, modularizing it without modifying its contents. In this way, each JAR file can be treated like a module. Automatic modules help us work with modules instead of working with non-modularized JAR files. They represent a bridge for each JAR file to the modular world.

An automatic module has at least five important characteristics :

  • It requires transitive all the existing modules from the system, which comprise all our own modules plus all modules from the JDK image plus all the other automatic modules.

  • It exports and opens all of its packages.

  • It doesn’t consist of a module-info.class file in its top-level directory.

  • It can access every type from the unnamed module (from the class path).

  • It can’t declare that it has any dependencies to any other modules.

We stated previously that an automatic module exports and opens all of its packages. This means the following things:

  • All the packages from an automatic module are exported for being accessible at both compile-time and at runtime.

  • All the packages from an automatic module are open for being accessible using deep reflection.

Note

An automatic module isn’t explicitly declared by us because it’s automatically created when a JAR file is placed onto the module path.

An automatic module can access types on the class path and is useful especially for third-party code. Automatic modules are used for migrating existing applications to Java 9. Let’s suppose our application uses the Log4j library. If we put the Log4j JAR file on the module path, we can use it in our module by requiring it inside the module descriptor of our application:

module com.apress.myModule {
        requires log4j;
}

In this way, the Log4j JAR is turned into an automatic module and can be used in our modular application. We can access all the packages from the Log4j module because as an automatic module it exports all its packages by default.

We don’t have to wait until the maintainers of the Log4j library have modularized their library because we can turn the Log4j library in an automatic module and use it on the module path (even if the Apache committers were hard-working and have already modularized Log4j at the time of writing this book).

When using an automatic module, the only thing we have to know is the name of the automatic module that will be generated. For this, Jigsaw makes use of a filename-based algorithm we cover shortly.

Don’t worry if you run the preceding piece of code and see some warnings. The warnings have been deliberately added at runtime by the JDK team to make the users aware of the fact that they’re using automatic modules.

An automatic module requires transitive all the existing modules. If we require an automatic module, then we acquire readability to all the modules, because the automatic module requires transitive all modules. Publishing a module that requires an automatic module on public repositories such as Maven Central is discouraged. That’s because some of the properties of an automatic module, such as its exported packages, could change when it’s later converted into an explicit module. This makes the automatic module unstable and increases the level of risk considerably.

The names of the automatic modules are generated automatically by the Java Platform Module System unless we set them explicitly in the MANIFEST.MF file. The name of the automatic module can be defined directly inside the MANIFEST.MF file from the META-INF directory of the JAR file. Inside MANIFEST.MF we need to set a value to the attribute Automatic-Module-Name in order to define the name of the automatic module that will be generated:

Automatic-Module-Name: myModule

This solution gives us the advantage and flexibility of being able to choose the name of the automatic module. Alternatively, if we don’t set the automatic module name, Jigsaw will use an algorithm for deriving the name of the automatic module out of the name of the JAR, covered next.

Computing the Name of the Automatic Module

If the attribute Automatic-Module-Name isn’t set, the name of the automatic module is automatically derived from the name of the JAR file. If the attribute Automatic-Module-Name is set, but the JAR also contains a module-info.class file, then the information stored in the attribute Automatic-Module-Name is simply ignored. The name of the automatic module will be the same as the one defined inside the module-info.class file.

Next let’s talk about the filename-based algorithm used by Jigsaw for computing the name of the automatic module from the name of the JAR. Two strings are derived from the JAR file: the name of the automatic module and its version:

  1. The .jar suffix is removed from the name of the JAR file. The resulting string is further used for determining and extracting the name and the version of the automatic module.

  2. The module name is extracted. According to the JDK 9 API documentation, “if the name matches the regular expression -(\d+(\.|$)), then the module name will be derived from the subsequence preceding the hyphen of the first occurrence. The subsequence after the hyphen is parsed as a version and it is ignored if it can’t be parsed as a version.”

  3. Some replacements on the name of the module are performed. The JDK 9 API documentation states that “all non-alphanumeric characters ([^A-Za-z0-9]) in the module name are replaced with a dot (‘.’), all repeating dots are replaced with one dot, and all leading and trailing dots are removed.”

Table 8-1 shows some examples of deriving the name and the version from a couple of JAR files. The first column represents the name of the JAR file, and the second and third columns represent the automatically extracted name of the module and version, respectively. The fourth column tells us if an error occurred or not.

Table 8-1. Examples of Deriving Module Names and Versions from JAR Files

Name of JAR

Name of Module

Version of Module

Error

guava-19.0.jar

guava

19.0

no

hadoop-common-2.8.0.jar

hadoop.common

2.8.0

no

mockito-all-2.0.2-beta.jar

mockito.all

2.0.2-beta

no

spark-core_2.10-2.1.0.jar

-

-

yes

spring-core-4.3.7.RELEASE.jar

spring.core

4.3.7.RELEASE

no

com.apress.myModule0.0.1.jar

-

-

yes

log4j-1.2.17.redhat-2.jar

log4j

1.2.17.redhat-2

no

jackson-core-2.9.0.pr3.jar

jackson.core

2.9.0.pr3

no

jaxrs-api-3.0.12.Final.jar

jaxrs.api

3.0.12.Final

no

maven-plugin-api-3.5.0-beta-1.jar

maven.plugin.api

3.5.0-beta-1

no

123.jar

-

-

yes

1my-module.jar

-

-

yes

The name and version of the guava-19.0.jar JAR file could be successfully derived. According to the filename-based algorithm , first the jar suffix is deleted. The result string is "guava-19.0". Afterward, the name of the module is extracted by searching for the first occurrence of the hyphen. The new string is extracted from the beginning of the resulting string until the last position before the hyphen. In our case, the string found is "guava", which corresponds to the name of the module. The string after the hyphen represents the version of the module: "19.0".

The name and version of the JAR hadoop-common-2.8.0.jar can be successfully extracted. The name of the module is hadoop.common because the hyphen from hadoop-common is replaced with a dot.

There are situations when we’re not able to extract the name of the automatic module. An example is the JAR file called spark-core_2.10-2.1.0.jar. When attempting to extract its name, we get the following error:

Unable to derive module descriptor for: spark-core_2.10-2.1.0.jar
spark.core.2.10: Invalid module name: '2' isn’t a Java identifier

The filename-based algorithm searches for the last hyphen in the string "spark_core-2.10-2.1.0" and splits the string into name and version. The resulting string for the name is "spark_core-2.10". The hyphens on this string are replaced with dots. Therefore, the string "spark.core.2.10" is computed as the name of the module. However, this string is invalid because it contains the identifiers 2 and 10, which aren’t valid as Java identifiers. As a result, an error is thrown, and the name of the automatic module can’t be extracted. If we place this JAR on the module path, we get the following exception:

java.lang.module.ResolutionException: Unable to derive module descriptor for: spark-core_2.10-2.10.jar

We get the same kind of error when we attempt to extract the module name from our com.apress.myModule0.0.1.jar, from our 123.jar, or from our 1my-module.jar:

Unable to derive module descriptor for: com.apress.myModule0.0.1.jar
com.apress.myModule0.0.1: Invalid module name: '0' isn’t a Java identifier


Unable to derive module descriptor for: 123.jar
123: Invalid module name: '123' isn’t a Java identifier


Unable to derive module descriptor for: 1my-module.jar
1my.module: Invalid module name: '1my' isn’t a Java identifier

Attempting to place any of these three JARs on the module path will result in a ResolutionException being thrown.

Note

A fatal error is thrown while placing a JAR on the module path for which the module name can’t be extracted .

For the JAR commons-lang3-3.0.jar, the automatic module’s name is commons.lang3. As we can observe, the digits are preserved at the end of the module name.

The JDK 9 specification recommends that the modules names follow the reverse Internet domain-name convention. According to the specification, “a module’s name should correspond to the name of its principal exported API package, which should also follow that convention. If a module doesn’t have such a package, or if for legacy reasons it must have a name that doesn’t correspond to one of its exported packages, then its name should at least start with the reversed form of an Internet domain with which the author is associated.”

Describing a JAR File

If we have a JAR that we want to use as an automatic module and want to find out what kind of name the JPMS system derives out of it, we can use the --describe-module option of the jar tool:

jar --describe-module --file <our_JAR_name>

The --describe-module option prints the following:

  • The name of the module and the version

  • The module descriptor

  • The entire list of packages that the JAR consists of

Listing 8-1 displays the results of running the jar command with the --describe-module option on the guava.jar file.

Listing 8-1. Running jar --describe-module on the guava-19.0.jar File
$ jar --describe-module --file guava-19.0.jar
No module descriptor found. Derived automatic module.


[email protected] automatic
requires java.base mandated
contains com.google.common.annotations
contains com.google.common.base
contains com.google.common.base.internal
contains com.google.common.cache
contains com.google.common.collect
contains com.google.common.escape
contains com.google.common.eventbus
contains com.google.common.hash
contains com.google.common.html
contains com.google.common.io
contains com.google.common.math
contains com.google.common.net
contains com.google.common.primitives
contains com.google.common.reflect
contains com.google.common.util.concurrent
contains com.google.common.xml
contains com.google.thirdparty.publicsuffix

The module system finds no module descriptor inside the guava-19.0.jar, so it derives an automatic module out of the JAR file. The new automatic module has the name “guava” and the version “19.0”. It requires java.base and consists of the packages listed above.

No Support for Automatic Modules at Link-time

There’s a limitation regarding the use of automatic modules at link-time using Jlink. There’s no support for automatic modules at link-time, which means that linking automatic modules into a runtime image is deliberately not supported. Automatic modules can’t be used with Jlink because they have access to the class path. That means that if automatic modules were hypothetically supported by Jlink, errors of type NoClassDefFoundError would have been thrown at runtime.

Note

It isn’t possible to create a runtime with Jlink unless all the components are standard modules (not automatic modules).

The ModuleDescriptor class of the new module API contains a method called isAutomatic(). This method returns true if the module is an automatic one and false otherwise. We talk about the new module API and the API support for automatic modules in the next chapter.

Note

Automatic modules open all their packages by default so we don’t need to use the option --add-opens when working with automatic modules.

Now that we’ve covered almost everything we need to know about automatic modules, it’s time to look at the JDeps tool, which is an extremely important tool used to find dependencies of a library.

The JDeps Tool

The Java Dependency Analysis Tool (JDeps) is a command-line tool used for different purposes: to discover all the static dependencies of a library, to discover the usages of internal JDK APIs, or to automatically generate a module descriptor for a JAR file. The tool was introduced in Java 8 but enhanced in Java 9 with some useful new options and features. It can be found in the bin directory of the JDK. JDeps is a very useful tool for migration to Java 9. We’ll explain why.

Find Dependencies of Unsupported JDK Internal APIs

JDeps has an option called --jdk-internals that finds dependencies of any unsupported JDK internal APIs that are private to the JDK implementation. Its syntax is as follows:

jdeps --jdk-internals --class-path <input_file>

As an input, we can specify a JAR file or a .class file that will be analyzed.

Listing 8-2 shows an example of using JDeps with the option --jdk-internals by taking the Guava library and checking whether it has any unsupported APIs.

Listing 8-2. Running jdeps --jdk-internals on the JAR File guava-19.0.jar
$ jdeps --jdk-internals guava-19.0.jar
guava-19.0.jar -> jdk.unsupported
   com.google.common.cache.Striped64 -> sun.misc.Unsafe JDK internal API (jdk.unsupported)
   com.google.common.cache.Striped64$1 -> sun.misc.Unsafe JDK internal API (jdk.unsupported)
   com.google.common.cache.Striped64$Cell -> sun.misc.Unsafe JDK internal API (jdk.unsupported)
   com.google.common.primitives.UnsignedBytes$LexicographicalComparatorHolder$UnsafeComparator -> sun.misc.Unsafe
JDK internal API (jdk.unsupported)
   com.google.common.primitives.UnsignedBytes$LexicographicalComparatorHolder$UnsafeComparator$1 -> sun.misc.Unsafe
JDK internal API (jdk.unsupported)
   com.google.common.util.concurrent.AbstractFuture$UnsafeAtomicHelper -> sun.misc.Unsafe
JDK internal API (jdk.unsupported)
   com.google.common.util.concurrent.AbstractFuture$UnsafeAtomicHelper$1 -> sun.misc.Unsafe
JDK internal API (jdk.unsupported)


Warning: JDK internal APIs are unsupported and private to JDK implementation that are
subject to be removed or changed incompatibly and could break your application.
Please modify your code to eliminate dependency on any JDK internal APIs.
For the most recent update on JDK internal API replacements, please check:
https://wiki.openjdk.java.net/display/JDK8/Java+Dependency+Analysis+Tool


JDK Internal API                         Suggested Replacement
----------------                         ---------------------
sun.misc.Unsafe                          See http://openjdk.java.net/jeps/260

In the output we can see that JDeps finds all the JDK internal libraries that the Guava library is using. In our case, it finds the JDK internal class sun.misc.Unsafe in five different locations, listed in the preceding code.

JDeps also suggests replacements for the internal APIs found. For sun.misc.Unsafe it suggests taking a look on the Open JDK website at JEP 260. In general, JDeps is capable of giving clear information about possible replacements by suggesting the class name that could be eventually used instead.

In order to prepare for using Java 9, JDeps is very useful because we can check whether our JAR files from the class path are making use of JDK internal APIs. It isn’t mandatory to replace the JDK internal APIs with the ones suggested by JDeps. We can replace them with whatever library we want. But we should replace them so that we can compile and run our application with JDK 9.

Note

JDeps can be applied to modules as well.

Generate Module Descriptors with JDeps

JDeps can be used to generate a module descriptor for one or more JAR files using the command-line option --generate-module-info:

jdeps --generate-module-info <output_directory> <list_of_jar_files>

This command gets two parameters :

  • <output_directory> represents the directory where the module-info.java files will be created.

  • <list_of_jar_files> represents one or more JAR files for which a module-info will be generated. The list is separated by a blank space. For each JAR, each dependency must be listed here.

This command creates a module descriptor module-info.java for each JAR file that we pass. To demonstrate this, next we generate a module-info.java file for the junit-4.12.jar file. We also have to pass the hamcrest-core-1.3.jar file because this JAR is a dependency of JUnit:

jdeps --generate-module-info output hamcrest-core-1.3.jar junit-4.12.jar

As a result, two module-info files are created inside the output directory, one for JUnit and one for Hamcrest Core:

outputjunitmodule-info.java
outputhamcrest.coremodule-info.java

Listing 8-3 shows an excerpt of the module-info.java files created.

Listing 8-3. Module Descriptors for JUnit and Hamcrest Core That Were Generated by JDeps
 module junit {
    requires transitive hamcrest.core;
    requires java.management;
    exports junit.extensions;
    exports junit.framework;
    exports junit.runner;
    exports junit.textui;
    exports org.junit;
    exports org.junit.experimental;
    ...
    exports org.junit.runners;
    exports org.junit.runners.model;
    exports org.junit.runners.parameterized;
    exports org.junit.validator;
}


module hamcrest.core {
    exports org.hamcrest;
    exports org.hamcrest.core;
    exports org.hamcrest.internal;
}
Note

The module descriptor generated by JDeps exports by default all the existing packages of its corresponding JAR.

JDeps can also generate a module-info.java file for an open module. The command is generate-open-module and it takes the same type of parameters:

jdeps --generate-open-module <output_director> <name_of_jar_file>

The only difference is that this command creates a module descriptor that defines an open module instead of a simple module. As a result, no packages are exported. This is suitable for frameworks that access the JDK using reflection.

JDeps also provides other useful features. Table 8-2 shows a list of the most useful options offered by JDeps, as defined in the JDK 9 API specification.

Table 8-2. JDeps Options

JDeps Option

Description

--check <module_name>

[,<module_name>...

Prints the module descriptor and the resulting module dependences after analyzing the dependence of the specified modules

--list-deps

Lists the dependences of JDK internal APIs

--class-path <path>

Specifies the path where to find class files

--module-path <module_path>

Specifies the module path

--upgrade-module-path <module_path>

Specifies the upgrade module path

--module <name_module>

Specifies the root module that will get analyzed

--multi-release <version>

Specifies the version for processing multi-release JAR files

-filter:module

Filters dependences within the same module

--regex <regex>

Finds dependences matching the given pattern

We’ve looked at automatic modules and JDeps. It’s time to focus on the Java 9 encapsulation topic. We’ll learn how to break the encapsulation in Java 9, how to open packages and modules, and how to use the --add-opens, --add-reads, and --add-modules command-line options.

Encapsulation in Java 9

Java has two categories of APIs in the JDK: supported APIs and unsupported APIs. The supported APIs comprise JCP standard APIs like java.* and javax.*, JDK-specific APIs like com.sun.* and jdk.*. These APIs are intended to be used outside the JDK.

The unsupported APIs comprise the sun.* packages. These APIs were never intended for external use outside the JDK. Typically all the packages that contain the name "internal" are JDK internal APIs. The problem is that in the past many developers used the sun.* packages, even if they were told they weren’t allowed to use these packages outside the JDK.

Java 9 encapsulated almost all the JDK internal APIs, which means that by default, without any hacks, these APIs aren’t accessible either at compile-time nor at runtime.

Note

Oracle made a study that revealed the most-used JDK internal classes: sun.misc.BASE64Encoder, sun.misc.BASE64Decoder, and sun.misc.Unsafe.

The JCP team put the JDK-internal APIs into two categories: non-critical JDK-internal APIs and critical JDK-internal APIs.

The non-critical JDK-internal APIs category comprises the APIs that are used outside the JDK to an extremely low degree. Therefore, the risk of breaking applications by encapsulating these APIs is also low. This category of APIs also contains the sun.misc.BASE64Encoder and sun.misc.BASE64Decoder classes.

The critical JDK-internal APIs category comprises the APIs whose functionality would be extremely difficult to implement outside the JDK. It’s demanding, if not almost impossible, to develop replacements for these APIs outside the JDK. This category contains, for instance, the sun.misc.Unsafe class, which was marked as critical because it’s very demanding to build a similar class outside the JDK.

Therefore, the JCP team decided to do the following:

  • Encapsulate all non-critical internal APIs

  • Encapsulate all critical internal APIs for which supported replacements exist in JDK 8

  • Not encapsulate critical internal APIs, but just deprecate them

The critical internal APIs that weren’t encapsulated are sun.misc.Unsafe, sun.misc.Signal, sun.misc.SignalHandler, sun.misc.Cleaner, sun.reflect.Reflection, sun.reflect.ReflectionFactory. These are still accessible in JDK 9.

The following example from Listing 8-4 demonstrates the encapsulation of JDK internal APIs. Therefore, we use an instance of the class URLCanonicalizer from the package sun.net. All the classes from package sun.net were encapsulated in JDK 9.

Listing 8-4. Use of a Class from an Internal JDK API
package com.apress.jdkinternal;

import sun.net.URLCanonicalizer;

public class Main {

        public static void main(String[] args) {
                URLCanonicalizer urlCanonicalizer = new URLCanonicalizer();
                String apressUrl =  urlCanonicalizer.canonicalize("www.apress.com");
                System.out.println(apressUrl);
        }
}

The compilation fails because we try to access a JDK internal API that is encapsulated:

error: package sun.net isn't visible
import sun.net.URLCanonicalizer;
  (package sun.net is declared in module java.base, which doesn't export it to module com.apress.jdkinternal)

The error states that the package sun.net, located in module java.base, isn’t visible from our module com.apress.jdkinternal. We know that the sun.net package has been encapsulated, so we need a way to make our module com.apress.jdkinternal able to access the sun.net package at compile-time.

Fortunately, there’s a solution to gain access to the sun.net package—by exporting this package to our module during compilation using the --add-exports command-line option, described next.

Exporting a Package at Compile-time and Runtime

The --add-exports option added to the Java compiler (javac) exports a package to a specific named module or to the unnamed module. It corresponds to the qualified export "exports … to" statement from the module declaration. It can be used to break the encapsulation of JDK internal APIs and to make them accessible in a named module or in the unnamed module.

The following shows the syntax of the --add-exports command-line option:

--add-exports <source_module>/<name_of_package_to_be_exported>=<list_of_target_modules>
  • <source_module> represents the module where the package to be exported is located.

  • <name_of_package_to_be_exported> represents the name of the package that will be exported to the <list_of_target_modules>.

  • <list_of_target_modules> represents a comma-separated list of modules that will gain access to the exported package.

In Figure 8-1 the package sun.net, located in module java.base, is exported to our module com.apress.jdkinternal .

A431534_1_En_8_Fig1_HTML.gif
Figure 8-1. Exporting the sun.net package to a named module with the --add-exports option

In this way, the package sun.net will be accessible from our the module com.apress.jdkinternal. By compiling our application again using the option --add-exports mentioned earlier, the package sun.net will be exported to our module. We pass the module where the package is located (java.base) and the module where the package should be exported (com.apress.jdkinternal).

To compile we have to use the --add-exports option, as mentioned:

$  javac –d outputDir --add-exports java.base/sun.net=com.apress.jdkinternal --module-source-path src $(find . –name "*.java")

The compilation succeeds, and the .class files are created. However, a warning is displayed, informing us that URLCanonicalizer is an internal proprietary API that may be removed in a future release:

warning: URLCanonicalizer is internal proprietary API and may be removed in a future release
import sun.net.URLCanonicalizer;

We run the application by using exactly the same --add-exports command:

java --module-path outputDir --add-exports java.base/sun.net=com.javausergroup.jdkinternal -m com.apress.jdkinternal/com.apress.jdkinternal.Main

Because we need readability at runtime, not only at compile-time, it’s mandatory to use the same --add-exports option with the same arguments when running the application. If we had run our application without the --add-exports flag, the following error would have been thrown:

Exception in thread "main" java.lang.IllegalAccessError: class com.apress.jdkinternal.Main (in module com.apress.jdkinternal) can’t access class sun.net.URLCanonicalizer (in module java.base) because module java.base doesn’t export sun.net to module com.apress.jdkinternal
        at com.apress.jdkinternal/com.apress.jdkinternal.Main.main

The IllegalAccessError occurs at runtime because the package sun.net isn’t exported.

Export to the Unnamed Module

We exported an unsupported package to our module to make it accessible. But what if our code was on the class path? Fortunately, there’s a solution for that. The constant ALL-UNNAMED stands for the entire class path. In our case, the following command exports the sun.net package to the class path so it can be accessed from the entire code on the class path:

--add-exports java.base/sun.net=ALL-UNNAMED
Note

The constant ALL-UNNAMED stands for all the code in the unnamed module, which represents the entire class path.

There are some general aspects that want to mention:

  • The --add-exports command-line option can be used more than once when running it, meaning it allows duplicates.

  • The --add-exports command-line option is used by both Java compiler and Java launcher.

  • If our code uses only the JDK-critical APIs that remained accessible in Java 9, we don’t have to use the --add-exports option because these APIs are already accessible.

  • If the --add-exports command-line option encounters bad values, a warning is raised, but no fatal error is thrown, so the program doesn’t stop working.

Throughout this book we’ve looked at two ways of exporting a package: by specifying the exports clause in the module declaration or by using the command-line option --add-exports. But there’s still another option: specifying the attribute Add-Exports in the MANIFEST.MF file of a JAR file. This attribute has the format module/package. It exports the specified module from the specified package to the unnamed module. For instance, in order to export the package sun.net from module java.base to the unnamed module, we could write the following:

Add-Exports: java.base/sun.net

In this section, we’ve learned to make code that uses encapsulated JDK APIs compile and run in JDK 9. This workaround is very useful because if our code uses JDK internal APIs, we know that we have one solution to make the code run in Java 9 without needing to redesign the code or replace the encapsulated JDK internal APIs. But relying on this flag forever isn’t recommended because the JDK internal APIs were deprecated in JDK 9 and may be removed in JDK 10. That means you have time only during a release cycle to refactor your code in order to get rid of these unsupported APIs. If your third-party library is using JDK internal APIs, you should check on a regularly basis if a new version of the library, one that replaces the unsupported APIs with supported ones, has been published.

Opening Packages for Deep Reflection

Chapter 4 talked about the opens clause in the module descriptors. There we mentioned that deep reflection is by default allowed by code in a named module to code on the class path, but by default it’s not allowed to code in another named module. In this second case, in order to allow reflective access from code in a named module to code in another named module, we could use the new --add-opens command-line option. It’s used to provide deep reflective access from one module to another module or to the code on the class path. It’s equivalent to a qualified opens from a module declaration:

opens <package_name> to <list_of_target_modules>

The syntax of the add-opens command-line option goes like this:

--add-opens <source_module>/<name_of_package_to_be_opened>=<list_of_target_modules>

The package defined and located in the <source_module> is opened for deep reflective access to the modules listed in the <list_of_target_modules>. These modules will be able to access the package at runtime only using deep reflection but won’t be able to access the package during compilation. If we put the constant ALL-UNNAMED instead of the <list_of_target_modules>, then the entire code on the class path will be able to access the package at runtime using deep reflection. However, this last case happens already by default, so we should use it only if someone programmatically disabled reflective access by code in a named module to code from the class path.

Note

Deep reflection can take place only at runtime. It can’t take place at compile-time. As a result, the --add-opens command-line option can be used only at runtime using the java command. It can’t be used at compile-time using the javac command.

Because automatic modules open all their packages by default, there’s no need to open them using the --add-opens option. Now that we know how to open packages at runtime for deep reflection, let’s learn how to add readability at runtime using the option --add-reads.

Providing Readability Between Modules

The --add-reads command-line option is used at both compile-time and runtime to add readability from a module to another module. Its syntax is as follows:

--add-reads <source_module>=<list_of_target_modules>

By using the --add-reads command-line option, the <source_module> gets readability to all the modules represented by the <list_of_target_modules>, which means that the <source_module> will require all those modules. This is equivalent to providing a requires clause in a module descriptor:

module <source_module> {
        requires target_module_A;
        requires target_module_B;
}

We can make a module <source_module> read the entire class path by providing the constant ALL-UNNAMED to the --add-reads option:

--add-reads <source_module>=ALL-UNNAMED                

This option is used merely during testing—for instance, when a module is patched at compile-time and at runtime in order to add tests in the same modules as the module under test. During testing, we might need a module to read another module, although the first module doesn’t depend on the other module because it doesn’t define a requires directive to the other module. By using the --add-reads option, we get readability between the two modules. The first module will be able access all the exported types from the other module.

Suppose we have a Junit test class inside our module com.apress.testing. This class extends a class from the Junit library. So we need a readability relation from our module com.apress.testing to the automatic module junit. This can be achieved very simply using the --add-reads option:

--add-reads com.apress.testing=junit

If we have the Junit library on the class path and don’t want to move to the module path, then we use the ALL-UNNAMED constant to provide readability between our module and the entire code on the class path:

--add-reads com.apress.testing=ALL-UNNAMED

Chapter 11 provides an explanatory example of using the --add-reads command-line option for running JUnit tests.

Note

The --add-reads command allows duplicates and if it encounters bad values, a warning is raised, but no fatal error is thrown. If duplicates are found, only the first class will be taken into consideration.

Before we discuss the --add-modules command-line option, there’s one more thing worth mentioning: when we use reflection on a member in a module, readability is automatically granted.

Adding Modules to the Root Set

The --add-modules command-line option is used to add modules directly to the set of root modules. Hence, the modules will be resolved. This option is used to resolve modules that aren’t resolved by default.

Its syntax is simple. It takes one or more modules separated by comma:

--add-modules <module_name>(,<module_name)*

<module_name> represents the name of a module that will be added to the default set of root modules

There are three values that can be used with the --add-modules option instead of specifying a list of modules:

  • ALL-DEFAULT: The official specification released for JDK 9 states that by using the ALL-DEFAULT option “the default set of root modules for the unnamed module, as defined above, is added to the root set. This is useful when the application is a container that hosts other applications which can, in turn, depend upon modules not required by the container itself.”

  • ALL-SYSTEM: This option adds all the system modules to the root set.

  • ALL-MODULE-PATH: This option adds all the observable modules found on the module path to the root set. It’s helpful to be able to add each module from the module path at once to the root set. For a large list of automatic modules, it’s more practical and easier to add all of them at once and without the need to enumerate them one by one. Maven uses this option considerably because it needs all the modules from the module path.

The --add-modules option is also used by Jlink to set the root module inside the runtime image. We saw in Chapter 7 how to create a runtime image and how to add modules to the runtime image using the --add-modules command-line option.

Note

Both javac and java support the --add-modules command-line option.

The --add-modules option can be repeated. The following usages of --add-modules options have the same effect and cause no errors:

--add-modules com.apress.moduleA --add-modules com.apress.moduleB
--add-modules com.apress.moduleA,com.apress.moduleB

Next we’ll look at an explanatory example to better understand when we should use the --add-modules option. We download the junit-4-12.jar and the hamcrest-core-1.3.jar and put them into a folder. We run jdeps -s on the entire folder to find all the dependencies of the two JAR files:

 $ jdeps -s *.jar
hamcrest-core-1.3.jar -> java.base
junit-4.12.jar -> hamcrest-core-1.3.jar
junit-4.12.jar -> java.base
junit-4.12.jar -> java.management

Hamcrest-Core depends only on java.base, meaning it use types only from java.base. Hence, Junit is depending on Hamcrest-Core because it uses types from it. If we look inside the content of Junit, we can see lots of imports from Hamcrest-Core packages.

Suppose we have a module com.apress.myModule and we put the junit-4.1.2.jar and the hamcrest-core-1.3.jar files on the module path in order to use them as automatic modules. You already learned at the beginning of this chapter, in the “Automatic Modules” section, that an automatic module can’t declare dependencies on other modules. So we can’t use the directive requires hamcrest-core because we don’t have a module descriptor available where to place it, because an automatic module doesn’t have a module descriptor module-info.java. The situation is depicted in Figure 8-2.

A431534_1_En_8_Fig2_HTML.gif
Figure 8-2. Relation between named modules and automatic modules that have dependencies

Module com.apress.myModule contains in its module descriptor a requires junit clause. The automatic module junit uses types from the automatic module hamcrest.core. The module graph contains the modules com.apress.myModule and junit. The module hamcrest.core isn’t added to the module graph because it can’t be identified during the resolution process. There’s no requires clause inside the junit automatic module so that the module system could discover the hamcrest.core automatic module and add it to the module graph. This means we have to manually add the hamcrest.core automatic module to the module graph using the --add-modules option at both compile-time and runtime :

--add-modules hamcrest.core

If we don’t add the hamcrest.core module to the module graph, the classes from the hamcrest.core won’t be found and an exception of type ClassNotFoundException will be thrown at runtime.

Another option provided by default in JDK is the --illegal-access one, covered in the next section.

The --illegal-access Option

The --illegal-access option was added into JDK 9 to ease migration. This option states that code on the class path can perform illegal reflective access by default.

Note

Illegal reflective access means access using reflection to types in named modules only for code in the class path.

With the --illegal-access option, the code on the class path gets reflective access to types in any named modules. The reflective access is done using standard reflection-related APIs like java.lang.reflect and java.lang.invoke. --illegal-access is very useful for third-party frameworks like Spring, Hibernate, or Guava, which were so designed that they need to perform reflective access inside the internals of the JDK in order to be able to work properly.

Note

The --illegal-access option allows reflective access only for code on the class path to types in any named modules. It doesn’t allow reflective access for code in named modules to types in other named modules.

The syntax of the --illegal-access option is as follows:

java --illegal-access <options>

The --illegal-access option can take one of four possible parameters: permit, warn, debug, and deny:

  • --illegal-access=permit: The permit mode represents the default behavior in Java 9. It states that every package from every module is opened for deep reflection to code in all the unnamed modules. The unnamed modules represent the class path. This means that at runtime the code from the class path can access the entire information stored in modules using deep reflection. A warning will be displayed during the first access.

  • --illegal-access=warn: The warn mode is very similar to the permit mode previously discussed. The only difference is that the warn mode returns a warning each time an illegal access is performed using reflection.

  • --illegal-access=debug: The debug mode shows a warning in the stack trace for every illegal access performed using reflection.

  • --illegal-access=deny: The deny mode disables all the illegal access operations using reflection. When this mode is set, no illegal access can be done using reflection. Hence, this mode can be overwritten by the command-line option --add-opens. With --add-opens we’re able to open specific packages for reflection.

Note

The JDK internal APIs aren’t encapsulated at runtime.

However, the JCP team announced that the --illegal-access option will be removed in JDK 10. It will be available only in JDK 9 in order to ease migration of third-party libraries that were constructed by making use of deep reflective access into the internals of the JDK. The --illegal-access option has not been planned right from the beginning. It was added later in order to ease migration to Java 9, because an important number of external libraries and frameworks use reflection to access the internal APIs of the JDK.

This option emits warning messages when used:

WARNING: Illegal access by A to B (permitted by C)
  • A is the name of the type that contains the code that invoked the reflective operation in question.

  • B is the name of the member being accessed.

  • C is the name of the command-line option that enabled this access.

Note

The --illegal-access option is set by default in JDK 9.

The next example shows what kind of warning messages are displayed when a library performs illegal reflective access to types in the JDK. We run the java -jar command on the the JRuby Complete-9.1 JAR file:

$ java -jar jruby-complete-9.1.12.0.jar

WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by org.jruby.util.io.FilenoUtil (file:/C:/Users/Alex/Downloads/jruby-complete-9.1.12.0.jar) to method sun.nio.ch.SelChImpl.getFD()
WARNING: Please consider reporting this to the maintainers of org.jruby.util.io.FilenoUtil
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release

A warning message lets us know that the jruby-complete-9.1.12.0.jar performs an illegal reflective access operation on method sun.nio.ch.SelChImpl.getFD() of the JDK. This illegal reflective access is allowed, because the --illegal-access flag is set by default.

If we want to see a warning for every illegal access performed, we can use the mode=warn of the --illegal-access command-line option:

$ java --illegal-access=warn -jar jruby-complete-9.1.12.0.jar

WARNING: Illegal reflective access by org.jruby.util.io.FilenoUtil (file:/C:/Users/Alex/Downloads/jruby-complete-9.1.12.0.jar) to method sun.nio.ch.SelChImpl.getFD()
WARNING: Illegal reflective access by org.jruby.util.io.FilenoUtil (file:/C:/Users/Alex/Downloads/jruby-complete-9.1.12.0.jar) to field sun.nio.ch.FileChannelImpl.fd
WARNING: Illegal reflective access by org.jruby.util.io.FilenoUtil (file:/C:/Users/Alex/Downloads/jruby-complete-9.1.12.0.jar) to field java.io.FileDescriptor.fd
WARNING: Illegal reflective access by jnr.posix.JavaLibCHelper (file:/C:/Users/Alex/Downloads/jruby-complete-9.1.12.0.jar) to method sun.nio.ch.SelChImpl.getFD()
WARNING: Illegal reflective access by jnr.posix.JavaLibCHelper (file:/C:/Users/Alex/Downloads/jruby-complete-9.1.12.0.jar) to field sun.nio.ch.FileChannelImpl.fd
WARNING: Illegal reflective access by jnr.posix.JavaLibCHelper (file:/C:/Users/Alex/Downloads/jruby-complete-9.1.12.0.jar) to field java.io.FileDescriptor.fd
WARNING: Illegal reflective access by jnr.posix.JavaLibCHelper (file:/C:/Users/Alex/Downloads/jruby-complete-9.1.12.0.jar) to field java.io.FileDescriptor.handle
WARNING: Illegal reflective access by org.jruby.java.invokers.RubyToJavaInvoker (file:/C:/Users/Alex/Downloads/jruby-complete-9.1.12.0.jar) to method java.lang.Object.clone()
WARNING: Illegal reflective access by org.jruby.java.invokers.RubyToJavaInvoker (file:/C:/Users/Alex/Downloads/jruby-complete-9.1.12.0.jar) to method java.lang.Object.finalize()
...

The output is very long, so we decided not to include it all. You can see that the type that performs the illegal reflective access is displayed together with the name of the method or the field from the JDK that’s accessed reflectively.

The JDK 9 API introduced a new useful method in the AccessibleObject class of package java.lang.reflect called boolean canAccess(Object object). This method lets us test whether the caller can access this reflected object. The method returns true if access is allowed and false otherwise. According to the JDK 9 API documentation, an IllegalArgumentException will be thrown if “this reflected object is a static member or constructor or if it is an instance method or field and the given object is null.” To avoid any exceptions, we could also use the new boolean trySetAccessible() method from the same class. This method doesn’t throw any exceptions except a SecurityException if the request is denied by the Security Manager.

Note

The system property sun.reflect.debugModuleAccessChecks=access allows us to get a stack trace on each warning. It also can help to debug exceptions raised by the use of --illegal-access.

We talked about the command-line flags. Now it’s time to present some migration issues that can commonly occur.

Migration Issues

This section explains the concepts and gives practical solutions to some of the most common issues that usually occur during the migration process to Java 9:

  • Encapsulated JDK internal APIs

  • Not resolved modules

  • Cyclic dependencies

  • New versioning scheme

  • Split packages

  • Removed methods in Java 9

  • Removal of rt.jar, tools.jar, and dt.jar

Encapsulated JDK Internal APIs

Throughout the book we’ve talked about the encapsulation of the JDK internals APIs. This can cause critical problems when we move to JDK 9. However, this probably isn’t the most encountered problem during migration to JDK 9. In our opinion, the split packages and cyclic dependencies problems can occur more often.

Two independent solutions can help solve the problem of JDK internal APIs:

  • Replace each of your JDK internal APIs with supported APIs.

  • Keep the existing JDK internal APIs and use the --add-exports command-line option to break the encapsulation—to make the JDK internal APIs accessible to code in other modules or to code on the class path.

The first solution is by far the better one because we completely get rid of the unsupported JDK APIs in our code. Because the JDK internal APIs are marked as deprecated, it’s wise to provide replacements for them as soon as possible.

The second solution is reasonable if you don’t have enough time to replace the unsupported JDK internal APIs with supported ones. If all you want is to make your code compile and run again using JDK 9 this time, using the --add-exports option is a way to move forward. However, Oracle has stated that the JDK unsupported APIs will be removed in the next major JDK release. This could be JDK 10 or later. Sooner or later, you’ll have to replace them with supported JDK APIs in order to ensure that your code won’t break. As a conclusion, adding the --add-exports option is just a temporary solution to make code work. There’s no guarantee how long this workaround will work. It all depends on how long the unsupported JDK APIs that you’re using will remain in JDK and not be removed.

We already saw in this chapter how to identify the presence of JDK internal APIs in a JAR file or in a module: by using the JDeps tool with the option --jdk-internals. Let’s move on to discuss another possible problem we may encounter during compilation: not resolved modules.

Not Resolved Modules

Remember the module graph that resulted after the modularization of the JDK ? Figure 8-3 shows a small part of it.

A431534_1_En_8_Fig3_HTML.gif
Figure 8-3. Small part of the module graph of the Java SE modules with module java.se.ee at the top

Module java.se.ee is located right at the top of the module graph, and module java.se is only a level below. As described in Chapter 3, the differences between the module java.se.ee and the module java.se are as follows:

  • The module java.se.ee collects all modules that comprise the Java SE platform, including the modules that overlap with the Java EE platform.

  • The module java.se collects all the modules that comprise the Java SE platform that don’t overlap with the Java EE platform.

  • The module java.se.ee contains a total number of five modules that aren’t present in module java.se: java.xml.ws, java.xml.bind, java.corba, java.activation, and java.xml.ws.annotation.

We deliberately use the term collects instead of contains here because both module java.se.ee and module java.se are aggregator modules, which means that according to the JDK 9 specification, they “collect and re-export the content of other modules but add no content of their own.”

During the compilation process in JDK 9, the java.se module is considered the root module and not the java.se.ee module. This means that in the compilation step, the visible modules are the ones that are under the java.se module. It also means that the five modules from the module java.se.ee that aren’t in module java.se aren’t visible at compilation.

Note

The reason why the five modules aren’t resolved by default is related to backward-compatibility problems.

If our code makes use of any of the following five modules, the compilation will fail:

  • java.xml.ws

  • java.xml.ws.annotation

  • java.xml.bind

  • java.corba

  • java.activation

We can compare the modules contained by the java.se module with the modules contained by the java.se.ee module by limiting the observable modules with the --limit-modules option. The following two commands return the name of all the modules having module java.se and java.se.ee, respectively, in the root of the transitive closure:

java --limit-modules java.se --list-modules
java --limit-modules java.se.ee --list-modules

The solution for getting rid of the non-resolved modules problem is simple. We have to add the modules to the default root set of modules at both compile-time and runtime using the --add-modules command-line option, so they can be resolved:

--add-modules <module_name>

For instance, if we’re using types from module java.xml.ws, then it’s absolutely necessary to always add the module java.xml.ws in the root set of modules at both compile-time and runtime:

javac --add-modules java.xml.ws
java --add-modules java.xml.ws

As a result, the java.xml.ws module is resolved and can be used.

Note

Even if we use only libraries that have dependencies on these five modules, we still have to add the non-resolved modules to the root set of modules.

We now know that by adding the modules to the root set of module we can solve compilation errors like "package java.activation doesn't exist" or "package java.xml.bind doesn't exist".

It’s time to explore another issue that can occur during migration: split packages.

Split Packages

The split packages problem is one of the most serious problems that can take place in the Java 9 modular world. Split packages occur when two or more members of a package reside in more than one module. In order to support reliable configuration, the Java Platform Module System doesn’t allow split packages at compile-time. The reason is that the system loads all the modules from the module path with a single class loader, which can’t have more than one single type of a package. Two modules loaded by the same class loader can’t split a package.

Figure 8-4 illustrates two modules having a split package.

A431534_1_En_8_Fig4_HTML.gif
Figure 8-4. Two modules having a split package

Module A and module B contain both the package myPackage. Even if the modules contain different subpackages, the split package problem is present, because they share a package with the same name. The split package arises even if the packages aren’t exported.

In this case, the compilation will fail with the following error because we have split packages at compile-time:

error: module A reads package myPackage from both A and B

This error clearly states that the package B is in both module A and module C.

A split package problem can occur for every type of package, even for packages that aren’t exported, the so-called concealed packages . If two modules contain a package with the same name, an error will occur when we put the modules on the module path. It doesn’t matter if the packages are exported, open, or concealed. The split package problem will occur anyway.

Note

If a package is neither exported nor open, we can say that the module conceals the package.

It’s also important to mention another aspect. If we develop our own module that uses a package name that already exists in one of the platform modules, we have the split packages problem too.

Note

The packages from the platform modules also count in the split package problem. This means we can’t use in our own module a package that has the same name as a package that resides in the existing platform modules.

There’s no universal solution to repair the split packages problem. You can choose whatever solution you want in order to reach the desired goal—to not have a package or members of a package with the same name in more than one module.

Suppose we have two third-party JAR files that share a package with the same name. Some of the most used solutions to fix the split packages problems include the following:

  • Create a single JAR file out of the two JAR files. Combine them into a single JAR file. If we have two third-party JAR files that share a package with the same name, then we could make a single JAR file out of the two by unzipping them in the same directory and then zipping the entire directory into a single ZIP file. Don’t forget to change the suffix of the new ZIP file into a JAR file. In this way, we have just one single JAR that can be put into the module path and have just one automatic module, not two. The package is now in a single module, and the split packages issue is gone.

  • Check to see whether one of the JARs can be eventually replaced by a different one. If there’s a chance of replacing the JAR with another one, we should at least try it.

  • Rename one of the packages. Renaming one of the packages is also a solution to consider. The probability of success depends on the structure of the classes and especially if the classes live in a single namespace or not.

Until now we’ve talked about different use cases for JAR files . Let’s move on to modules. There are three possible solutions that can help getting rid of split packages in the case of modules:

  • Create a single module out of two or more modules: Combine them into a single module. If we have two modules that share a package with the same name, then we could eventually redesign our code and have just one module out of the two.

  • Create a third module: Another option would be to take the entire packages that cause the split package problem from both modules and move them into a third new module, which exports the packages we need inside our module. This solution is much easier to implement.

  • Attempt to remove the package dependencies: This is questionable and can be implemented only if you really don’t need the dependency anymore.

You’ve seen some suggestions of how to solve the split packages problem . You can choose these approaches or implement your own solutions in order to reach the goal of not having a package with the same name in more than one module.

Note

Split package problems can also occur in JAR files that are making use of the Service Provider API.

The JEP 200 states that “a non-standard module must not export any standard API packages.” This makes sense because if we have our own module com.apress.myModule, we shouldn’t, for example, export the java.sql package, because the java.sql package is already exported by the java.sql platform module. This will result in a split package.

Note

Don’t export any standard API from a non-standard module. Otherwise, you will have a split package.

One requirement of the JPMS states that “the Java compiler, virtual machine, and runtime system must ensure that modules that contain packages of the same name don’t interfere with each other. If two distinct modules contain packages of the same name then, from the perspective of each module, all of the types and members in that package are defined only by that module. Code in that package in one module must not be able to access package-private types or members in that package in the other module.”

Note

When we develop unit test cases in JDK 9, we must be careful not to introduce split packages. If we have a specific test module where we put the test cases, then when we import types from the module under test in the test module, we introduce the split package issue because we’ll have the same package in two different modules.

The next section talks about another problem that can arise: cyclic dependencies.

Cyclic Dependencies

A cyclic dependency is a relation between two or more modules expressed by the fact that the modules depend on each other, either directly or indirectly. Cyclic dependencies are considered anti-patterns. They aren’t allowed at compile-time in Java 9. If two modules contain a cyclic dependency, the compilation will fail. Jigsaw deliberately imposes a cyclic dependency check during compilation. The requirement imposed by the Java Platform Module System is severe: no cycle dependencies are allowed in the module graph.

However, cyclic dependencies are allowed at runtime, but only after the module graph is already resolved. We refer here to the reads relations of runtime modules, which are allowed at runtime. Cyclic dependencies aren’t allowed at compile-time, link-time, and runtime when the module graph is resolved for the first time. But at runtime, you can add readability edges using the command-line option --add-reads. At runtime, you can introduce a cyclic dependency using the --add-reads option because the module graph has already been resolved before and because we’re at runtime, not at compile-time.

The reasons for interdicting cyclic dependencies are justified: to simplify the module system or to make the module graph more understandable. Two modules that require each other would be better represented as a single module.

Cyclic dependencies can occur often for automatic modules, for instance. Because automatic modules imply readability to all other modules, the likelihood of getting two modules that depend on each other isn’t low.

Note

Cyclic dependencies between modules are forbidden during compilation. Cyclic dependencies between classes are allowed just inside of a single class, not between distinct modules.

Chapter 4 had an example of cyclic dependency in our module declaration. Cyclic dependencies can be solved by using interfaces to decouple the coupling between modules. A module should depend on an interface, not on another module. This can be implemented using the Service Provider API described in Chapter 6. What we need to do is to implement Service consumers and Service providers to decouple the coupling between modules.

Note

There’s an official proposal to allow cyclic relationships amongst modules at runtime, but not at compile-time. It’s unclear when this proposal will be implemented—possible in JDK 10. Allowing cyclic relationships at runtime will help solve some problems that can arise especially for very large applications, where the probability of having cycles is much higher.

We covered the cyclic dependencies issue in this section. The next section covers the new versioning scheme introduced in JDK 9.

New Versioning Scheme

Java 9 introduces a new format to define the version. This matters for a migration point of view because code that relies on the old string format will break. The maintainers of the Hadoop library had to fix the Hadoop library because it was broken in JDK 9 due to the introduction of the new version format:

System.getProperty("java.version").substring(0, 3).compareTo("1.7") >= 0

This piece of code isn’t working on JDK 9 anymore, because the version is no longer represented as 1.7. Instead of 1.7, the new version could have a format similar to 7 (containing only the major version) or 7.1.1 (containing the major version, minor version, and security version).

The format of the new version string is as follows:

$MAJOR.$MINOR.$SECURITY.$PATCH
  • $MAJOR serves as the major version of a JDK release.

  • $MINOR serves as the minor version of a JDK release.

  • $SECURITY serves as a security-related release of the JDK.

The changes affect not only java -version, but also the following system properties: java.runtime.version, java.vm.version, java.specification.version, and java.vm.specification.version.

We know how the new version looks in JDK 9, so let’s move on and see what methods were removed in JDK 9.

Removed Methods in JDK 9

The following methods have been completely removed in Java 9:

  • java.util.logging.LogManager.addPropertyChangeListener

  • java.util.logging.LogManager.removePropertyChangeListener

  • java.util.jar.Pack200.Packer.addPropertyChangeListener

  • java.util.jar.Pack200.Packer.removePropertyChangeListener

  • java.util.jar.Pack200.Unpacker.addPropertyChangeListener

  • java.util.jar.Pack200.Unpacker.removePropertyChangeListener

We should make sure not to use these six methods in Java 9—otherwise our code will break at compile-time. The probability of having one at least one of these six methods in our code is low.

Another change in JDK 9, with a definitely greater impact, is the removal of the runtime rt.jar and of tools.jar and dt.jar .

Removal of rt.jar, tools.jar, and dt.jar

Chapter 2 talked about the removal of the rt.jar, tools.jar, and dt.jar in JDK 9. This can have a consequence on our code if we make assumptions throughout our code based on one of these three JAR files. But the impact is greater on tools rather than on our own code.

Calling the ClassLoader::getSystemResource() method in JDK 9 won’t return an URL to a JAR file. Instead, it will return a valid URL.

If we call the method getSystemResource() with the parameter java/lang/Class.class,

ClassLoader.getSystemResource("java/lang/Class.class")

The following URL will be returned:

jrt:/java.base/java/lang/Class.class

We have to be aware of these new changes and check if our code expects to receive this URL in a specific format, which may now not be the same.

Let’s move to the next section, where we show migration strategies for migrating a Java application to Java 9.

Migrating an Application to Java 9

This chapter describes the process of migrating an application to Java 9 using the top-down approach. There are basically two types of migration we can perform when we decide to migrate our existing Java application, together with its dependencies, to Java 9: top-down migration and bottom-up migration.

The main difference between the two approaches is that application migration migrates the application first. By contrast, library migration starts migrating the libraries first, rather than the application.

Note

In Chapter 4 you learned what an unnamed module is. It is important to remember the following rule: code that exists in a named module can’t access anything on the class path!

Top-down Migration

We have a small application that reads some news from a JSON file using Google Gson . It logs the output using SLF4J and formats it using Google Guava. Therefore, we have four JAR libraries on the class path:

  • slf4j-simple-1.7.25.jar

  • slf4j-api-1.7.25.jar

  • guava-21.0.jar

  • gson-2.8.0.jar

Our application consists of a POJO class called News that has four attributes: id, title, category, and link. It also consists of a Main class that reads the entire information from the news.json file as a list of News objects. A for loop iterates over the entire list of News, formats the results so that everything is uppercase, and then logs the results.

Listing 8-5 shows the News class .

Listing 8-5. The News Class
package org.news;

public class News {

    private String id;

    private String title;

    private String category;

    private String link;

    public String getId() {
        return id;
    }


    public void setId(String id) {
        this.id = id;
    }


    public String getTitle() {
        return title;
    }


    public void setTitle(String title) {
        this.title = title;
    }


    public String getCategory() {
        return category;
    }


    public void setCategory(String category) {
        this.category = category;
    }


    public String getLink() {
        return link;
    }


    public void setLink(String link) {
        this.link = link;
    }


    @Override
    public String toString() {
        return "Id: " + id + " - " + "Title: " + title + " - " + "Category: " + category + " - " + "Link: " + link;
    }
}

Listing 8-6 represents the Main class , which imports packages from Gson, Guava, and SLF4J, reads the news.json, logs its content, and also formats it.

Listing 8-6. The Main Class of our Application
package org.news;

import java.io.*;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import com.google.gson.reflect.TypeToken;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.common.base.CaseFormat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


public class Main {

    public static void main(String[] args) throws FileNotFoundException {

        Logger logger = LoggerFactory.getLogger(Main.class);

        BufferedReader bufferedReader = new BufferedReader(new FileReader("news.json"));

        Type listType = new TypeToken<ArrayList<News>>(){}.getType();
        List<News> yourClassList = new Gson().fromJson(bufferedReader, listType);


        for(News news : yourClassList) {
            logger.info("Id: " + CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_UNDERSCORE, news.getId()));
            logger.info("Title: " + CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_UNDERSCORE, news.getTitle()));
            logger.info("Category: " + CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_UNDERSCORE, news.getCategory()));
            logger.info("Link: " + CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_UNDERSCORE, news.getLink()));
        }
    }
}

First, we compile and run our application in JDK 9 using only the class path, so we’re sure that our application works in JDK 9 without any changes.

javac -d out -cp "lib/gson-2.8.0.jar;lib/guava-21.0.jar;lib/slf4j-api-1.7.25.jar;lib/slf4j-simple-1.7.25.jar" $(find src -name '*.java')

We create a JAR file named news.jar:

jar --create --file lib/news.jar -C out.

Finally, we run our application:

java -cp "lib/gson-2.8.0.jar;lib/guava-21.0.jar;lib/slf4j-api-1.7.25.jar;lib/slf4j-simple-1.7.25.jar;lib/news.jar" org.news.Main

Our application is running successfully. We now have the confirmation that our not modularized application is running with JDK 9 without any changes.

Let’s start the modularization process. In this part we’ll modularize our News application only as part of the top-down migration strategy. We won’t modularize and won’t even change the four JAR files that represent our dependencies.

The first thing we do is to create a module-info.java file in the root directory. We have to figure out what kind of requires and exports clauses we need to put inside the module descriptor. We have to require our dependency JAR files inside the module descriptor and we do this by putting them on the module path so they become automatic modules. We covered the automatic modules in detail at the beginning of this chapter, where we also talked about how to find out the name of the generated automatic modules:

jar --describe-module --file gson-2.8.0.jar

By running the command jar --describe-module on the Gson JAR file , we find out that the generated name of the automatic module is gson. We add this name into our module descriptor and do the same for all the other JAR files , because our application depends on these. If we’re not sure about the dependencies used by our application, we can run the Jdeps on our previously created news.jar file:

$ jdeps -cp "lib/gson-2.8.0.jar;lib/guava-21.0.jar;lib/slf4j-api-1.7.25.jar;lib/slf4j-simple-1.7.25.jar;lib/news.jar" -s lib/news.jar

news.jar -> libgson-2.8.0.jar
news.jar -> libguava-21.0.jar
news.jar -> java.base
news.jar -> libslf4j-api-1.7.25.jar

The JDeps tool informs us that our news.jar file has dependencies on three JAR files and on module java.base.

Our module-info.java looks like this:

module news {
    requires slf4j.simple;
    requires slf4j.api;
    requires guava;
    requires gson;
}

Because our News application is standalone and isn’t an API, we have no exports clauses. We don’t need to give our application to somebody else to include it in their own application, so exports clauses are for the moment not necessary.

We compile our application:

javac -d modules --module-path lib --module-source-path src -m news

Now, if we take a look in the modules directory, we see that we have .class files not only for the corresponding Java classes, but also for the module-info.java we have a compiled module-info.class file .

We create a modular JAR for our application :

jar --create --file lib/news.jar -C modules/news.

Next we run our Main class:

java --module-path lib -m news/org.news.Main

Unfortunately, we got a ClassNotFoundException, which informs us that the class java.sql.Time can’t be found:

Exception in thread "main" java.lang.NoClassDefFoundError: java/sql/Time
        at [email protected]/com.google.gson.Gson.<init>(Gson.java:240)
        at [email protected]/com.google.gson.Gson.<init>(Gson.java:174)
        at news/org.news.Main.main(Main.java:23)
Caused by: java.lang.ClassNotFoundException: java.sql.Time
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(Unknown Source)
        at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(Unknown Source)
        at java.base/java.lang.ClassLoader.loadClass(Unknown Source)
        ... 3 more

The java.sql.Time class is located in the java.sql module. We need to add this module to the root set of modules using the --add-modules option so it can be resolved:

java --add-modules java.sql --module-path lib -m news/org.news.Main

The previous error doesn’t appear anymore, because it was solved. Unfortunately, we get now a different type of exception:

Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make field private java.lang.String org.news.News.id accessible: module news doesn’t "opens org.news" to module gson
        at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(Unknown Source)
        at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(Unknown Source)
        at java.base/java.lang.reflect.Field.checkCanSetAccessible(Unknown Source)
        at java.base/java.lang.reflect.Field.setAccessible(Unknown Source)

We get an InaccessibleObjectException because Gson is performing deep reflective access into the JDK and it doesn’t succeed because our package org.news is by default not opened for deep reflection. We have to open our package org.news to module Gson using a qualified opens clause. Therefore, we need to add the following statement in our module descriptor:

opens org.news to gson;

We compile and run our application again, and, because we opened the org.news package so that the Gson library can perform deep reflection on it, it works.

In this section, we managed to migrate an application that uses JAR files to run on JDK 9. We created a module for our own code and put the JAR files on the module path by transforming them into automatic modules. We don’t have any code on the class path, because the entire code is now on the module path.

Note

The source code we had before starting the modularization process can be found in the directory /ch08/topDownMigrationStart. The source code after the top-down modularization process can be found in the directory /ch08/topDownMigration.

This was top-down migration , where we modularize our application and use the JAR libraries as automatic modules on the module path .

Summary

This chapter presented useful information on topics related to migration. Migrating an application to Java 9 is a multi-step process, depending on the size and the libraries the application is using.

We started this chapter by presenting the automatic modules, which help us make significant steps forward in the process of migrating to modules because they reuse existing JARs. The automatic modules can be used as a replacement for JAR files. If you don’t plan to migrate your codebase to modules, you can use automatic modules instead. It’s understandable to use automatic modules when the JAR file has not yet been modularized by its authors, but you should replace the automatic modules by their corresponding named modules as soon as the corresponding named modules are available.

Further we presented the JDeps tool. This is a very useful tool used to find static dependencies of a library, but it can’t find reflective uses of JDK internal APIs. JDeps performs a static investigation on class level and outputs any use of JDK internal APIs. If we’re using Maven, we can make use of the Maven JDeps plugin, because JDeps is very well integrated with Maven through it.

Next we talked about the encapsulation introduced in Java 9. We learned which packages are encapsulated in Java 9 and which aren’t. We showed an example of breaking encapsulation of JDK internal APIs using the --add-exports command-line option. We also saw how to open packages, provide readability between modules, and add modules to the root set of modules. The --illegal-access option introduced to allow deep reflection for code in the class path was covered in detail.

When talking about changes in the area of the JDK internal APIs in Java 9, we must distinguish between accessing the internal APIs and accessing the internal APIs using reflection. The first isn’t so surprising due to the fact that for a long time the JDK internal APIs were marked as deprecated. The latter is different because no deprecation warning can be thrown as the code is executed at runtime. As a conclusion, normal access to JDK internal APIs isn’t possible in Java 9 anymore, but reflective access to JDK internal APIs is still possible, though limited.

We also discussed and gave solutions to some of the most common issues that can occur during migration to Java 9: encapsulated JDK internal APIs, not resolved modules, cyclic dependencies, new versioning scheme, split packages, removed methods in Java 9, and removal of rt.jar, tools.jar, and dt.jar. An important problem that can occur when moving to Java 9 is the split packages problem. A split package is a single package located inside two or more modules.

We finished this chapter by showing an example of migrating a small application that uses some third-party libraries to Java 9. We migrated the application step-by-step using the top-down approach. During migration, the messages displayed in the exceptions and errors give valuable hints for solving the root cause of the problem and for moving forward.

In Chapter 9, we’ll learn about the new API introduced in JDK 9 for handling modules, module descriptors, module references, and layers.

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

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