Upgrading to Java 17, or: how I learned to stop worrying and love NoSuchMethodError

14 June 2024

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:

  • The background behind it all
  • Some pitfalls to avoid
  • Some new things to look out for in Java 17/Spring 6/Spring Boot 3

…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 recordssealed typesswitch 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

Here be dragons! I’m getting into the weeds a bit here. If you’re not interested in this stuff then you can skip this section.

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:

  • Replace instances of javax.* with jakarta.* – this sometimes blew up in my face, but I want to say it was a net positive?
  • Remove any references to Java 11/Java versions/compiler plugins/etc. (this is centralised and shouldn’t be in individual projects)
  • Remove references to any weird plugin definition overrides that we were creating as part of our regular project creation job and were causing issues

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:

  • Don’t spray random plugin executions and dependencies throughout your projects: I have noticed that engineers tend to do this when trying to solve a problem and copy-and-pasting from StackOverflow/Baeldung/mkyong/etc. Those answers and articles are usually a number of years old and use outdated plugin executions or dependency versions. Have a single parent project which controls all plugin versions, dependency versions, etc. and avoid having any specific version overrides in child dependencies. Check out FasterXML/oss-parent as an example.
  • Stay on top of dependencies and vulnerabilities: Ensure that versions – especially those of plugins – are always updated. This will lead to a smoother landing when having to do a more sweeping upgrade such as the one described in this post. Consider using platform tooling like Dependabot if on GitHub, or renovate for other platforms, which performs a similar job.
  • Minimum Boy Scout: Is this a controversial take? Of course you should leave things neater than when you found them, but after moving a project to Java 17 I had lots of lovely attempts to help by IntelliJ: “Can be a record!”, “Can be a text block!”, “Use pattern variable!”, “Replace with switch expression!” – these are all wonderful suggestions, but you are soon distracted by them and it will make the job take that little bit longer. Also, I’d argue this barely qualifies for the boy scout rule: does migrating the code to a more modern form of itself count as leaving it “neater than it was”?

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!

Patrick Altaie