Upgrade Java Projects to JDK 21: Challenges, Solutions & Best Practices
This article outlines the motivations, challenges, and step‑by‑step solutions for migrating over 100 Java applications to JDK 21, covering dependency conflicts, module system adjustments, Maven plugin compatibility, garbage‑collector selection, and practical build and deployment practices to ensure a smooth upgrade.
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 parallel projects
Multiple technology stacks coexist
Continuous‑integration system adaptation challenges
Progress
Application Total
Completed
Offline
Pending Upgrade
100+
73
13
10+
Main Problem Domains and Solutions
1. Dependency “Butterfly Effect”
sun.misc.BASE64Encoder and other internal APIs deprecated → compilation errors
JAXB/JAX‑WS removed from JDK core → XML processing chain broken
Lombok compatibility issues with new compiler (especially record types)
Root cause is JEP 320: 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> <code><!-- Old compiler configuration causing build failure -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5</version>
<configuration>
<source>1.6</source>
<target>1.6</target>
</configuration>
</plugin></code>Updated configuration:
<code><plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<release>8</release><!-- Use release parameter -->
</configuration>
</plugin></code>Case 2: JAXB modular removal
<code>javax.xml.bind.JAXBException: Implementation of JAXB‑API has not been found</code> <code><dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>4.0.5</version>
</dependency></code>Case 3: Lombok and new compiler compatibility
<code>java: java.lang.NoSuchFieldError</code> <code><dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</dependency></code>Case 4: Missing Resource annotation
<code>Caused by: java.lang.NoSuchMethodError: 'java.lang.String javax.annotation.Resource.lookup()'</code> <code><dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>1.3.5</version>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency></code>Recommended unified version: jakarta.annotation:jakarta.annotation-api (2.1.1)
2. Modular Break and Build
Reflection access module wall
<code>[ERROR] Unable to make field private int java.text.SimpleDateFormat.serialVersionOnStream accessible</code> <code># Add module opens
--add-opens java.base/java.text=ALL-UNNAMED
--add-opens java.base/java.lang.reflect=ALL-UNNAMED</code>Full module‑open configuration template:
<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 “Time‑Travel”
Case 1: Base64 encoding refactor
<code>// JDK8 deprecated way
BASE64Encoder encoder = new BASE64Encoder();
String encoded = encoder.encode(data);
// JDK21 standard way
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
Replace SimpleDateFormat with DateTimeFormatter
Add module‑open parameter: --add-opens java.base/java.text=ALL-UNNAMED
4. Hidden “Dependency War”
Annotation package conflict example
<code>[ERROR] javax.annotation.Resource exists in both jsr250-api-1.0.jar and jakarta.annotation-api-1.3.5.jar</code> <code><!-- Use Jakarta standard -->
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>2.1.1</version>
</dependency>
<!-- Exclude old version -->
<exclusions>
<exclusion>
<groupId>javax.annotation</groupId>
<artifactId>jsr250-api</artifactId>
</exclusion>
</exclusions></code>5. Build System Refactor
Maven plugin compatibility issue
<code>[ERROR] The plugin org.apache.maven.plugins:maven-compiler-plugin:3.13.0 requires Maven version 3.6.3</code>Upgrade strategy:
Upgrade Maven version
Standardize plugin versions
<code><build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.4.0</version>
</plugin>
</plugins>
</pluginManagement>
</build></code>Best‑Practice Summary
1. Local Compilation
Compile locally first to detect syntax errors, version conflicts, and incompatibilities. Key scenarios include Base64 refactor, Lombok version upgrade, annotation package conflicts, and Maven plugin upgrades.
2. Cloud Build
Same as local compilation, but executed in CI/CD environment.
3. Cloud Deployment
a) Use matching JDK21 Docker image or custom image. b) Apply full module‑open configuration template. c) Handle JDSecurity encryption/decryption. d) Process important.properties with PropertyPlaceholderConfigurer, not JDSecurityPropertyFactoryBean.
4. Runtime Issues
a) Serialization exception caused by list view input; fix by converting sublist to new ArrayList. b) Thread‑context class not found; prefer explicit thread pools in multithreaded scenarios.
5. JVM Tuning
Garbage‑collector options:
Feature
UseParallelGC
UseG1GC
UseZGC
Design Goal
High throughput
Balance throughput and latency
Ultra‑low latency
Pause Time
Longer
Shorter
Very short
Heap Size
Small‑to‑medium (GBs)
Large (tens‑to‑hundreds GB)
Very large (TB)
CPU Cost
Medium
Medium
Higher
Typical Use
Batch, compute‑intensive
Latency‑sensitive apps
Latency‑critical apps
Choose the GC that matches your application's throughput and latency requirements.
JD Tech Talk
Official JD Tech public account delivering best practices and technology innovation.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.