Backend Development 12 min read

Seamless Java JDK 21 Upgrade: Solving Dependency and Build Challenges

This article outlines the motivations, progress, and common issues encountered when upgrading over 100 Java applications to JDK 21, including deprecated APIs, module system constraints, Maven plugin incompatibilities, and GC choices, and provides concrete migration steps, configuration templates, and best‑practice recommendations for a smooth transition.

JD Cloud Developers
JD Cloud Developers
JD Cloud Developers
Seamless Java JDK 21 Upgrade: Solving Dependency and Build Challenges

Background and Challenges

Upgrade Drivers

Oracle long‑term support strategy

Modern feature requirements: coroutines, pattern matching, ZGC, etc.

Security and performance needs

Version requirements introduced by new AI technologies

Project Situation

Coordinated upgrade of over 100 projects

Multiple technology stacks coexist

Adaptation challenges for the continuous‑integration system

Progress

Total Applications

Completed

Decommissioned

Pending Upgrade

100+

73

13

10+

Main Problem Domains and Solutions

1. Dependency "Butterfly Effect"

Internal APIs such as

sun.misc.BASE64Encoder

removed → compilation errors

JAXB/JAX‑WS stripped from JDK core → XML processing chain broken

Lombok compatibility issues with new compiler (especially

record

types)

Root cause: JEP 320 (Encapsulate JDK Internals) – https://openjdk.org/jeps/320

Case 1: Legacy SDK compilation trap

<code>Compilation failure: Compilation failure:
#14 4.173 [ERROR] Source option 6 is no longer supported. Use 8 or higher.
#14 4.173 [ERROR] Target option 6 is no longer supported. Use 8 or higher.</code>

Old Maven compiler plugin configuration:

<code>&lt;plugin&gt;
  &lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;
  &lt;artifactId&gt;maven-compiler-plugin&lt;/artifactId&gt;
  &lt;version&gt;3.5&lt;/version&gt;
  &lt;configuration&gt;
    &lt;source&gt;1.6&lt;/source&gt;
    &lt;target&gt;1.6&lt;/target&gt;
  &lt;/configuration&gt;
&lt;/plugin&gt;
</code>

Updated configuration using

release

parameter:

<code>&lt;plugin&gt;
  &lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;
  &lt;artifactId&gt;maven-compiler-plugin&lt;/artifactId&gt;
  &lt;version&gt;3.13.0&lt;/version&gt;
  &lt;configuration&gt;
    &lt;release&gt;8&lt;!-- unified release parameter --&gt;
  &lt;/configuration&gt;
&lt;/plugin&gt;
</code>

Case 2: JAXB modular removal

<code>javax.xml.bind.JAXBException: Implementation of JAXB‑API has not been found</code>
<code>&lt;dependency&gt;
  &lt;groupId&gt;org.glassfish.jaxb&lt;/groupId&gt;
  &lt;artifactId&gt;jaxb-runtime&lt;/artifactId&gt;
  &lt;version&gt;4.0.5&lt;/version&gt;
&lt;/dependency&gt;
</code>

Case 3: Lombok compatibility with new compiler

<code>java.lang.NoSuchFieldError</code>
<code>&lt;dependency&gt;
  &lt;groupId&gt;org.projectlombok&lt;/groupId&gt;
  &lt;artifactId&gt;lombok&lt;/artifactId&gt;
  &lt;version&gt;1.18.30&lt;/version&gt;
&lt;/dependency&gt;
</code>

Case 4: Missing @Resource annotation

<code>Caused by: java.lang.NoSuchMethodError: 'java.lang.String javax.annotation.Resource.lookup()'</code>
<code>&lt;dependency&gt;
  &lt;groupId&gt;jakarta.annotation&lt;/groupId&gt;
  &lt;artifactId&gt;jakarta.annotation-api&lt;/artifactId&gt;
  &lt;version&gt;1.3.5&lt;/version&gt;
&lt;/dependency&gt;
</code>

Recommendation: Use the Jakarta standard and exclude the old

jsr250-api

dependency.

2. Modular "Wall" and Reflection Access

Example error:

<code>[ERROR] Unable to make field private int java.text.SimpleDateFormat.serialVersionOnStream accessible</code>

Solution: Add module‑open JVM arguments, e.g.:

<code>--add-opens java.base/java.text=ALL-UNNAMED
--add-opens java.base/java.lang.reflect=ALL-UNNAMED
</code>

Full template for module opening:

<code>export JAVA_OPTS="-Djava.library.path=/usr/local/lib -server -Xmx4096m \
--add-opens java.base/sun.security.action=ALL-UNNAMED \
--add-opens java.base/java.lang=ALL-UNNAMED \
--add-opens java.base/java.math=ALL-UNNAMED \
--add-opens java.base/java.util=ALL-UNNAMED \
--add-opens java.base/sun.util.calendar=ALL-UNNAMED \
--add-opens java.base/java.util.concurrent=ALL-UNNAMED \
--add-opens java.base/java.util.concurrent.locks=ALL-UNNAMED \
--add-opens java.base/java.security=ALL-UNNAMED \
--add-opens java.base/jdk.internal.loader=ALL-UNNAMED \
--add-opens java.management/com.sun.jmx.mbeanserver=ALL-UNNAMED \
--add-opens java.base/java.net=ALL-UNNAMED \
--add-opens java.base/sun.nio.ch=ALL-UNNAMED \
--add-opens java.management/java.lang.management=ALL-UNNAMED \
--add-opens jdk.management/com.sun.management.internal=ALL-UNNAMED \
--add-opens java.management/sun.management=ALL-UNNAMED \
--add-opens java.base/sun.security.action=ALL-UNNAMED \
--add-opens java.base/sun.net.util=ALL-UNNAMED \
--add-opens java.base/java.time=ALL-UNNAMED \
--add-opens java.base/java.lang.reflect=ALL-UNNAMED \
--add-opens java.base/java.io=ALL-UNNAMED"
</code>

3. Syntax‑Level "Time‑Travel"

Case 1: Base64 encoding rewrite

<code>// JDK 8 style (deprecated)
BASE64Encoder encoder = new BASE64Encoder();
String encoded = encoder.encode(data);

// JDK 21 style
Base64.Encoder encoder = Base64.getEncoder();
String encoded = encoder.encodeToString(data);
</code>

Case 2: Date serialization issue

<code>Caused by: java.lang.reflect.InaccessibleObjectException:
Unable to make field private int java.text.SimpleDateFormat.serialVersionOnStream accessible
</code>

Solution: Use

DateTimeFormatter

instead of

SimpleDateFormat

, or add the appropriate

--add-opens

flag.

Best‑Practice Summary

1. Local Compilation

Compile locally first to catch syntax errors, version conflicts, and incompatibilities. Typical scenarios include Base64 migration, Lombok version upgrade, annotation package conflicts, and Maven plugin upgrades.

2. Cloud Build

Align with local compilation results; ensure proper JDK 21 image and module opening configuration.

3. Cloud Deployment

Address image mismatches, module permissions, and JDSecurity encryption handling. Use the provided module‑opening template.

4. JVM Tuning

Choose appropriate garbage collector based on workload:

UseParallelGC – high throughput, longer pauses, suitable for batch processing.

UseG1GC – balanced throughput and latency, suitable for moderate‑size heaps.

UseZGC – ultra‑low latency, suitable for very large heaps (TB scale).

Adjust GC choice according to application requirements.

Image
Image
JavaMigrationMavenJDK21GarbageCollectionDependencyManagementModuleSystem
JD Cloud Developers
Written by

JD Cloud Developers

JD Cloud Developers (Developer of JD Technology) is a JD Technology Group platform offering technical sharing and communication for AI, cloud computing, IoT and related developers. It publishes JD product technical information, industry content, and tech event news. Embrace technology and partner with developers to envision the future.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.