Upgrading to Java 17, or: how I learned to stop worrying and love NoSuchMethodError
Icon recently released IPF 2024.1, which included upgrading all of our components to Java 17 and required Java 17 as a minimum. I was the lead engineer on this piece of work, and – now that it’s out in the wild – I decided to take some time to write about the experience, including:
…which you may find useful if you’re looking to embark on a similar journey yourself, whether you use IPF or not.
But why?
Well, besides the normal security and supportability reasons for our customers, the main reason a Java upgrade came onto our radar was this announcement in 2022 from Juergen Hoeller stating that Spring 6+ and Spring Boot 3+ will only be supporting Java 17+. Also, there were increasing grumblings and growing dissent among the ranks as they were not able to use some of the cool new features in Java 12→17, such as records, sealed types, switch expressions (to name but a few). So, not wanting to be ousted in a mutiny, I proposed that we should allow some time to target an IPF release, do this upgrade, and communicate the change to customers.
In addition, there have been generational garbage collection (GC) improvements between Java 8 and 17, even if just using the default GC configuration. This blog post by Stefan Johnasson from the JDK GC team at Oracle describes the GC improvements the GC team has managed to achieve for the JDK between Java 8 and Java 17.
Blast from the past?
You read that right. We moved from Java 11 to 17, and not to 21. The main reason was that it was a lowest common denominator: we surveyed all of our customers around October/November last year (2023), and all of them replied that they had support for Java 17 (released ~2 years before the survey), but only one or two declared support for Java 21 (released ~2 months before the survey).
Another reason is that we wouldn’t have benefited from arguably the biggest change to Java 21: virtual threads. This is because Akka – the non-blocking, message-oriented, async framework we use for event sourcing, clustering, and a bunch of other things – only became certified for Java 21 as part of their 24.05 release, which was released on 17 May 2024. We wouldn’t be independently writing virtual thread code as part of this upgrade, so Java 17 allowed us to be compatible with Spring 6/Spring Boot 3, and helped me avoid walking the plank.
Frankly, the world of payments processing is too important to risk, and there are no prizes for introducing the risk of being an early adopter. Let someone else (other domains and use cases) iron out the kinks!
Character-building rabbit hole
So, what have I got to do? We use Maven for our build and dependency management, so surely I can just change some magical permutation of properties and plugin configurations according to this mini novella on StackOverflow to change the Java version from 11 to 17. OK, that’s done, and…
[ERROR] Failed to execute goal [org.jvnet.jaxb2.maven2:maven-jaxb2-plugin:0.14.0:generate (default) on project shared-domain: Execution default of goal org.jvnet.jaxb2.maven2:maven-jaxb2-plugin:0.14.0:generate failed: An API incompatibility was encountered while executing org.jvnet.jaxb2.maven2:maven-jaxb2-plugin:0.14.0:generate: java.lang.ExceptionInInitializerError: null
What a delightfully helpful and informative error message to start me off on my journey. The maven-jaxb2-plugin
has exploded into smithereens.
Because we are ISO20022-based, and most of the payment schemes we support are also ISO20022-based, we have to deal with a lot of XML This means that we have quite a few JAXB Maven plugin executions that take in an XML schema and generate value objects. Digging into the exception, we get a bunch of nonsense I will not reproduce here, but the root cause was #1 of three horsemen of the apocalypse I would continue to encounter for about a month:
Horseman
Error message
What it means
1
java.lang.NoSuchMethodError: [some class or method]
I expected this class or method but it’s not here any more (or its signature has changed)
2
java.lang.reflect.InaccessibleObjectException: Unable to make [a] accessible: module [b] does not "opens [c]" to [d]
Moving to Java 17 is fun
3
(anything else)
Mystery fun time!
So I just had to upgrade this and many other plugins and dependencies. As you can imagine though, defeating one horseman from the above table simply summoned another. So I had to keep going around and consulting many GitHub issues, StackOverflow pages, forum posts and so on, until I was able to achieve a set of dependencies and BOMs that could hang together without causing a compile-, build-, or runtime explosion.
Inevitable fallout
The main source of the issues I had was not the upgrade of Java 17 itself, but actually the various breaking changes introduced by Spring 6/Boot 3; for example, moving from Java EE to Jakarta EE. This caused a significant amount of breakage. To cope with this, in addition to updating quite literally every dependency that used Java EE annotations to Jakarta EE, we even had to ask our partner Lightbend to make a special release of Alpakka to support Jakarta Messaging! After doing a painful find-and-replace on a few projects, I actually developed a terrible alias (which I will not reproduce here for fear of reprisals) that I ran on the root of each project before trying to build it which would do the following things:
javax.*
with jakarta.*
– this sometimes blew up in my face, but I want to say it was a net positive?
We actually wrote a guide for our customers on upgrading to Java 17/Spring 6/Spring Boot 3 containing all issues that I came across which they too might come across in their IPF implementations. I won’t reproduce it here because this post is already dangerously long, but you can click here to view our migration docs that contain this advice.
“Why not use…?”
I am aware of the existence of OpenRewrite – but honestly, most of the challenges I’d faced were not ones that this was designed to address (not easily, anyway), and I decided that it was causing more issues than it was solving (adding weird dependencies because of it not being aware of dependencyManagement
of a parent project, putting files in strange places). I don’t think our projects are niche or unusual, but it was taking more time to understand OpenRewrite’s strange behaviour than to write the alias mentioned above.
Conclusion and learnings
I don’t know if it came across to you, dear reader, but this was a therapeutic exercise for me. I think it was slightly more difficult than I had initially imagined, but it was a nice learning experience for me and my colleagues who helped me out. We also gained some insights that will make any future upgrade easier:
I hope you have enjoyed this tale, with its twists and turns. We will keep an eye on things as our customers adopt IPF 2024.1 and see what they encounter along their Java 17 journey. I may very well link them to this therapy session!