Java 11 is released today! Formally it marks the end of a monumental shift in the Java ecosystem. With the challenges of migrating from Java 8 onto a modular and flexible JDK, with the six-month release cycle, the new licensing and long-term support models, we’ve entered a new era! Now the code bases have to follow and many projects will move from Java 8 directly to Java 11. If that describes yours, you’ve come to the right place – this migration guide tells you everything you need to know when moving from Java 8 to Java 11.
We’ll start with a super-quick tour through the new release cadence, licensing, and support before discussing how to prepare a migration (TL;DR: update all the things!) and finally, how to overcome the four most common hurdles (if you already migrated to Java 9, you can skip most of that). Note that we’re talking migration, not modularization (that’s not required and should be a separate step), so we won’t be creating any modules.
- On Releases, JDKs, And Licenses
- Preparing Your Migration
- Migrating From Java 8 To Java 11
- Share & Follow
On Releases, JDKs, And Licenses
This may seem like boring stuff, but the six-month release cycle, the commercialization of Oracle’s JDK, and the open question of long-term support for OpenJDK probably has more impact on your project than the technical challenges of moving to Java 11. So let’s discuss this, but be quick about it – more details in the links.
⇝ Java Next (talk at JavaZone 2018)
New Release Cadence
This is the most well-known change, so I’ll keep it short:
- new major release every six months (March and September)
- two minor updates for each (one and four months later)
OpenJDK Is The New Default
Before September 2018, Oracle’s JDK (and before that Sun’s JDK) was richer in features and perceived to be more stable and performant (although that was mostly an illusion) – it was hence the default choice for most of the ecosystem. OpenJDK was much less widely used but that will change with Java 11.
Oracle JDK is fully commercial
Oracle worked hard to make Oracle JDK 11 and OpenJDK 11 almost identical from a technical point of view – so much so that the most important difference is the license file they ship with. Oracle further pushes developers towards OpenJDK by making their branded JDK commercial, meaning you can’t use it in production without paying Oracle from day one after its release (you can use it for development and testing).
As a consequence, OpenJDK will become the new default – with full feature set, prime performance, and a free license (GPL+CE) it’s a great choice. On the next rung come, side by side, Oracle and other vendors with their OpenJDK variants for which they sell long-term support.
Oracle ships OpenJDK builds at jdk.java.net and, as mentioned, publishes two updates for each major version. So what happens after six months if you want to stay on a specific major version while still receiving updates with security and bug fixes? Two choices:
- pay someone for commercial support
- hope for free support of OpenJDK
As to commercial support, there are various vendors that have you covered for the specific versions they mark as long-term support (the focus seems to be on 11/17/23/…):
There will likely be free LTS for OpenJDK
Regarding OpenJDK, there are very promising discussions on the mailing list that suggest that there will be at least four years of public updates to the same versions. Most likely, each LTS version will get a steward that manages the updates and it looks like it may be Red Hat for Java 11. That covers the sources, but where can we get the final binaries from? AdoptOpenJDK is gearing up to continuously build the various OpenJDK versions for all kinds of platforms.
Put together, we’d get free OpenJDK LTS, organized by companies that are well-known in the Java community and continuously built by AdoptOpenJDK. That would be awesome!
Preparing Your Migration
Here you are: With your favorite vendor’s JDK installed (and maybe some LTS in the back pocket), you want your Java 8 project to work on Java 11. I know you’re ready to roll but before we go in, we need to discuss how to best approach and prepare the migration.
Short Or Long-Lived?
When starting to migrate from Java 8 to Java 11 (or later), the first thing you have to answer is whether you can and want to do this in one fell swoop or over a longer period of time. If the project causes little trouble and you are raising your minimum requirements, then go for a quick migration where you use the new version for the entire build process, including the target for compilation. All that’s left is to fix any problems that may pop up.
Don’t create a long-lived migration branch
If you don’t want to raise the minimum version requirement or the project is too large to migrate in a single sitting or a short-lived branch, I recommend the following approach:
- Don’t create a long-lived branch for the migration – instead do it on the regular development branch. This way, you’re not facing merge conflicts and can be sure that your colleagues’ changes are always also built on Java 11.
- Configure your continuous integration server to run the build once in its common configuration and once on Java 11.
- Learn about your build tool’s support for configuration specific to individual Java versions. (In Maven, that would be profiles.) This way you can keep the old build running while adding required configuration for the new version.
- Try to keep the version-specific configuration to a minimum. For example, prefer updating dependencies over adding command line flags (more on that below).
This way you can take all the time you need to guarantee that your project works on Java 8 as well as on Java 11 (or later). Whether you want to keep building on both (or more) versions or flip the switch once you’re done depends on the project’s minimum Java requirement. When you’re eventually leaving Java 8 behind for good, don’t forget to merge the version-specific bits into the default configuration to reduce complexity.
Update All The Things
The first rule of moving to Java 11 is
you do not talk … to update all the things. Your IDE, your build tool, its plugins, and, most importantly, your dependencies. You don’t have to do all of these updates in advance, but if you can, you absolutely should – it will very likely get you past some hurdles you can then stay blissfully unaware of.
Here are the recommended minimum versions for a few tools:
- IntelliJ IDEA: 2018.2
- Eclipse: Photon 4.9RC2 with Java 11 plugin
- Maven: 3.5.0
- compiler plugin: 3.8.0
- surefire and failsafe: 2.22.0
- Gradle: blocked by #5120
Some dependencies that you should keep an eye on (and versions that are known to work on Java 11):
- Anything that operates on bytecode like ASM (7.0), Byte Buddy (1.9.0), cglib (3.2.8), or Javassist (3.23.1-GA). Since Java 9, the bytecode level is increased every six months, so you will have to update libraries like these pretty regularly.
- Anything that uses something that operates on bytecode like Spring (5.1), Hibernate (unknown), Mockito (2.20.0), and many, many other projects.
The second bullet is not very helpful in its generality, but it’s the unfortunate truth: Many powerful projects work with bytecode under the hood. It helps to develop an eye for identifying problems related to that. Some (obvious?) tips:
- stack traces ending in bytecode manipulation libraries
- errors or warnings complaining about the bytecode level
- errors or warnings mumbling about "unknown (constant pool) entries"
If you don’t update in advance, it should still be the first action you take when encountering a problem with any specific tool or dependency. Either way, you may occasionally encounter problems even though your dependencies are up to date. In that case, have a look at the precise artifact causing the problem – chances are it’s a transitive dependency, in which case you should look into updating it separately.
With an older version of Hibernate, for example, it was necessary to update Javassist to work on Java 11:
<!-- LOOK OUT: YOU SHOULD USE A NEWER VERSION! -->
<!-- update Hibernate dependency on
Javassist from 3.20.0 to 3.23.1
for Java 11 compatibility -->
Likewise, with the outdated version 3.7.0 of the Maven compiler plugin, its ASM dependency needed updating:
<!-- LOOK OUT: YOU SHOULD USE 3.8.0! -->
<!-- update compiler plugin dependency on
ASM for Java 11 compatibility -->
Only dive into the problem if updating is impossible or doesn’t help
Unfortunately, not all projects are well-maintained or were even discontinued, in which case you need to look for alternatives. Examples are FindBugs (use SpotBugs instead), Log4j 1 (use Log4J 2), and Cobertura (use JaCoCo).
Only if the problem lies in your own code or such updates/replacements don’t help or aren’t possible, does it make sense to dive into the actual problem.
A Word On The Module System
I’m sure you’ve heard about the Java Platform Module System (JPMS) that was introduced in Java 9. Since it’s causing most of the compatibility challenges you’re going to face during a migration from Java 8, it definitely helps a lot to understand its basics – for example, by reading this fine module system tutorial (cough) or my book (cough cough). But keep in mind that you are not required to create modules to have your code run on Java 9 or later!
You don’t need modules to run on Java 9+
The class path is here to stay and if your code or its dependencies don’t do anything forbidden (more on that later), you can expect it to Just Work™ on Java 9, 10, or 11 exactly as it did on 8 – modules are no requirement and so this post does not address modularization, just migration.
Migrating From Java 8 To Java 11
We’re done preparing, time to go! Let’s see which problems you may expect on Java 11 and how to fix them.
Removal Of Java EE Modules
There used to be a lot of code in Java SE that was actually related to Java EE. It ended up in six modules that were deprecated for removal in Java 9 and removed from Java 11. Here are the removed technologies and packages:
- the JavaBeans Activation Framework (JAF) in javax.activation
- CORBA in the packages javax.activity, javax.rmi, javax.rmi.CORBA, and org.omg.*
- the Java Transaction API (JTA) in the package javax.transaction
- JAXB in the packages javax.xml.bind.*
- JAX-WS in the packages javax.jws, javax.jws.soap, javax.xml.soap, and javax.xml.ws.*
- Commons Annotation in the package javax.annotation
Here’s a compile error for a class using JAXBException from the java.xml.bind module:
error: package javax.xml.bind does not exist
If you get it past the compiler but forget to massage the run time, you’ll get a NoClassDefFoundError:
Exception in thread "main" java.lang.NoClassDefFoundError: javax/xml/bind/JAXBException
Caused by: java.lang.ClassNotFoundException: javax.xml.bind.JAXBException
... 1 more
Add third-party dependencies that contain the classes you need. The easiest way to do that is to stick to the reference implementations (given as Maven coordinates without version – use the most current ones):
- JAF: with com.sun.activation:javax.activation
- CORBA: there is currently no artifact for this
- JTA: javax.transaction:javax.transaction-api
- JAXB: com.sun.xml.bind:jaxb-impl
- JAX-WS: com.sun.xml.ws:jaxws-ri
- Commons Annotation: javax.annotation:javax.annotation-api
For more details, sources, and other recommendations, see this StackOverflow answer.
Illegal Access To Internal APIs
One of the module system’s biggest selling points is strong encapsulation. It makes sure non-public classes as well as classes from non-exported packages are inaccessible from outside the module. First and foremost, this of course applies to the platform modules shipped with the JDK, where only java.* and javax.* packages are fully supported. Most com.sun.* and sun.* packages, on the other hand, are internal and hence inaccessible by default.
While the Java 11 compiler behaves exactly as you would expect and prevents illegal access, the same is not true for the run time. To offer a modicum of backwards compatibility it eases migration and improves the chances of applications built on Java 8 to run on Java 11 by granting access to internal classes. If reflection is used for the access, a warning is emitted.
During compilation against Java 11 you may see compile errors similar to the following:
error: package com.sun.imageio.plugins.jpeg is not visible
(package com.sun.imageio.plugins.jpeg is declared
in module java.desktop, which does not export it)
Warnings emitted for reflection look as follows:
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by j9ms.internal.JPEG
(file:...) to field com.sun.imageio.plugins.jpeg.JPEG.TEM
WARNING: Please consider reporting this
to the maintainers of j9ms.internal.JPEG
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
# here's the reflective access to the static field com.sun.imageio.plugins.jpeg.JPEG.TEM
The most obvious and sustainable fix for dependencies on internal APIs is to get rid of them. Replace them with maintained APIs and you paid back some high-risk technical debt.
If that can’t be done for whatever reason, the next best thing is to acknowledge the dependencies and inform the module system that you need to access it. To that end you can use two command line options:
- The option --add-exports module/package=$readingmodule exports $package of $module to $readingmodule. Code in $readingmodule can hence access all public types in $package but other modules can not. When setting $readingmodule to ALL-UNNAMED, all code from the class path can access that package. During a migration to Java 11, you will always use that placeholder (you will have to change it when you modularize). The option is available for the java and javac commands.
- This covers access to public members of public types but reflection can do more than that: With the generous use of setAccessible(true) it allows interaction with non-public classes, fields, constructors, and methods (sometimes called deep reflection), which even in exported packages are still encapsulated. The java option --add-opens uses the same syntax as --add-exports and opens the package to deep reflection, meaning all of its types and their members are accessible regardless of their visibility modifiers.
You obviously need --add-exports to appease the compiler but using --add-exports and --add-opens for the run time has advantages as well:
- the run time’s permissive behavior will change in future Java releases, so you have to do that work at some point anyway
- --add-opens makes the warnings for illegal reflective access go away
- as I will show in a minute, you can make sure no new dependencies crop up by making the run time actually enforce strong encapsulation
Compiling against Java 11 helps hunting down dependencies on internal APIs in the project’s code base. But the libraries and frameworks your project uses are just as likely to make trouble.
JDeps is the perfect tool to find compile dependencies on JDK-internal APIs in your project and your dependencies. If you’re not familiar with it, I’ve written a tutorial that gets you started. Here’s how to use it for the task at hand:
jdeps --jdk-internals -R --class-path 'libs/*' $project
Here, libs is a folder containing all of your dependencies and $project your project’s JAR. Analyzing the output is beyond this article’s scope but it’s not that hard – you’ll manage.
Finding reflective access is a little tougher. The run time’s default behavior is to warn you once for the first illegal access to a package, which is insufficient. Fortunately, there’s the --illegal-access=$value option, where $value can be:
- permit: Access to all JDK-internal APIs is permitted to code on the class path. For reflective access, a single warning is issued for the first access to each package. (Default in Java 9, but will be removed in a future release.)
- warn: Behaves like permit but a warning is issued for each reflective access.
- debug: Behaves like warn but a stack trace is included in each warning.
- deny: The option for those who believe in strong encapsulation: All illegal access is forbidden by default.
Particularly deny is very helpful to hunt down reflective access. It is also a great default value to set once you’ve collected all required --add-exports and --add-opens options. This way, no new dependencies can crop up without you noticing it.
Removal Of Deprecated APIs and JavaFX
Since Java 9, the @Deprecated annotation got a Boolean attribute: forRemoval. If true, the deprecated element is going to be removed as soon as the next major release. That’s mildly shocking – in the past @Deprecated just meant yellow squiggly lines.
Removed Classes and Methods
Here are some of the more common classes and methods that were removed between Java 8 and 11:
- sun.misc.Base64 (use java.util.Base64)
methods addPropertyChangeListener and removePropertyChangeListener
- on java.lang.Runtime: methods getLocalizedInputStream and getLocalizedOutputStream
- various methods on SecurityManager
Heeding Deprecation Warnings
To make it easier to keep up with deprecation warnings, I recommend using the command line tools jdeps and jdeprscan. They work on class files and JARs and you can find them in your JDK’s bin folder. The former is a multi-purpose dependency analysis tool while the latter focuses on reporting the use of deprecated APIs, highlighting those that will be removed.
Then there’s JavaFX. It was never part of Java SE (i.e. The Standard™) and few OpenJDK variants shipped with it. For a while, Oracle seemed to push JavaFX and so they included it in their JDK, but that dwindled out and with Oracle aligning its JDK with OpenJDK, they no longer ship JavaFX. In fact, from Java 11 on, you will have a hard time finding any JDK that ships with JavaFX.
Don’t worry, though, the future is bright. OpenJFX, the project behind JavaFX, pulled the entire UI framework into their own artifacts that you simply add as a regular dependency. You can download them from Gluon or even Maven Central.
Casting To URL Class Loader
Java 9 and the module system improved the platform’s class loading strategy, which is implemented in a new type and in Java 11 the application class loader is of that type. That means it is not a URLClassLoader, anymore, so the occasional (URLClassLoader) getClass().getClassLoader() or (URLClassLoader) ClassLoader.getSystemClassLoader() sequences will no longer execute. This is another typical example where Java 11 is backwards compatible in the strict sense (because that it’s a URLCassLoader was never specified) but which can nonetheless cause migration challenges.
This one is very obvious. You’ll get a ClassCastException complaining that the new AppClassLoader is no URLClassLoader:
Exception in thread "main" java.lang.ClassCastException:
cannot be cast to java.base/java.net.URLClassLoader
The class loader was probably cast to access methods specific to URLClassLoader. If so, you might have to face some serious changes.
If you want to access the class path content, check the system property java.class.path and parse it:
String pathSeparator = System
String classPathEntries = System
If you’ve used the URLClassLoader to dynamically load user provided code (for example as part of a plugin infrastructure) by appending to the class path, then you have to find a new way to do that as it can not be done with Java 11. You should instead consider creating a new class loader for that. This has the added advantage that you’ll be able to get rid of the new classes as they are not loaded into the application class loader. You should also read up on layers – they give you a clean abstraction for loading an entirely new module graph.
Beyond that, your chances to do a migration with only small changes are slim. The only supported (and hence accessible) super types of the new AppClassLoader are SecureClassLoader and ClassLoader and only few methods were added here in 9. Still, have a look, they might do what you’re looking for.
As executive summary:
- new release every six months
- pick OpenJDK by default
- assume that there will be free LTS, otherwise pay a commercial vendor
- avoid long-lived branches
- update all the things
- keep the module system in mind
- replace Java EE modules with third-party implementations
- if absolutely necessary, use --add-exports or --add-opens to gain access to internal APIs
- heed deprecation warnings as classes and methods will be removed
- add JavaFX as a regular dependency
- don’t cast the application class loader to URLClassLoader
- More migration details:
- Planning Your Java 9 Update (fully applies to Java 11)
- Java 9 Migration Guide: The Seven Most Common Challenges (if you have problems not covered here)
- On the module system:
- Features in Java 11:
- Java 11 Tutorial ⇜ read this to get started!
- Improve Launch Times On Java 10 With Application Class-Data Sharing
- 90 New Features (and APIs) in JDK 11
- Tools on Java 9 and later: