Transcript
Janssen: My name is Johan Janssen. I work for Sanoma Learning. What are we going to talk about? First, we start with the reasons on why you should actually upgrade your application to the newest version of Java. Then we have a look at the release cadence of Java. Then we start with the more practical parts on how you should actually upgrade your Java application, and some tips and tricks for that. Then see what the challenges are of the various Java versions and how we can resolve them. If you have any questions, please have a look at the GitHub repository, Java Upgrades, which I created. A lot of the things in this presentation are based on that repository, which includes examples and a lot of information on challenges that you encounter, errors that you might see, and the solutions for that.
Background
I didn't get this information by upgrading HelloWorld or by Googling a lot. Actually, I do Google a lot as well. That was during the process of upgrading many applications over the years to newer Java versions. I've been doing that for quite a while for bigger applications with hundreds of thousands of users a day. It's a lot of practical information.
Why Upgrade?
Why should you actually upgrade your application? Every Java version, even minor versions offer performance benefits, but also security fixes and other useful extras that make your application more robust and better maintainable. It includes cool, new features, for instance, the recently released Java records or the pattern matching functionality.
Why this Session?
I've noticed that a lot of applications are still on Java 8 or maybe even older, because upgrading is seen as quite a challenge. It's often estimated that it's a lot of work, maybe even weeks or months of work to upgrade from one LTS version to the next LTS version of Java, the long term support versions. Then, when that information is being sent to the business, they often say, we don't have time to invest so much time now. It is postponed. That's not really helpful. It's seen as a challenge. It's difficult to estimate. That's not a good idea. We try to make it more practical. Actually, in the case of the weeks to months' estimation, I managed to do it in a couple of days. Of course I had some prior experience with the Java Upgrades. It's often a matter of simply trying it, and if you're lucky, you manage to upgrade it quite easily. Depending on what dependencies you use, or what code bases you use, it might be a bit more of a challenge.
Goals
I will share some recurring challenges and their solutions that I've discovered over the years with various Java versions, to help you get the migration to the newest version of Java in a quick manner. Therefore, I use Maven as an example but of course this works with Gradle as well or any other build tool that you prefer.
Releases
For Java, nowadays, we have the long term support or LTS versions, which are released every couple of years. The latest version was 11. The latest LTS version released in 2018. The next one will be 17, which will be released in September this year. The major LTS versions also receive minor updates, and those updates are at least until the next LTS version is released. Depending on the vendor that created your build, it can be for a much longer period, but that differs per vendor. Between those LTS versions, we also have the six monthly Java releases. It also has minor updates, but only until the next major release is created.
Should we then stick to using LTS versions, or should we use the latest versions of Java? If you have the time available, I would recommend using the latest version of Java because you can use the new features, there's new things, and also increase developer productivity. However, because it's a major version, it might take a bit more work. You have to do it every six months, because you cannot use the new minor versions. If you don't have that time available to upgrade every six months, you might opt to using only the long term support versions. However, then, each time you need to upgrade your long term support version, that actually might take quite a bit more time as well. In the end, I think staying on long term support versions or using the latest version shouldn't differ too much in the investment that needs to be done in upgrading.
What to Upgrade
What do we need to upgrade? Typically, your application consists of some application code that we wrote ourselves and some dependencies, which we're using, such as Spring, JPA, or other things. Those all use Java, the JDK, to build and run our application. When something is removed from the JDK we're using, then we need to change either our dependency or code, or maybe even both. With the dependencies, it's sometimes good to have a little bit of patience. After a while, those dependencies will hopefully get new versions, with support for the new Java version. Then the only thing you need to do is to upgrade to the newest version of the dependency instead of having to fix the challenges yourself.
Deletion and Deprecations
We'll mainly focus on the deletion of features, because for everything that's being deleted, which you're using in your application, you need to find a solution before you can actually start using the cool, new additions of the new Java versions. Actually, in Java, stuff isn't deleted immediately most of the time. First, the algorithms are marked for deprecation, for instance, for JAXB. In Java 9, JAXB was marked for deprecation. Same in Java 10. Then in Java 11, it was removed. If you continuously update your Java version, then already on Java 9, you get a warning that JAXB will be removed in the future. You can make sure that you fix that and have everything in place before it's actually removed. Then the step to Java 11 will be relatively easy. On the other side, if you go from Java 8 to Java 11 at once, you don't get the deprecation warnings, and you get compile errors, because actually these features are already removed. It might be good to go step by step.
What Gets Removed?
What's removed in Java? There is a lot of things that can be removed and are actually being removed: certificates, encryption algorithms, various JVM flags or options, even garbage collectors, or tools on Java for mission control. There are various options, you could use the javaalmanac.io website, and compare different Java versions. In this case, we compare version 8 with Java 17. We see which methods in Java are added, deprecated, or removed. There's a lot of detail in this page. Other places where you can get detailed information about new features or removed features inside Java are offered through the OpenJDK website, and through the Oracle release notes documentation.
If we look at the OpenJDK release notes or the OpenJDK documentation, we can see the various JEPs, the Java Enhancement Proposals that are being proposed for this version of Java. For instance, the deprecation for the Applet API. If you look at the Oracle release notes, we get even more detail, we can see which features are removed, which features are deprecated, and also, which features are new. If we look at the removed features, we can see in detail what's going on in here. If you're upgrading and you encounter any issues, or you want to know upfront what is being changed in a Java version, then this is a good place to get started.
Running Multiple JDKs
When you do the upgrade process, probably you're still working on the application to create new features and to deploy new features to production. You want to have the old version of Java running next to the new version of Java. There are various ways to do that. You, of course, can install and uninstall the various versions, but that's a bit cumbersome. For Maven, you can also use the JAVA_HOME variable, because Maven uses that to determine which JDK to use. If you change the JAVA_HOME variable, Maven will use that specific JDK that you configure there. Another option is to use Maven Toolchains. They allow you to specify the pom.xml, and specify there which JDK you want to use for that project. Unfortunately, I encountered some issues with the Maven Toolchains solution with the new versions of Java. It wasn't really useful for me in the upgrade process. The last option here is using Docker, which is a bit more work. You have to set it up, of course. You have to add your application to a Docker image, and then run it. The feedback loop is a bit longer. I would start with simply, first locally, with a local JDK, make sure you fix all the issues. Then maybe use Docker in a clean environment, without old configurations of yourself for specific settings, to make sure that it still works, so it also works in the build environment and in the production environment. Or, to simply create some examples like I did in the GitHub repository, where I also use Docker to run my examples against various Java versions.
To do that, I created a Dockerfile based on the Maven 16 release, Java 16. I added my Java application, and I ran the tests. I continue. If there is a failure, I simply want to see the end result including what's built. To build this, I use the Docker build command. I specify the tag, and I specify the dot, so the current directory is being used by Docker. This is a way to easily test if your code is running on the newest version of Java, in a clean environment.
Rolling Out a New Java Version
When we start developing against the new Java version, it's recommended to start with your local machine, get that up and running first. Then upgrade your build environment, so it supports the latest version of Java. Get everything built and tested over there. Then at the end, release it to the other environments, including the production environment. When upgrading Java, you might upgrade from 8 to 17 at once. If you've encountered any issues, it might be hard to pinpoint what's caused the exact issue. Therefore, it might be better to do the migration, step by step. Maybe you migrate from 8 to 11 first, or maybe from 8 to 9. Then if you encounter any issues, you can easily see what's changed in that Java version, or maybe Google for it. That way, it's easier to get the upgrades done instead of in one big bang.
Preparations
Let's look at the ingredients to get this upgrade process done. First, we start with an IDE with proper support for the Java version where we want to upgrade to, because then we already get a lot of warnings and feedback inside your IDE and we can relatively easily fix it. It's also important that you upgrade the build tools and plugins so that they support the latest version of Java. In case of Maven, we specify the compiler plugin to use Maven with Java 17. The next step is to basically upgrade the dependencies of our application. We can do that, already, on the current version of Java which we're using, and simply upgrade all the dependencies, check if the application still works. Then upgrade the Java version. Make sure you also upgrade the dependency that you created yourself. Keep in mind that if you upgrade your dependencies, sometimes they have new names. If you have a dependency and you update it to the latest version, but it's still a couple of years old, it might be good to check if there is another dependency available with the same functionality. There are some plugins available that help you check for latest versions of dependencies. For instance, for Maven, mvn versions:display-dependency-updates, is the command you can use to get an overview of the latest versions of your dependencies.
Cooking
How do we move forward? Now we start by compiling the source code on the new Java version, we run it. If that succeeds, and we make the necessary fixes, we go to running the unit tests. If we fix that as well, we can package the application, and in the end, we can run the application. Based on the nice ingredients, we should get a nice result. Of course, sometimes you miss some ingredients, or you lack a bit of time and you take some shortcuts. That's perfectly fine. The recipe I showed you is basically a recipe I think that works quite well, but some circumstances prevent you from using the recipe exactly. For instance, if some dependencies don't work on certain Java versions, or some dependencies are incompatible with each other. This basically is a way to get started. If you encounter any issues, feel free to deviate from it. That's up to you.
Java 11
Normally, you're going to see what's changed in the different versions of Java. If we look at Java 11, we see that JavaFX, which is used for the frontend, has been removed. As a replacement, we can use the OpenJFX build provided by Gluon. Another way is to use a JDK build which actually still includes support for OpenJFX. The normal JDK and the specification mention that JavaFX is removed. There are still vendors that build JDKs which still include OpenJFX support, such as the Liberica JDK that has a full build with a lot of extra tooling inside. There is also the ojdkbuild available. A third way to still keep using JavaFX is to use the Maven dependencies, which are documented on the OpenJFX website. Next to that, also the JDK fonts have been removed. Previously, in the JDK, some fonts were packaged, so your application could use the fonts of the JDK. For instance, with Apache POI dependency, you can use fonts to create or edit various Microsoft Office documents. Now those fonts are removed from the JDK. If those fonts aren't supplied by the operating system, you will get an error. The solution is basically to make sure the font support is provided by the operating system instead of through their JDK. To do that, you can install the packages that are mentioned.
Java Mission Control has also been removed. Java Mission Control can be used to monitor or profile your application and get deep insights into the workings of your application. It's no longer available as part of the JDK, but you can download separate builds on the AdoptOpenJDK or on the Oracle website. Nowadays, it's also called JDK Mission Control, so if you go for it you should use the new name for it. One of the bigger changes was the removal of the Java EE and CORBA modules. That impacts a lot of applications.
New Package Names
One of the changes there is that not only do you now need to use the packages yourself and add them as dependencies, but also, there are new packages being created. Previously, we had the javax packages, but they are no longer maintained. We now have new packages provided by Jakarta which are still maintained. You should use the artifacts mentioned at the bottom to make sure that you're on a current version of this functionality. Your application could use a simple javax import before because the CLI was provided by the JDK. Nowadays, you need to import the right Jakarta package and you need to add the Jakarta dependency through your application. That's not only the case for the CLI, but also for the activation, annotation, transaction, web services and for CORBA. Most of them have official replacements, except for CORBA, there's no official replacement, though you can use the glassfish implementation, which is also available. For the others, you can see in the column at the right, the latest versions of the artifacts which you should use as a replacement for the built-in functionality to JDK. As you see in the column at the right, for JAXB and JAX-WS, there are two artifacts that you need to include, both the API and the implementation should be included to make sure it still works.
Java 15
In Java 15, the Nashorn JavaScript engine was removed. However, if you still need that functionality, you can simply add the dependency as shown.
Java 16
In Java 16, they encapsulated some of the JDK internals, so some lower level stuff in the JDK. The JDK developers don't want you or other frameworks to use it directly. Basically, they try to hide it away. Some frameworks actually use those features directly, for instance Lombok. A solution for that is to upgrade your dependencies and make sure that you use a version of the dependency that's compatible with Java 16. Lombok 1.18.20 supports Java 16, so if you upgrade to that one, it works again.
Java 16 Workaround
If you have dependencies that aren't upgraded, or if you have code yourself that uses those JDK internals, then there is a workaround by specifying some compiler arguments in the Maven compiler plugin. Basically, these arguments can be used. However, the JDK developers basically made sure that those internals couldn't be used anymore. With this workaround, you still allow those features to be used, which isn't really nice. It's like removing a lock on a door, which is maybe good for a temporary solution. I think it's better to create a proper solution and make sure that you no longer use those internal JDK APIs that are now hidden away.
Maven Toolchains
Here, I also encountered some issues with the Maven Toolchains plugin. I ran Java 16 with an old version of Lombok, and then I got a compilation failure, exactly this error. For me, this error isn't really descriptive. I didn't know what to do, because no detail was given on what actually went wrong.
Java 16: Lombok
When I ran this again, without Maven Toolchains, and with Java 16 directly, I got a more descriptive error. The part at the end is probably the most interesting one, the module JDK compiler does not export processing to unnamed module. That's exactly what we exposed in the workaround, to make it available. This is a better descriptive error. If this is fixed in the Maven Toolchains plugin, then probably this is also a good way to work with different JDKs. For now, for me, I couldn't really use it because the compiler errors weren't descriptive enough.
Java 17
Java 17 is still under development, so it's not completely determined what will be in there and what items will be removed. Until now, the only thing that will be removed is the deprecated API for the Applets. It's not really a removal, but it is an indication that it will be removed. It shouldn't really be an issue for applications as Applets were no longer supported by browsers for quite a while already, so this shouldn't impact too many people.
Resources
If you want to have a bit more detail about this, you of course can have a look at the GitHub page, github.com/johanjanssen/JavaUpgrades. It shows a lot of information including the links to all the detailed changes in the Java versions, and what is being removed, in detail. For instance, if you get this error, then you probably need to add this dependency. There is a lot of detailed information in here. There are quite a few examples for the different Java versions in here as well. I can show an example in IntelliJ. If we look at the lombok_broken example, you can see that here, the version 1.18.18 was used, and in the fixed one, 1.18.20 was used. If I now run this on Java 15, this should still work, even the broken example, because it was only changed in Java 16. We can actually see that indeed on Java 15, everything still works. If we now change this to Java 16, we can now see that the broken one indeed failed, because on Java 16, we had to fix it. That's the one that you see here. That one passed, and it was a success.
Basically, now, we conquered our challenge where we've finished our Java upgrade and now the real fun can actually start. We can start using those cool, new features and start benefiting from our upgrade.
See more presentations with transcripts