This chapter covers
URLClassLoader
failThis chapter and chapter 7 discuss compatibility challenges when migrating an existing code base to Java 9 and beyond. You won’t be creating any modules yet; these chapters are about building and running an existing project on the newest release.
Why does moving to Java 9+ require two entire chapters? Can’t you install the newest JDK and expect everything to just work? Isn’t Java meant to be backward-compatible? Yes—if your project, including its dependencies, only relies on nondeprecated, standardized, documented behavior. But that’s a big if, and it turns out that in the absence of any enforcement, the wider Java community has strayed from that path.
As you’ll see in this chapter, the module system deprecated some Java features, removed others, and changed some internals:
URLClassLoader
, which breaks some casts (section 6.2).That’s not all, though. Chapter 7 discusses two more challenges (internal APIs and split packages). They got their own chapter because chances are you’ll encounter them again with non-JDK modules after you’ve migrated your project.
Taken together, these changes break some libraries, frameworks, tools, techniques, and maybe your code, too, so unfortunately updating to Java 9+ isn’t always an easy task. Generally speaking, the larger and older the project, the higher the chances it will take some work. Then again, it’s usually well-invested time, because it’s an opportunity to pay back some technical debt and get the code base into better shape.
By the end of this chapter and the next, you’ll know the challenges of updating to Java 9, 10, and 11 or even later. Given an application, you’ll be able to make informed guesses about what needs to be done; and assuming all your dependencies play along, you’ll be able to make it work on the newest release. You’ll also be well prepared for chapter 9, which discusses strategies for migrating to Java 9 and later.
A lot of code in Java SE is related to Java EE / Jakarta EE (which I abbreviate as JEE): CORBA comes to mind, and so do Java Architecture for XML Binding (JAXB) and Java API for XML Web Services (JAX-WS). These and other APIs ended up in the six modules shown in table 6.1. This could be nothing more than a small side note and the end of the story, but unfortunately it’s not. When you try to compile or run code that depends on a class from these modules, the module system will claim the modules are missing from the graph.
Here’s a compile error on Java 9 for a class using JAXBException
from the java.xml.bind module:
> error: package javax.xml.bind is not visible
> import javax.xml.bind.JAXBException;
> ^
> (package javax.xml.bind is declared in module java.xml.bind,
> which is not in the module graph)
> 1 error
If you get it past the compiler but forget to massage the runtime, you’ll get a NoClassDefFoundError
:
> Exception in thread "main" java.lang.NoClassDefFoundError:
> javax/xml/bind/JAXBException
> at monitor.Main.main(Main.java:27)
> Caused by: ClassNotFoundException:
> javax.xml.bind.JAXBException
> at java.base/BuiltinClassLoader.loadClass
> (BuiltinClassLoader.java:582)
> at java.base/ClassLoaders$AppClassLoader.loadClass
> (ClassLoaders.java:185)
> at java.base/ClassLoader.loadClass
> (ClassLoader.java:496)
> ... 1 more
What’s going on? Why are properly standardized Java APIs not present for code on the class path, and what can be done about that?
Module name | Description | Packages |
java.activation | Defines the JavaBeans Activation Framework (JAF) API | javax.activation |
java.corba | Defines the Java binding of the Open Management Group (OMG) CORBA APIs, and the RMI-IIOP API | javax.activity , javax.rmi , javax.rmi.CORBA , org.omg.* |
java.transaction | Defines a subset of the Java Transaction API (JTA) to support CORBA interop | javax.transaction |
java.xml.bind | Defines the JAXB API | javax.xml.bind.* |
java.xml.ws | Defines the JAX-WS and Web Services Metadata APIs | javax.jws , javax.jws.soap , javax.xml.soap , javax.xml.ws.* |
java.xml.ws.annotation | Defines a subset of the Common Annotations API to support programs running on the Java SE platform | javax.annotation |
Java SE contains a few packages that consist of endorsed standards and standalone technologies. These technologies are developed outside the Java Community Process (JCP), often because they rely on standards governed by other bodies. Examples are the Document Object Model (DOM), developed by the World Wide Web Consortium (W3C) and the Web Hypertext Application Technology Working Group (WHATWG), and Simple API for XML (SAX). If you’re interested, you can find a list of them and the packages they’re in at http://mng.bz/8Ek7. Disproportionately many of them fall into the JEE modules listed in table 6.1: java.corba, java.xml.bind, and java.xml.ws.
Historically, the Java Runtime Environment (JRE) shipped with implementations of these technologies but was ready to let users upgrade them independently of the JRE. This could be done with the endorsed standards override mechanism (see section 6.5.3).
Similarly, application servers often extend or upgrade the CORBA, JAXB, or JAX-WS APIs as well as the JavaBeans Activation Framework (in java.activation) or the JTA (in java.transaction) by providing their own implementations. Finally, java.xml.ws.annotation contains the javax.annotation
package. It’s often extended by the various JSR 305 implementations, which are most famous for their null
-related annotations.
In all these cases of extending or replacing APIs that ship with Java, the trick is to use the exact same package and class names, so the classes are loaded from an external JAR instead of the built in ones. In the parlance of the module system, this is called a split package: the same package is split across different modules or a module and the class path.
This is a general mechanism for all packages of all modules: splitting them between a module and the class path makes the class-path portion invisible. What makes the six JEE modules special is that unlike other modules, it’s customary to extend or upgrade them with the split-package approach.
To keep application servers and libraries like the JSR 305 implementations working without extensive configuration, a trade-off was made: for code on the class path, Java 9 and 10 by default don’t resolve the JEE modules, meaning they don’t make it into the module graph and hence aren’t available (see section 3.4.3 for unresolved modules and section 8.2.2 for details of the class path scenario).
That works well for applications that come with their own implementations of these JEE APIs, but not so much for those that relied on the JDK variants. Without further configuration, code on the class path using types from those six modules will fail to compile and run.
To get rid of this complexity and to properly separate Java SE from JEE, these modules are deprecated in Java 9 and removed in Java 11. With their removal, command-line tools like wsgen
and xjc
are also no longer shipped with the JDK.
What do you do if you get a compile or run-time error due to missing JEE APIs, or if a JDeps analysis (see appendix D) shows that you depend on JEE modules? There are three answers:
--add-modules
as described in section 3.4.3. Because the JEE modules are removed in Java 11, this won’t work there.The example at the beginning of the section tried to use JAXBException
from the java.xml.bind module. Here’s how to make that module available for compilation with --add-modules
:
$ javac
--class-path ${jars}
--add-modules java.xml.bind
-d ${output-dir}
${source-files}
When the code is compiled and packaged, you need to add the module again for execution:
$ java
--class-path ${jars}
--add-modules java.xml.bind
${main-class}
If you depend on a few of the JEE APIs, it may be easier to add the java.se.ee module instead of each individual module. It makes all six EE modules available, which simplifies things a bit. (How does it make them available? Read about aggregator modules in section 11.1.5.)
The effort of manually adding JEE modules is only required for unmodularized code. Once it’s modularized, the EE modules stop being special: you can require them like any other module, and they will be resolved like any other module—at least, until they’re removed.
Maybe you’ve been using the endorsed standards override mechanism to update standards and standalone technologies. In that case, you may wonder what happened to it in a time of modules. As you may have guessed, it was removed and replaced by something new.
Both the compiler and runtime offer the --upgrade-module-path
option, which accepts a list of directories, formatted like the ones for the module path. When the module system creates the module graph, it searches those directories for artifacts and uses them to replace upgradeable modules. The six JEE modules are always upgradeable:
JDK vendors may make more modules upgradeable. On Oracle JDK, for example, this applies to java.jnlp. Furthermore, application modules that were linked into an image with jlink
are always upgradeable—see section 14.2.1 for more on that.
JARs on the upgrade module path don’t have to be modular. If they lack a module descriptor, they’ll be turned into automatic modules (see section 8.3) and can still replace Java modules.
Running a project on Java 9 or later, you may encounter a class-cast exception like the one shown in the following example. Here, the JVM complains that it couldn’t cast an instance of jdk.internal.loader.ClassLoaders.AppClassLoader
to URLClassLoader
:
> Exception in thread "main" java.lang.ClassCastException:
> java.base/jdk.internal.loader.ClassLoaders$AppClassLoader ①
> cannot be cast to java.base/java.net.URLClassLoader ②
> at monitor.Main.getClassPathContent(Main.java:46)
> at monitor.Main.main(Main.java:28)
What’s this new type, and why does it break the code? Let’s find out! In the process, you’ll learn how Java 9 changes class-loading behavior to improve launch performance. So even if your project doesn’t suffer from this particular problem, it’s still a great opportunity to deepen your Java knowledge.
In all Java versions, the application class loader (often called the system class loader) is one of three class loaders the JVM uses to run an application. It loads JDK classes that don’t need any special privileges as well as all application classes (unless the app uses its own class loaders, in which case none of the following applies).
You can access the application class loader by calling ClassLoader.getSystemClassLoader
()
or by calling getClass().getClassLoader
()
on an instance of one of your classes. Both methods promise to give you an instance of type ClassLoader
. On Java 8 and before, the application class loader is a URLClassLoader
, a subtype of ClassLoader
; and because URLClassLoader
offers some methods that can come in handy, it’s common to cast the instance to it. You can see an example of that in listing 6.1.
Without modules as a run-time representation of JARs, URLClassLoader
has no idea in which artifact to find a class; as a consequence, whenever a class needs to be loaded, URLClassLoader
scans every artifact on the class path until it finds what it’s looking for (see figure 6.1). That’s obviously pretty ineffective.
private String getClassPathContent() {
URLClassLoader loader =
(URLClassLoader) this.getClass().getClassLoader(); ①
return Arrays.stream(loader.getURLs()) ②
.map(URL::toString)
.collect(joining(", "));
}
Now let’s turn to Java 9+. With JARs getting a proper representation at run time, the class-loading behavior could be improved: when a class needs to be loaded, the package it belongs to is identified and used to determine a specific modular JAR. Only that JAR is scanned for the class (see figure 6.1). This relies on the assumption that no two modular JARs contain types in the same package—if they do, it’s called a split package, and the module system throws an error as section 7.2 explains.
The new type AppClassLoader
and its equally new supertype BuiltinClassLoader
implement the new behavior, and from Java 9 on, the application class loader is an AppClassLoader
. That means the occasional (URLClassLoader) getClass().getClassLoader()
sequence will no longer execute successfully. If you want to learn more about the structure and relationships of class loaders in Java 9+, take a look at section 12.4.1.
If you encounter a cast to URLClassLoader
in a project you depend on and there’s no Java 9+-compatible version to update to, you can’t do much except one of the following:
If push came to shove, you could switch to another library or framework if it had versions that run fine on Java 9+.
If your own code does the casting, you can (and have to) do something about it. Unfortunately, chances are you may have to give up a feature or two. It’s likely you cast to URLClassLoader
to use its specific API, and although there have been additions to ClassLoader
, it can’t fully replace URLClassLoader
. Still, have a look—it may do the thing you want.
If you just need to see the class path an application was launched with, check the system property java.class.path
. If you’ve used URLClassLoader
to dynamically load user-provided code (for example, as part of a plugin infrastructure) by appending JARs to the class path, then you have to find a new way to do that, because it can’t be done with the application class loader used by Java 9 and later versions.
Instead, consider creating a new class loader—which has the added advantage that you’ll be able to get rid of the new classes, because they aren’t loaded into the application class loader. If you’re compiling at least against Java 9, layers could be an even better solution (see section 12.4).
You may be tempted to investigate AppClassLoader
and use its abilities if it does what you need. Generally speaking, don’t! Relying on AppClassLoader
is ugly because it’s a private inner class, so you have to use reflection to call it. Relying on its public supertype BuiltinClassLoader
isn’t recommended, either.
As the package name jdk.internal.loader
suggests, it’s an internal API; and because the package was added in Java 9, it isn’t available by default, so you’d have to use --add-exports
or even --add-opens
(see section 7.1 for details). This not only complicates the code and build process, it also exposes you to possible compatibility problems on future Java updates—for example, when these classes are refactored. So don’t do it unless it’s absolutely necessary to implement a mission-critical feature.
Examining the code for these casts is simple: a full-text search for “(URLClassLoader)” should do it and contain few false positives (include the parentheses to only find casts). As for finding them in your dependencies, I don’t know of any tool that make that process comfortable. I guess a combination of build-tool magic (to get all your dependencies’ source JARs in one place), command-line sorcery (to access all their .java
files and their file content), and yet another full-text search could do the trick.
The JDK’s and JRE’s directory structures evolved incrementally, and it shouldn’t be surprising that over the course of more than 20 years, they collected dust. One reason for not reorganizing them over time was, of course, backward compatibility. As is true for seemingly every detail, some code depends on their specific layout. Two examples:
rt.jar
(the classes making up the core Java runtime), tools.jar
(support classes for tools and utilities), and src.zip
(the JDK source code).javac
, jar
, or javadoc
by speculating that the running JRE has a sibling directory bin
containing them—which is true if the JRE is part of a JDK install, because that contains a bin
folder with those commands and a jre
folder next to each other.Then came the module system, which broke with the basic assumptions that made these two examples possible:
rt.jar
and tools.jar
.jlink
, run-time images can be created from any set of modules.Starting with Java 11, there is no longer a standalone JRE package. Running a program requires either a JDK or a package created by
jlink
.
As it became clear the module system would incur some breaking changes, the decision was made to go all the way and completely reorganize the run-time image directory structure. You can see the resulting changes in figure 6.2. Overall, the new layout is much simpler:
bin
directory and no duplicate binarieslib
directoryconf
, to contain all files meant for configurationThe most immediate consequence of these changes is that you need to update your development tools, because old versions likely won’t work with JDK installs of version 9 and later. Depending on the project, it may make sense to search it for code that rummages around in the JDK/JRE folder to look up binaries, property files, or anything else.
The URL you get for system resources, for example from ClasLoader::getSystemResource
, has also changed. It used to be of the following form, where ${path}
is something like java/lang/String.class
:
jar:file:${java-home}/lib/rt.jar!${path}
It now looks like this:
jrt:/${module}/${path}
All JDK APIs that create or consume such URLs operate on the new schema, but non-JDK code handcrafting these URLs must be updated for Java 9+.
Furthermore, the Class::getResource*
and ClassLoader::getResource*
methods no longer read JDK-internal resources. Instead, to access module-internal resources, use Module::getResourceAsStream
or create a JRT file system as follows:
FileSystem fs = FileSystems.getFileSystem(URI.create("jrt:/"));
fs.getPath("java.base", "java/lang/String.class"));
For more details on how to access resources, see section 5.2.
When compiling code or launching the JVM, there used to be various ways to specify which classes constitute the JDK platform. You could select a subset of the JDK, replace a specific technology (like JAXB) with another, add a few classes, or pick an entirely different platform version to compile against or launch with. The module system made some of these features obsolete and reimplemented others with a more modern approach; and regardless of the JPMS, the Java 9 release removes a few more.
If you’re relying on one or more of the features discussed in this section, you’ll have to put in some work to keep your project running. Nobody likes to be forced into reworking something that doesn’t cause any apparent problems, but looking over these features (most of which I never used), I can only imagine how much simpler the JDK internals became without them.
As section 1.5.5 explains, one goal of the module system was to allow users to create a run-time image with only the modules they need. This is particularly interesting for small devices with limited storage and for virtualizing environments, because both are interested in small run-time images. When it became apparent the module system wouldn’t be released with Java 8, which was the plan for a while, compact profiles were created as an interim solution.
The three compact profiles define subsets of the Java SE 8 API and JREs with just the required classes to support those API subsets. After picking a profile that matches your application’s requirements, you’d use the javac
option -profile
to compile against it (to make sure you stay within the selected subset) and then run the bytecode on the matching variant.
With the module system in play, much more flexible run-time images can be created with jlink
(see section 14.1), and compact profiles are no longer needed. The Java 9+ compiler will hence only accept -profile
if compiling for Java 8. To compile against a specific selection of modules, you can use the --limit-modules
option, as explained in section 5.3.5.
These are the modules you need to get the same APIs as the three compact profiles:
Instead of relying on a fixed selection, I recommend a different approach. Use jlink
to create an image with only the platform modules you need (see section 14.1); if your application and its dependencies are fully modularized, you can even include your application modules (see section 14.2).
Before Java 9, the extension mechanism let us add classes to the JDK without having to place them on the class path. It loaded them from various directories: from directories named by the system property java.ext.dirs
, from lib/ext
in the JRE, or from a platform-specific system-wide directory. Java 9 removes this feature, and the compiler and runtime will exit with an error if the JRE directory exists or the system property is set.
Alternatives are as follows:
java
and javac
option --patch-module
injects content into modules (see section 7.2.4).java
and javac
option --upgrade-module-path
replaces an upgradeable platform module with another one (see section 6.1.3).Before Java 9, the endorsed standards override mechanism let us replace certain APIs with custom implementations. It loaded them from the directories named by the system property java.endorsed.dirs
or the lib/endorsed
directory in the JRE. Java 9 removes this feature, and the compiler and runtime will exit with an error if the JRE directory exists or the system property is set. The alternatives are the same as for the extension mechanism (section 6.4.2).
The -Xbootclasspath
and -Xbootclasspath/p
options were removed. Use the following options instead:
javac
option --system
specifies an alternate source of system modules.javac
option --release
specifies an alternate platform version.java
and javac
option --patch-module
injects content into modules in the initial module graph.The Java compiler can process sources from various Java language versions (for example, Java 7, specified with -source
) and can likewise produce bytecode for various JVM versions (for example, for Java 8, specified with -target
). Java used to follow a "one plus three back" policy, which means javac
9 supports Java 9 (obviously) as well as 8, 7, and 6.
Setting -source 5
or -target 5
on javac
8 leads to a deprecation warning and is no longer supported by javac
9. Similarly, setting -source 6
or -target 6
on Java 9 results in the same warning. Now that there are releases every six months, this policy no longer applies. Java 10, 11, and 12 can compile for Java 6 just fine.
Before Java 9, you could use the -version:N
option on java
(or the corresponding manifest entry) to launch the application with a JRE of version N
. In Java 9, the feature was removed: the Java launcher quits with an error for the command-line option and prints a warning for the manifest entry while otherwise ignoring it. If you’ve been relying on that feature, here’s what the Java documentation has to say about that:
Modern applications are typically deployed via Java Web Start (JNLP), native OS packaging systems, or active installers. These technologies have their own methods to manage the JREs needed, by finding or downloading and updating the required JRE, as needed. This makes the launcher’s launch-time JRE version selection obsolete.
Looks like the docs think applications using -version:N
aren’t modern—what a rude thing to say. Joking aside, if your application depended on that feature, you have no other option but to make it work without -version:N
; for example, by bundling it with the JRE it works best on.
In addition to the larger challenges posed by the module system, there are a few changes, often not related to the JPMS, that are smaller but will cause trouble all the same:
I don’t want to keep you too long, but I also don’t want to leave out something that stops your migration dead in its tracks. So I’ll address each of these but be quick about it.
After more than 20 years, Java has finally and officially accepted that it’s no longer on version 1.x. About time. From now on, the system property java.version
and its siblings java.runtime.version
, java.vm.version
, java.specification.version
, and java.vm.specification.version
no longer start with 1.x
but with x
. Similarly, java -version
returns x
, so on Java 9 you get 9.something
.
An unfortunate side effect is that version-sniffing code may suddenly stop reporting the correct results, which could lead to weird program behavior. A full-text search for the involved system properties should find such code.
As for updating it, if you’re willing to raise a project’s requirements to Java 9+, you can eschew the system property prodding and parsing and instead use the new Runtime.Version
type, which is much easier:
Version version = Runtime.version();
// on Java 10 and later, use `version.feature()`
switch (version.major()) {
case 9:
System.out.println("Modularity");
break;
case 10:
System.out.println("Local-Variable Type Inference");
break;
case 11:
System.out.println("Pattern Matching (we hope)");
break;
}
The JDK accrued a lot of tools, and over time some became superfluous or were superseded by others. Some were included in Java 9’s spring cleaning:
hprof
agent library has been removed. Tools replacing its features are jcmd
, jmap
, and the Java Flight Recorder.jhat
heap visualizer was removed.java-rmi.exe
and java-rmi.cgi
launchers were removed. As an alternative, use a servlet to proxy RMI over HTTP.native2ascii
tool was used to convert UTF-8–based property resource bundles to ISO-8859-1. Java 9+ supports UTF-8 based bundles, though, so the tool became superfluous and was removed.Furthermore, all JEE-related command-line tools like wsgen
and xjc
are no longer available on Java 11 because they were removed together with the modules containing them (see section 6.1 for details on JEE modules).
Here comes probably the littlest thing that can make your Java 9 build fail: Java 8 deprecated the single underscore _
as an identifier, and on Java 9 you get a compile error when using it as one. This was done to reclaim the underscore as a possible keyword; future Java versions will give it special meaning.
Another issue: Thread.stop(Throwable
)
now throws an UnsupportedOperationException
. The other stop
overloads continue to work, but using them is highly discouraged.
The JNLP syntax has been updated to conform with the XML specification and “to remove inconsistencies, make code maintenance easier, and enhance security.” I won’t list the changes—you can find them at http://mng.bz/dnfM.
Each Java version removes some deprecated JVM options, and Java 9 is no different. It has a particular focus on garbage collection, where a few combinations are no longer supported (DefNew
+ CMS
, ParNew
+ SerialOld
, Incremental CMS
) and some configurations were removed (-Xincgc
, -XX:+CMSIncrementalMode
, -XX:+UseCMSCompactAtFullCollection
, -XX:+CMSFullGCsBeforeCompaction
, -XX:+UseCMSCollectionPassing
) or deprecated (-XX:+UseParNewGC
). Java 10, in turn, removes -Xoss
, -Xsqnopause
, -Xoptimize
, -Xboundthreads
, and -Xusealtsigs
.
Finally, here’s a non-exhaustive list of things that are deprecated in Java 9, 10, and 11:
java.applet
package, together with the appletviewer
tool and the Java browser pluginjavaws
tool-Xprof
policytool
security toolJava 10 and 11 already followed through on some of the deprecations:
For more, as well as for details and suggested alternatives, check the release notes (Java 9: http://mng.bz/GLkN; Java 10: http://mng.bz/zLeV) and the list of deprecated code that’s marked for removal (Java 9: http://mng.bz/YX9e; Java 10: http://mng.bz/qRoU).
--add-modules
.URLClassLoader
, so code like (URLClassLoader) getClass().getClassLoader()
fails. Solutions are to only rely on the ClassLoader
API, even if that means a feature must be removed (recommended); create a layer to dynamically load new code (recommended); or hack into the class-loader internals and use BuiltinClassLoader
or even AppClassLoader
(not recommended).jlink
and configure compilation with --limit-modules
.--patch-module
, --upgrade-module-path
, or the class path.-Xbootclasspath
option, use --system
, --release
, or --patch-module
.-version:N
option to launch an application with Java version N
.java.version
report their version as 9.${MINOR}.${SECURITY}.${PATCH}
(in Java 9) or as ${FEATURE}.${INTERIM}.${UPDATE}.${PATCH}
(in Java 10 and later), meaning on Java X they start with X
instead of 1.x
. A new API Runtime.Version
makes parsing that property unnecessary.hprof
, jhat
, java-rmi.exe
, java-rmi.cgi
, and native2ascii
policytool
idlj
, orbd
, schemagen
, servertool
, tnameserv
, wsgen
, wsimport
, and xjc
18.227.10.45