Why and How to Upgrade to Java 16 or Java 17
Key Point
Link to Original Posted 2021/09/08
April 27, 2021 InfoQ Live Announcing 'Upgrade to Java 16 or 17' and why you should consider upgrading to Java 16 or Java 17 (post-release ), including practical advice on how to upgrade.
This session was based on my personal GitHub repository, JavaUpgrades. This repository contains documentation and examples of common challenges and exceptions when upgrading to Java 16 or Java 17. You can use the specific solutions included for your application as well. The example runs with Docker and builds with Maven, but of course you can also set up a Gradle build.
This article, along with a recording of my InfoQ session, aims to ease the process of upgrading to Java 16 or Java 17. We cover the most common upgrade tasks so you can easily solve them and focus on your application-specific challenges.
Why upgrade?
All new Java releases, especially major releases, contain improvements such as security vulnerabilities resolved, performance improvements, and new features added. is included. Keeping Java up to date helps maintain healthy applications. In doing so, the organization retains existing employees and may also attract new employees as developers are generally more enthusiastic about working on new technologies.
Upgrading is sometimes seen as difficult
It is recognized that upgrading to a new version of Java can require significant work. This is to make any necessary changes to the codebase and install the latest version of Java on all servers where you build and run your application. Luckily, some companies use Docker and allow teams to upgrade those items themselves.
Many developers see the Java 9 module system (aka Jigsaw) as a major challenge. However, in Java 9 there was no need to explicitly use the module system. In fact, most applications running on Java 9 and above do not have Java modules in their codebase.
Estimating the amount of work required for an upgrade can be difficult. It depends on a number of factors, including the number of dependencies and current dependencies. For example, if you're using Spring Boot, upgrading Spring Boot may have already fixed most upgrade issues. Unfortunately, due to uncertainty, most developers estimate that the upgrade will take days, weeks, or even months. This may cause organizations or administrators to postpone upgrades due to cost, time, or other priorities. I've seen estimates that it would take weeks or months to upgrade a Java 8 application to Java 11, but I've done similar upgrades in just a few days. This may be due to previous experience, but it is also just a matter of starting the upgrade process. Upgrading Java and seeing what happens would be an ideal Friday afternoon task. I recently migrated a Java 11 application to Java 16 and the only task required was to upgrade one dependency, Lombok.
Upgrading can seem difficult and impossible to estimate, but the actual upgrade process often doesn't take that long. I had the same issue while upgrading many applications. I want to help my team quickly resolve recurring issues and focus on application-specific challenges.
Java Release Rhythm
In the past, new versions of Java were released every few years. However, since the Java 9 release, the release cadence has changed to every 6 months, with a new Long Term Support (LTS) version released every 3 years. Most non-LTS versions will be supported with minor updates for about 6 months until around the time the next version is released. LTS versions, on the other hand, receive minor updates over the years, at least until the next LTS release. Support may actually be available much longer for some OpenJDK vendors (Adoptium, Azul, Corretto, etc.). For example, Azul provides longer support for non-LTS versions.
You might ask yourself, "Should I always upgrade to the latest version, or should I stay with the LTS release?" Keeping the application on his LTS version means that minor upgrades are easily available with various improvements, especially those related to security. On the other hand, if you use the latest non-LTS version, you must upgrade to a new non-LTS version every 6 months. Otherwise minor upgrades will not be available.
However, upgrading every 6 months can be difficult as you may have to wait for the framework to upgrade before upgrading your application. However, we don't have to wait too long until minor upgrades for non-LTS versions are released. We don't think our company has time to upgrade in the short timeframe of every 6 months, so we've decided to stick with the LTS version for now. However, if the team really wants it, or if there are interesting new Java features released in the non-LTS version, it's still up for debate. In that case, we may revise our decision.
What to upgrade?
In general, an application consists of dependencies and your own code that is packaged and run in the JDK. If anything changes in the JDK, you'll have to change your dependencies, your own code, or both. Most often this happens when features are removed from the JDK. If your dependencies use removed JDK features, it may be helpful to be patient until newer versions of the dependencies are released.
Multiple JDKs
When upgrading an application, it is recommended to use two different versions of the JDK, such as the latest version for the actual upgrade and the older version for maintaining the application. It is recommended. The current version of the JDK used for application development can be specified in the JAVA_HOME
environment variable or in a package management utility such as SDKMAN! or JDKMon.
Examples in the GitHub repository use Docker to demonstrate how certain features may or may not work using different JDK versions. This allows you to try them without having to install multiple versions of the JDK. Unfortunately, the Docker container feedback loop is a bit long. First, we need to build and run the image. Therefore, it is generally recommended to upgrade from within the IDE whenever possible. However, I think it's a good idea to try a few things and build your application in a clean environment without personalizing your Docker container.
To demonstrate this, let's create a standard Dockerfile
file with the content shown below. This example uses a Maven JDK 17 image and copies the application sources inside the image. The RUN
command runs all tests in succession and does not fail on errors.
FROM maven:3.8.1-openjdk-17-slimADD ./yourprojectWORKDIR /yourprojectRUN mvn test --fail-at-end
To build the above image invokes the docker build
command with the -t
flag denoting the tag (or name) and the context (current directory in this case) . Add
.
docker build -t javaupgrade .
Preparation
Most developers start with a local environment, then a build server, then to upgrade various deployment environments. However, sometimes we don't set up all the configurations for a particular project and simply create a build on the build server using the new Java version and see what can go wrong.
It is possible to upgrade from Java 8 to 17 at once. However, if you run into problems, it can be difficult to determine which new feature between those Java versions is causing the problem. For example, a gradual upgrade from Java 8 to Java 11 can help identify issues. It will also help you find the problem if you can add the Java version that caused the problem to your search query.
First, we recommend that you upgrade any older Java dependencies. Then you can focus on getting your dependencies working without upgrading Java at the same time. Unfortunately some dependencies may require a newer version of Java and this is not always possible. In that case, you have no choice but to upgrade both Java and dependencies at once.
There are several Maven and Gradle plugins available that show new versions of dependencies. The mvn versions:display-dependency-updates
command calls the Maven Versions Plugin. The plugin will show each dependency that has a newer version available:
[INFO] --- versions-maven-plugin:2.8.1:display-dependency-updates (default-cli) @ mockito_broken ---[INFO] The following dependencies in Dependencies have newer versions:[ INFO]org.junit.jupiter:junit-jupiter ................... 5.7.2 -> 5.8.0-M1[INFO]org.mockito:mockito -junit-jupiter ................... 3.11.0 -> 3.11.2
gradle dependencyUpdates -Drevision The =release
command calls the Gradle Versions Plugin after configuring the plugin in the build.gradle
file:
plugins { id "com.github.ben-manes.versions" version "$version" }
After upgrading dependencies, upgrade Java. Of course, change your code to work with newer versions of Java and make sure you support the latest Java versions that work best with your IDE. Then upgrade your build tools to the latest version and configure your Java version:
org.apache.maven.plugins maven-compiler-plugin 3.8.117
plugins { java {toolchain {languageVersion = JavaLanguageVersion.of(16)} }}
compile 'org.apache.maven.plugins:maven-compiler-plugin:3.8.1'
Maven and Gradle Don't forget to update your plugins to the latest version as well.
Features Removed from the JDK
There is always the possibility that something will be removed from the JDK. These include methods, certificates, garbage collection algorithms, JVM options, and even complete tools. However, in most cases these items were initially deprecated or marked for deletion. For example, JAXB was originally deprecated in Java 9 and eventually removed in Java 11. If you've already resolved issues related to deprecated functionality, don't worry if it's actually removed.
You can use the Java Version Almanac or Foojay Almanac as a reference to compare different versions of Java and see what items have been added, deprecated, or removed. High-level changes in Java Extension Proposal (JEP) format are available on the OpenJDK website. For more information on each Java release, please refer to the release notes published by Oracle.
Java 11
Various features were removed in Java 11. The first feature is JavaFX, which is no longer part of the specification and is no longer bundled with OpenJDK. However, some vendors offer JDK builds that include more than what is included in the specification. For example, the ojdkbuild and Liberica JDK full JDKs both include OpenJFX. Alternatively, you can use the JavaFX build provided by Gluon or add the OpenJFX dependency to your application.
Before JDK 11, some fonts were included in the JDK. For example, Apache POI could use these fonts for Word and Excel documents. However, as of JDK 11, these fonts are no longer available. If your operating system doesn't provide them either, you may get strange errors. The solution is to install the font in your operating system. Depending on the fonts used by your application, you may need to install a few more packages:
apt install fontconfigOptional: libfreetype6 fontconfig fonts-dejavu
Java Mission Control (JMC), a monitoring and profiling application, can be used in any environment, including production, with minimal overhead. You can profile your application with If you haven't tried it yet, I highly recommend it. It is no longer part of the JDK itself, but is provided as a separate download under the new name AdoptOpenJDK and JDK Mission Control by Oracle.
The biggest change in Java 11 is that Java EE and It was the removal of the CORBA module. Oracle donated Java EE 8 to the Eclipse Foundation shortly after its release in 2017, with the goal of making Java EE open source. Due to Oracle's branding policy, we had to rename Java EE to Jakarta EE and migrate the namespace from javax
to jakarta
. So if you use dependencies such as JAXB, be sure to use the new Jakarta EE artifacts. For example, earlier development of the JAXB artifact with the name javax.xml.bind:jaxb-api
in the Java EE 8 version ended in 2018. Development of the Jakarta EE version of JAXB continues with a new artifact name jakarta.xml.bind:jakarta.xml.bind-api
. Also make sure to change all imports in your application to the new jakarta
namespace. For JAXB, for example, change javax.xml.bind.*
to jakarta.xml.bind.*
and add the relevant dependencies.
The image below shows the modules affected by this change in the left column. The two columns on the right show the groupId
and artifactId
that can be used as dependencies. Note that both JAXB and JAX-WS require two dependencies. One for the API and one for the implementation. CORBA does not have an official Jakarta replacement, but it does provide a usable artifact for Glassfish.
Java 15
The Nashorn JavaScript Engine has been removed in Java 15, but you can still use it by adding a dependency:
org.openjdk.nashorn nashorn-core 15.2
Java 16
In this release, the JDK developers have encapsulated some JDK internals. has become They don't want you or anyone else using the JDK's low-level APIs anymore. This primarily affected tools like Lombok. Fortunately, in the case of Lombok, a new version was released within a few weeks that fixed the problem.
If you have code or dependencies that still use JDK internals, you can reopen them. It can feel pretty 'dirty', like unlocking a door. Try to solve the problem using the JDK's high-level API. If that's not possible, you can use this workaround with Maven:
org.apache.maven.plugins maven-compiler-plugin 3.8.1true-J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED-J-- add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED-J--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED-J-- add-opens=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED-J--add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED-J-- add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED-J--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED-J-- add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED-J--add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED
I have tried switching JDKs by specifying the JDK in the pom.xml
file using the Maven toolchain. Unfortunately, running the application with Java 16 on an older version of Lombok resulted in an error:
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project broken: Compilation failure -> [Help 1]
That was all the information displayed. I don't know for you, but it didn't help me much, so I filed this issue. After the issue is fixed, you can switch versions using the Maven toolchain. I then ran the code directly in Java 16 and got a more descriptive error that mentions some of the workarounds shown earlier:
… class lombok.javac.apt.LombokProcessor (in unnamed module @0x21bd20ee) cannot access class com.sun.tools.javac.processing.JavacProcessingEnvironment (in module jdk.compiler) because module jdk.compiler does not export com.sun.tools.javac.processing to unnamed module …
Java 17
The JDK maintainers have already agreed on the content of the September release. increase. The Applet API is deprecated as browsers stopped supporting applets long ago. The experimental AOT and JIT compilers are also removed. GraalVM can be used as an alternative to the experimental compiler. The biggest change is JEP-403: Strongly Encapsulate JDK internals. The Java option --illegal-access
no longer works and the following exception is thrown when still accessing internal APIs:
java.lang.reflect.InaccessibleObjectException: Unable to make field private final {type} accessible: module java.base does not "opens {module}" to unnamed module {module}
In most cases, this can be resolved by upgrading dependencies or using higher level APIs. If that is not possible, you can re-allow access to internal APIs using the --add-opens
argument. However, use this only as a last resort.
Be aware that some tools will not work with Java 17 yet. For example, Gradle cannot build the project and Kotlin cannot use jvmTarget = "17"
. Some frameworks such as Mockito had some minor issues with Java 17. For example, the enum
field method caused this particular problem. However, I believe most issues will be resolved before or shortly after the release of Java 17.
When building your application, you may see an "Unsupported class file major version 61" message for either a plugin or a dependency. Class file major version 61 is used in Java 17 and 60 is used in Java 16. This basically means that the plugin or dependency will not work with that version of Java. In most cases, upgrading to the latest version will fix the issue.
Concluding remarks
After solving all the challenges, you can finally run your application on Java 17. After all the hard work, you'll be able to use exciting new Java features like records and pattern matching.
Conclusion
Upgrading Java can be difficult depending on how old your Java version and dependencies are, and how complex your setup is. This guide will help you solve the most common issues when upgrading Java. In general, it is difficult to estimate how long an upgrade will actually take. However, I have experienced that it is often not as difficult as some developers think. In most cases upgrading from Java 11 to Java 17 is easier than upgrading from Java 8 to Java 11. Most applications take hours or days to migrate from one LTS to the next. Most of the time is spent building applications. It's important to start and make changes incrementally. That way, you can motivate yourself, your team, and management to keep working on it.
Have you started upgrading your application yet?
About the Author
Johan Janssen is a Software Architect in the Education Division at Sanoma Learning. He loves sharing his knowledge, mostly Java. He has spoken at conferences such as Devoxx, Oracle Code One and Devnexus. He assisted the conference by participating in the Program Committee and devising and organizing JVMCON. Winner of JavaOne Rock Star and Oracle Code One Star awards. I have written various articles for both digital and print media. Maintainer of various Java JDK/JRE packages on Chocolatey with about 100,000 downloads per month.