Key Takeaways
- Estimating the amount of time and effort to upgrade to Java 16 or 17 may be difficult. However, upgrading is often easier than originally thought.
- This article and other resources can help solve common Java upgrading challenges.
- Simply get started, spend some time on the upgrade, and determine where the issues are.
- Upgrade step-by-step so you can monitor the progress. Your teammates and management can see it as well. That way, everyone is more eager to continue with the upgrade.
- Various features have been removed from the JDK such as fonts, certificates, tools and API methods. If your application uses one of those features, then you need to resolve it when upgrading to a new version of Java.
- While upgrading Java can be challenging, it’s often a matter of hours or days. From my experience, the upgrade from Java 11 to Java 17 is easier compared to the upgrade from Java 8 to Java 11. Upgrading your application’s dependencies already resolves most, if not all, of these issues.
At InfoQ Live on April 27, 2021, I presented Upgrade to Java 16 or 17 to discuss why you should consider upgrading to Java 16 or Java 17 (once it’s released) including some practical advice on how to accomplish the upgrade.
The session was based on my personal GitHub repository, JavaUpgrades, which contains documentation and examples on common challenges and exceptions while upgrading to Java 16 or Java 17. You may use the concrete solutions, which are included as well, for your applications. The examples may be run with Docker and are built with Maven, but you may, of course, set up your own Gradle build.
This article, along with the recording of my InfoQ session, aims to help ease the upgrade process to Java 16 or Java 17. The most common upgrade tasks are discussed so you can resolve them easier and focus on the challenges that are specific to your application.
Why Upgrade?
Every new Java release, especially the major ones, contains improvements such that security vulnerabilities are resolved, performance is improved and new features are added. Keeping Java up-to-date helps to maintain a healthy application. Doing so may also help organizations to retain existing employees and potentially attract new ones as developers are generally more eager to work on newer technologies.
Upgrading Is Sometimes Seen as a Challenge
It is perceived that upgrading to a newer version of Java may require a significant amount of work. This is due to the required changes in the codebase and installing the latest version of Java on all the servers that build and run the application. Luckily, some companies use Docker which can enable teams to upgrade those items by themselves.
Many developers see the Java 9 module system, AKA Jigsaw, as a big challenge. However, Java 9 didn’t require you to explicitly use the module system. In fact, most applications running on Java 9 or higher haven’t configured Java modules in their codebase.
Estimating the amount of work required for any upgrade can be a challenge. It depends on various elements such as the number of dependencies and how current they are. If you use Spring Boot, for example, then upgrading Spring Boot might already solve most upgrade issues. Unfortunately, due to the uncertainty, most developers estimate many days, weeks or even months for the upgrade. This may result in the organization or management postponing the upgrade due to the cost, time or other priorities. I’ve seen estimates ranging from weeks to months to upgrade a Java 8 application to Java 11. However, I managed a similar upgrade in just a matter of days. This was partly due to my previous experience, however, it’s also a matter of just starting the upgrade process. It’s an ideal Friday afternoon task to upgrade Java and see what happens. I recently migrated a Java 11 application to Java 16 and the only required task was to upgrade one Lombok dependency.
Upgrading can be difficult and estimating the amount of time may seem impossible, but often the actual upgrade process doesn’t take all that long. I’ve witnessed the same issues while upgrading many applications. I hope to help teams quickly solve recurring issues so they can focus their efforts on application-specific challenges.
Java Release Cadence
In the past, a new version of Java was released every couple of years. Since the release of Java 9, however, the release cadence has been changed to every six months with a new Long Term Support (LTS) version released every three years. Most non-LTS versions are supported with minor updates for about six months until approximately when the next version is released. LTS versions, on the other hand, receive minor updates for years, at least until the next LTS release. Depending on the OpenJDK vendor (Adoptium, Azul, Corretto, etc.), support might actually be available much longer. Azul, for example, offers longer support on non-LTS versions.
You may ask yourself, "Should I always upgrade to the latest version or remain on an LTS release?" Keeping your application on an LTS version means you can easily take advantage of the minor upgrades with the various improvements especially those related to security. On the other hand, when using the latest non-LTS version, you should upgrade to a new non-LTS version every six months or else the minor upgrades will no longer be available.
Upgrading every six months, however, can be a challenge as you may have to wait until your frameworks have also been upgraded before upgrading your application. But you should not wait too long as minor upgrades for that non-LTS version are no longer released. At our company, we decided to stay on LTS versions for now as we doubt we will have the time to upgrade every six months in a short time window. However, that's still open for debate, if a team really wants to, or if interesting new Java features are released for a non-LTS version, then we might revise our decision.
What to Upgrade?
In general, an application consists of dependencies and your own code that is packaged and runs on the JDK. If something changes in the JDK, then the dependencies, your own code - or both - need to be changed. In most instances, this happens when a feature is removed from the JDK. If your dependencies use a removed JDK feature, it helps to be patient and wait until a new version of the dependency is released.
Multiple JDKs
When upgrading your application, you may want to use two different versions of the JDK such as the latest version for the actual upgrade and an older version to maintain your application. The current version of the JDK used for application development may be specified with the JAVA_HOME
environment variable or package management utilities such as SDKMAN! or JDKMon.
For the examples in my GitHub repository, I use Docker to demonstrate how specific features work, or even break, using different JDK versions. This will allow you to try them without having to install multiple versions of the JDK. Unfortunately, the feedback loop with Docker containers is a bit long. First, the image needs to be built and run. So in general, I recommend upgrading as much as possible from within your IDE. But it might be a good idea to try some things or to build your application in a clean environment without personal settings in a Docker container.
To demonstrate this, lets’ create a standard Dockerfile
file containing the content shown below. The example uses a Maven JDK 17 image and copies your application source inside the image. The RUN
command continues to run all tests and will not fail when an error occurs.
FROM maven:3.8.1-openjdk-17-slim
ADD . /yourproject
WORKDIR /yourproject
RUN mvn test --fail-at-end
To build the image as described above, invoke the docker build
command adding the -t
flag to indicate a tag (or name) and the .
to configure the context, which in this case is the current directory.
docker build -t javaupgrade .
Preparations
Most developers start by upgrading their local environment, followed by the build server, and then the various deploy environments. However, I sometimes simply create a build on the build server with a new Java version to see what may potentially go wrong without having to setup all the configuration for that specific project.
It’s possible to upgrade from Java 8 to 17 at once. However, if you encounter any issues, it might be difficult to determine which new feature in between those Java versions caused the issue. Upgrading in smaller steps, say, from Java 8 to Java 11 for example, makes it easier to pinpoint issues. It also helps searching for the issue if you can add the Java version which caused the issue to your search query.
First, I recommend upgrading your dependencies on the old version of Java. That way you can focus on making the dependencies work without upgrading Java at the same time. Unfortunately, that’s not always possible as some dependencies might require a newer version of Java. If that’s the case, then you have no other choice but to upgrade both Java and the dependencies at once.
There are some available Maven and Gradle plugins that display new versions of your dependencies. The mvn versions:display-dependency-updates
command invokes the Maven Versions Plugin. The plugin displays each dependency that has an available new version:
[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
While the gradle dependencyUpdates -Drevision=release
command invokes the Gradle Versions Plugin after configuring the plugin in the build.gradle
file:
plugins { id "com.github.ben-manes.versions" version "$version" }
After upgrading the dependencies, it’s time to upgrade Java. Of course, changing the code to work on a new version of Java works best from within an IDE, so make sure it supports the latest Java version. After that, upgrade your build tool to the latest version and configure the Java version:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<release>17</release>
</configuration>
</plugin>
plugins {
java {
toolchain {
languageVersion = JavaLanguageVersion.of(16)
}
}
}
compile 'org.apache.maven.plugins:maven-compiler-plugin:3.8.1'
Don’t forget to update the Maven and Gradle plugins to their latest versions as well.
Features Removed from the JDK
There is always the potential for elements to be removed from the JDK. These include methods, certificates, garbage collection algorithms, JVM options and even complete tools. In most instances, however, these items are initially deprecated or marked for removal. For instance, JAXB was originally deprecated in Java 9 and ultimately removed in Java 11. If you already have resolved issues related to features that have been deprecated, then you will have no worries once they’re actually removed.
The Java Version Almanac or the Foojay Almanac may be used as a reference to compare different versions of Java to see which items have been added, deprecated or removed. Higher-level changes in the form of Java Enhancement Proposals (JEPs) are available on the OpenJDK website. More detailed information on each Java release is available in the release notes published by Oracle.
Java 11
Various features were removed from Java 11. The first one is JavaFX, which is no longer part of the specification nor bundled with OpenJDK. However, there are vendors offering JDK builds that include more than what’s in the specification. For example, ojdkbuild and Liberica JDK’s full JDK both include OpenJFX. Alternatively, you may use the JavaFX build offered by Gluon or add the OpenJFX dependencies to your application.
Before JDK 11, some fonts were included in the JDK. For instance, Apache POI was able to use those fonts for Word and Excel documents. Starting with JDK 11, however, those fonts are no longer available. If the operating system doesn’t provide them either, then you may experience some weird errors. The solution is to install the fonts on the operating system. Depending on the fonts used in an application, you may have to install some more packages:
apt install fontconfig
Optional: libfreetype6 fontconfig fonts-dejavu
Java Mission Control (JMC), a monitoring and profiling application, makes it possible to profile applications in any environment including production with minimal overhead. If you haven’t tried it, I highly recommend it. It’s no longer part of the JDK itself but provided as separate downloads under the new name JDK Mission Control by AdoptOpenJDK and Oracle.
The biggest change in Java 11 was the removal of the Java EE and CORBA modules such as the four web services APIs - JAX-WS, JAXB, JAF and Common Annotations - that were deemed redundant since they were already included in Java EE. Oracle donated Java EE 8 to the Eclipse Foundation shortly after its release in 2017 with the intent that Java EE be open-sourced. Due to Oracle’s branding policy, it was necessary to rename Java EE to Jakarta EE and migrate the namespace from javax
to jakarta
. So when using dependencies such as JAXB, make sure you use the newer Jakarta EE artifacts. For example, the Java EE 8 version of JAXB artifact is named javax.xml.bind:jaxb-api
and future development ended in 2018. Development of the Jakarta EE version of JAXB will continue under the new artifact name jakarta.xml.bind:jakarta.xml.bind-api
. Within the application make sure you also change all imports to the new jakarta
namespace. With JAXB, for example: javax.xml.bind.*
to jakarta.xml.bind.*
and add the relevant dependencies.
The image below shows in the left column which modules are impacted by this change. The two columns on the right show the groupId
and artifactId
which you can use as dependencies. Please note that both JAXB and JAX-WS require two dependencies: one for the API and one for the implementation. CORBA doesn’t have an official Jakarta alternative, but Glassfish still provides an artifact that you may use.
Java 15
The Nashorn JavaScript Engine was removed in Java 15, however you can still use it by adding the dependency:
<dependency>
<groupId>org.openjdk.nashorn</groupId>
<artifactId>nashorn-core</artifactId>
<version>15.2</version>
</dependency>
Java 16
In this release, the JDK developers encapsulated some JDK internals. They no longer want you or anyone else to use the lower-level APIs of the JDK. This mainly impacted tools like Lombok. Luckily, in the case of Lombok, a new version was released within weeks that resolved the issue.
If you have any code or dependencies that still use the JDK internals, then it’s possible to open them up again. It may feel quite "dirty" like removing a lock from a door. Try to solve the issue by using higher-level APIs of the JDK. If that’s not possible, then this workaround is available in Maven:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<fork>true</fork>
<compilerArgs>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED</arg>
</compilerArgs>
</configuration>
</plugin>
I’ve tried to use Maven Toolchains to switch between JDKs by specifying them in the pom.xml
file. Unfortunately, an error occurred when running the application using Java 16 with an old version of Lombok:
[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]
This was all the information that was presented. I don’t know about you, but for me, it wasn’t really helpful, so I filed this issue. If the issue is fixed, then using Maven Toolchains is a nice way to switch between versions. Afterward, I ran the code directly on Java 16 and got a more descriptive error which mentions a part of the workaround I’ve 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 already agreed on the content for the release in September. The Applet API will be deprecated as browsers stopped support for applets a long time ago. The experimental AOT and JIT compiler will also be removed. As an alternative for the experimental compiler, you can use GraalVM. The biggest change is JEP-403: Strongly Encapsulate JDK internals. The Java option --illegal-access
no longer works and if you still access an internal API then the following exception is thrown:
java.lang.reflect.InaccessibleObjectException:
Unable to make field private final {type} accessible:
module java.base does not "opens {module}" to unnamed module {module}
Most of the time this can be resolved by upgrading your dependencies or using higher-level APIs. If that’s not possible then you can use the --add-opens
argument to again give access to internal APIs. However, that should only be used as a last resort.
Be aware that some tools won’t work yet on Java 17. For instance, Gradle won’t build your project and Kotlin cannot use jvmTarget = "17"
. Some frameworks, such as Mockito, experienced some small issues on Java 17. For example, methods in enum
fields caused this particular issue. However, I expect most of the issues will be resolved before or slightly after the release of Java 17.
You might see the message ‘Unsupported class file major version 61’ for any of your plugins or dependencies when building the application. Class file major version 61 is used for Java 17, 60 was used for Java 16. This basically means that the plugin or dependency doesn’t work on that version of Java. Most of the time upgrading to the latest version solves the issue.
Finished
After solving all the challenges, you can finally run your application on Java 17. After all the hard work, you can now use exciting new Java features such as Records and Pattern Matching.
Conclusion
Upgrading Java can be a challenge depending on how old your Java version and dependencies are, and how complicated your setup is. This guide helps solve the most common challenges when upgrading Java. In general, it’s hard to estimate how long the upgrade will actually take. However, I’ve experienced that it’s often not as difficult as some developers may think. Most times, I would say that an upgrade from Java 11 to Java 17 is easier than from Java 8 to Java 11. For most applications migrating from one LTS to the next LTS takes a couple of hours to a couple of days. Most of the time is spent building the application. It’s important to get started and make incremental changes. That way you can motivate yourself, your team and management to continue working on it.
Have you started upgrading your applications already?
About the Author
Johan Janssen is a software architect in the education sector for Sanoma Learning. Loves to share knowledge mainly around Java. Spoke at conferences such as Devoxx, Oracle Code One, Devnexus, and many more. Assisted conferences by participating in program committees and invented and organized JVMCON. Received the JavaOne Rock Star and Oracle Code One Star awards. Wrote various articles both for digital and printed media. Maintainer of various Java JDK/JRE packages for Chocolatey with around 100 thousand downloads a month.