Getting off Java 8: what actually breaks

Java 8 Course · lesson 16 of 16 · 7 min read

The four failures that stop most Java 8 migrations, and how to clear them.

Open this lesson in the learning hub

Key points

  • The single biggest blocker is removed internal APIs. Java 9 encapsulated sun.misc.Unsafe and the javax.xml.bind family moved out of the JDK entirely, so code or libraries using them fail to compile or fail at runtime.
  • Second is reflection into the JDK. Libraries that call setAccessible on JDK internals got warnings in 9-16 and hard errors from 17, which is why an upgrade that worked on 11 can break on 17.
  • Third is bytecode level. Old versions of ASM, cglib, Lombok and mocking libraries cannot read newer class files, and the error - often an ArrayIndexOutOfBoundsException deep inside a library - names nothing useful.
  • Fourth is date and locale data. Java 9 switched to CLDR as the default locale provider, so formatted dates and numbers change subtly, which silently breaks assertions and report output.
  • The tool for finding all of this is jdeps. Run it against your full dependency set to list every internal API used, before changing a single line.
  • Go via 17 rather than jumping straight to 21 or 25. Each LTS has its own removals, and debugging one set of failures at a time is much faster than debugging three.

Example

# 1. Find internal API usage BEFORE you start.
$ jdeps --jdk-internals --multi-release 21 -R -cp "libs/*" build/libs/app.jar
#   app.jar -> JDK removed internal API
#     com.example.Hack -> sun.misc.Unsafe   JDK internal API (JDK removed)
#   Use --print-module-deps to see what module each dependency needs.

# 2. The javax.* packages that LEFT the JDK. Add them back explicitly:
#   javax.xml.bind      -> jakarta.xml.bind-api + jaxb-runtime
#   javax.activation    -> jakarta.activation-api
#   javax.annotation    -> jakarta.annotation-api
#   java.xml.ws         -> jakarta.xml.ws-api
#   com.sun.image.codec -> use ImageIO

# 3. Reflection into the JDK: a warning on 11, an ERROR from 17.
#   Temporarily reopen, THEN fix the library:
--add-opens java.base/java.lang=ALL-UNNAMED
--add-opens java.base/java.util=ALL-UNNAMED
#   Treat these as a migration crutch. Every one you keep is a library
#   that will break again on the next upgrade.

# 4. Locale data changed in Java 9 (CLDR). Restore the old behaviour
#    temporarily if reports or tests depend on it:
-Djava.locale.providers=COMPAT,CLDR
#   COMPAT is deprecated - use it to unblock, then fix the assertions.

---
# THE ORDER THAT WORKS:
#   1. upgrade the BUILD and its plugins first, still targeting 8
#   2. upgrade libraries to versions that support the new JDK
#   3. compile with --release 8 on the new JDK (catches API misuse early)
#   4. move to 11, then 17, then 21 - one LTS at a time
#   5. only then start using new language features
#
# Doing 5 before 4 is the classic mistake: now you cannot roll back.

# Verify what you actually produced:
$ javap -verbose -cp build/classes Main | grep "major version"
#   52 = Java 8, 55 = 11, 61 = 17, 65 = 21, 69 = 25

Run jdeps before changing anything, move one LTS at a time, and treat every --add-opens as a library you still have to fix.

This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the Java 8 Course course, and every lesson in it is listed on the Java 8 Course contents page.