Java 17 Migration Guide (11→17)

1. Introduction

Java 11 was released in September 2018. Java 17 was released in September 2021. Both are Long-Term Support (LTS) releases, and migrating from 11 to 17 is one of the most common upgrade paths in the Java ecosystem today. If you are still on Java 11, you are missing three years of language features, performance improvements, and security updates.

Unlike the Java 8 to 11 migration (which broke many applications due to module system changes and API removals), the 11 to 17 migration is significantly smoother. Most of the painful changes happened in Java 9-11. The 12-17 releases are largely additive — new language features, new APIs, and incremental improvements. That said, there are breaking changes you must plan for, particularly around strong encapsulation of JDK internals.

Here is what is at stake:

Factor Java 11 Java 17
LTS Support (Oracle) Extended support until September 2026 Extended support until September 2029
LTS Support (Adoptium/Eclipse) Available but winding down Actively maintained
Spring Boot Compatibility Spring Boot 2.x (maintenance mode) Spring Boot 3.x (active development, Java 17 required)
Language Features var, HTTP Client Records, sealed classes, pattern matching, text blocks, switch expressions
Performance Baseline 15-20% GC improvements, faster startup, smaller footprint
Security Known CVEs accumulating Latest security patches

The bottom line: If you are using Spring Boot and plan to stay current, you must migrate to Java 17 because Spring Boot 3.x requires it. Even without Spring, the language features and performance improvements alone justify the upgrade.

2. New Language Features Summary

Here is every language feature added between Java 12 and Java 17. Features marked as “Standard” are finalized and ready for production use:

Java Version Feature Status in 17 JEP
Java 12 Switch Expressions (preview) Standard (Java 14) JEP 325
Java 12 Compact Number Formatting Standard
Java 13 Text Blocks (preview) Standard (Java 15) JEP 355
Java 14 Switch Expressions (finalized) Standard JEP 361
Java 14 Helpful NullPointerExceptions Standard (default on) JEP 358
Java 14 Records (preview) Standard (Java 16) JEP 359
Java 14 Pattern Matching instanceof (preview) Standard (Java 16) JEP 305
Java 15 Text Blocks (finalized) Standard JEP 378
Java 15 Sealed Classes (preview) Standard (Java 17) JEP 360
Java 16 Records (finalized) Standard JEP 395
Java 16 Pattern Matching instanceof (finalized) Standard JEP 394
Java 16 Stream.toList() Standard
Java 17 Sealed Classes (finalized) Standard JEP 409
Java 17 RandomGenerator API Standard JEP 356
Java 17 Foreign Function & Memory API Incubator JEP 412
Java 17 Vector API Incubator (2nd) JEP 414

3. Breaking Changes

While the 11 to 17 migration is smoother than 8 to 11, there are still breaking changes that can cause compilation errors and runtime failures. Here are the ones you must address:

3.1 Strong Encapsulation (Most Impactful)

In Java 11, the --illegal-access=permit flag was the default, which meant reflective access to JDK internals produced warnings but still worked. In Java 17, the flag is removed entirely. All reflective access to JDK internals is blocked by default.

// This code compiled and ran on Java 11 with a warning
// On Java 17, it throws InaccessibleObjectException
var field = String.class.getDeclaredField("value");
field.setAccessible(true); // FAILS on Java 17

// Fix: Use --add-opens as a temporary workaround
// java --add-opens java.base/java.lang=ALL-UNNAMED -jar app.jar

// Better fix: Stop using internal APIs and use public alternatives

3.2 Removed APIs

Removed API Removed In Replacement
Nashorn JavaScript Engine Java 15 GraalJS or standalone V8
RMI Activation (java.rmi.activation) Java 17 gRPC, REST APIs, or standard RMI
Pack200 API and tools Java 14 Standard compression (gzip, bzip2)
AOT and JIT compiler (Graal-based) Java 17 GraalVM (external)
Solaris and SPARC ports Java 17 Linux, macOS, or Windows

3.3 Behavioral Changes

// 1. Stricter JAR signature validation
// Java 17 rejects JARs signed with SHA-1 by default
// Fix: Re-sign JARs with SHA-256 or stronger

// 2. Default charset change (Java 18 heads-up)
// Java 17 still uses platform-specific default charset
// But Java 18 defaults to UTF-8. Start using explicit charsets now:
Files.readString(path, StandardCharsets.UTF_8);  // explicit -- always safe
Files.readString(path);  // uses platform default -- risky

// 3. DatagramSocket reimplementation (Java 15+)
// The underlying implementation of DatagramSocket was rewritten
// Unlikely to affect most apps, but test network code thoroughly

4. Step-by-Step Migration

Follow this checklist in order. Each step builds on the previous one.

Step 1: Audit Your Dependencies

Before changing any JDK version, check that all your dependencies support Java 17. This is the single most important step.







Step 2: Update Build Tool Plugins



    org.apache.maven.plugins
    maven-compiler-plugin
    3.11.0
    
        17
    




    org.apache.maven.plugins
    maven-surefire-plugin
    3.1.2

Step 3: Update Critical Dependencies

Dependency Minimum for Java 17 Recommended
Spring Boot 2.5.x 3.x (requires Java 17)
Spring Framework 5.3.15 6.x (requires Java 17)
Hibernate 5.6.x 6.x
Lombok 1.18.22 1.18.30+
Mockito 4.0 5.x
Jackson 2.13 2.15+
JUnit 5 5.8 5.10+
ByteBuddy 1.12 1.14+
ASM 9.0 9.5+
Flyway 8.0 9.x+

Step 4: Install Java 17 JDK

# Using SDKMAN (recommended)
sdk install java 17.0.9-tem
sdk use java 17.0.9-tem

# Or download directly:
# - Eclipse Temurin: https://adoptium.net/
# - Amazon Corretto: https://aws.amazon.com/corretto/
# - Oracle JDK: https://www.oracle.com/java/

# Verify installation
java --version
# openjdk 17.0.9 2023-10-17
# OpenJDK Runtime Environment Temurin-17.0.9+9 (build 17.0.9+9)

# Set JAVA_HOME
export JAVA_HOME=$HOME/.sdkman/candidates/java/17.0.9-tem

Step 5: Compile and Fix Errors

Compile your project with Java 17 and address each compilation error. The most common errors fall into these categories:

// Error 1: Using removed APIs
// javax.script.ScriptEngine with Nashorn
// Fix: Remove Nashorn usage or add GraalJS dependency

// Error 2: Deprecated API warnings (promoted to errors)
// Fix: Replace deprecated APIs with recommended alternatives

// Error 3: Internal API access
// sun.misc.Unsafe, com.sun.*, etc.
// Fix: Use public API alternatives or --add-opens as temporary fix

// Error 4: New reserved keywords
// "sealed", "permits" are now restricted identifiers
// If you have variables named "sealed" or "permits", rename them
String sealed = "value"; // Compile error in Java 17 if used as type name

Step 6: Run jdeps to Find Internal API Usage

# Scan your application for JDK internal API usage
jdeps --jdk-internals --multi-release 17 -cp 'lib/*' your-app.jar

# Sample output:
# your-app.jar -> java.base
#   com.example.MyClass -> sun.misc.Unsafe (JDK internal API)
#
# Warning: JDK internal APIs are unsupported and private to JDK implementation
# that are subject to be removed or changed incompatibly and could break your
# application.
#
# JDK Internal API                Suggested Replacement
# ----------------                ---------------------
# sun.misc.Unsafe                 Use VarHandle or MethodHandle API

Step 7: Add –add-opens for Libraries (If Needed)

Some libraries (especially older versions of ORMs, serialization frameworks, and mocking tools) need reflective access to JDK internals. Add --add-opens flags as needed:



    org.apache.maven.plugins
    maven-surefire-plugin
    3.1.2
    
        
            --add-opens java.base/java.lang=ALL-UNNAMED
            --add-opens java.base/java.lang.reflect=ALL-UNNAMED
            --add-opens java.base/java.util=ALL-UNNAMED
        
    




Step 8: Run All Tests

After compilation succeeds, run your entire test suite. Pay special attention to:

  • Serialization/deserialization tests
  • Reflection-heavy code (custom annotations, DI containers)
  • Network and socket tests (DatagramSocket reimplementation)
  • Security-related tests (SecurityManager deprecation)
  • Date/time formatting tests (locale data updates)

Step 9: Performance Testing

Java 17 includes significant GC improvements. Run performance benchmarks to validate:

# Compare GC performance between Java 11 and Java 17
# Run your application with GC logging enabled

# Java 17 with G1GC (default)
java -Xlog:gc*:file=gc-java17.log -jar app.jar

# Key improvements in Java 17 GC:
# - G1GC: Reduced pause times, better throughput
# - ZGC: Production-ready, sub-millisecond pauses (Java 15+)
# - Shenandoah: Production-ready, low-pause concurrent GC

# Consider switching GC if applicable:
# java -XX:+UseZGC -jar app.jar          # Ultra-low pause times
# java -XX:+UseShenandoahGC -jar app.jar # Low-pause alternative

Step 10: Adopt New Language Features Incrementally

Once your application runs on Java 17, start adopting new language features gradually. You do not need to rewrite everything at once:

// Priority 1: Use text blocks for multi-line strings (immediate wins)
// Before
String sql = "SELECT u.id, u.name, u.email\n" +
             "FROM users u\n" +
             "WHERE u.active = true\n" +
             "ORDER BY u.name";

// After
String sql = """
    SELECT u.id, u.name, u.email
    FROM users u
    WHERE u.active = true
    ORDER BY u.name
    """;

// Priority 2: Use pattern matching instanceof (reduce casting)
// Before
if (obj instanceof String) {
    String s = (String) obj;
    System.out.println(s.length());
}

// After
if (obj instanceof String s) {
    System.out.println(s.length());
}

// Priority 3: Use switch expressions where appropriate
// Before
String label;
switch (status) {
    case ACTIVE:   label = "Active";   break;
    case INACTIVE: label = "Inactive"; break;
    case PENDING:  label = "Pending";  break;
    default:       label = "Unknown";
}

// After
String label = switch (status) {
    case ACTIVE   -> "Active";
    case INACTIVE -> "Inactive";
    case PENDING  -> "Pending";
};

// Priority 4: Use records for new DTOs and value objects
// Priority 5: Use sealed classes for new type hierarchies

5. Build Tool Updates

5.1 Maven



    17
    17
    17
    
    17



    
        
        
            org.apache.maven.plugins
            maven-compiler-plugin
            3.11.0
            
                17
            
        

        
        
            org.apache.maven.plugins
            maven-surefire-plugin
            3.1.2
        

        
        
            org.apache.maven.plugins
            maven-failsafe-plugin
            3.1.2
        

        
        
            org.apache.maven.plugins
            maven-jar-plugin
            3.3.0
        
    

5.2 Gradle

// build.gradle updates for Java 17
// Gradle 7.3+ is required for Java 17 support

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.2.0' // if using Spring Boot
}

java {
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

tasks.withType(JavaCompile).configureEach {
    options.release = 17
}

// For Kotlin DSL (build.gradle.kts)
java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(17))
    }
}

6. Framework Compatibility

6.1 Spring Boot 3.x (Requires Java 17)

If you are migrating to Spring Boot 3.x, Java 17 is the minimum requirement. This is the most significant framework change most Java developers will encounter:

// Key Spring Boot 3.x changes:
// 1. javax.* -> jakarta.* namespace migration
//    javax.persistence.* -> jakarta.persistence.*
//    javax.servlet.*     -> jakarta.servlet.*
//    javax.validation.*  -> jakarta.validation.*
//    javax.annotation.*  -> jakarta.annotation.*

// Before (Spring Boot 2.x / Java 11)
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.servlet.http.HttpServletRequest;
import javax.validation.constraints.NotNull;

// After (Spring Boot 3.x / Java 17)
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.constraints.NotNull;

Spring Boot 2.x to 3.x migration checklist:

  • Replace all javax.* imports with jakarta.*
  • Update Spring Security configuration (new Lambda DSL)
  • Update Spring Data repositories (new method signatures)
  • Replace WebSecurityConfigurerAdapter with SecurityFilterChain bean
  • Update Actuator endpoint paths if customized

6.2 Hibernate 6

Hibernate 6 is the default JPA provider in Spring Boot 3.x. Key changes:

  • javax.persistence to jakarta.persistence namespace
  • New type system for custom types
  • Some HQL query syntax changes
  • Removed deprecated criteria API methods

6.3 Compatibility Matrix

Framework/Library Java 11 Compatible Java 17 Compatible Java 17 Required
Spring Boot 2.7.x Yes Yes No
Spring Boot 3.x No Yes Yes
Quarkus 3.x No Yes Yes (11+ for 2.x)
Micronaut 4.x No Yes Yes
Jakarta EE 10 No Yes Yes (11 minimum)
Hibernate 5.6 Yes Yes No
Hibernate 6.x No Yes Yes (11 minimum)
JUnit 5.9+ Yes Yes No (Java 8+)

7. Common Migration Issues

Here are the most common problems teams encounter during the 11 to 17 migration, along with their solutions:

# Problem Symptom Solution
1 InaccessibleObjectException Library uses reflection on JDK internals Update library or add --add-opens
2 Lombok compilation failure java.lang.IllegalAccessError Update Lombok to 1.18.22+
3 Mockito/ByteBuddy failure Cannot access internal API for mocking Update Mockito to 4.0+, ByteBuddy 1.12+
4 ASM version incompatibility UnsupportedClassVersionError Update ASM to 9.0+ (transitive dependency)
5 Nashorn removal ScriptEngineManager returns null for “nashorn” Add GraalJS dependency or rewrite
6 JAR signature failures SHA-1 signed JARs rejected Re-sign with SHA-256 or disable validation
7 Gradle version too old Cannot compile Java 17 sources Update Gradle to 7.3+
8 Maven compiler plugin too old source release 17 requires target release 17 Update maven-compiler-plugin to 3.8.1+
9 Annotation processor failures Processors fail on Java 17 bytecode Update processors (MapStruct, Dagger, etc.)
10 Javadoc generation fails Stricter Javadoc linting in newer JDK Add -Xdoclint:none or fix Javadoc warnings
11 Date/time formatting differences Locale-dependent formatting changed Use explicit locale and format patterns
12 JaCoCo coverage failures Cannot instrument Java 17 classes Update JaCoCo to 0.8.7+

8. Testing Strategy

A thorough testing strategy is essential for a safe migration. Here is the approach that works for production systems:

8.1 Unit Tests

// Step 1: Run ALL existing unit tests on Java 17
// If tests fail, categorize the failures:
// - Compilation error -> fix source code
// - Runtime error -> update dependencies or add --add-opens
// - Behavior change -> investigate and update test expectations

// Step 2: Add tests for any workarounds
@Test
void testReflectiveAccessWorkaround() {
    // If you added --add-opens, test that the workaround works
    assertDoesNotThrow(() -> {
        // Code that requires reflective access
    });
}

// Step 3: Verify serialization compatibility
@Test
void testSerializationBackwardCompatibility() {
    // Deserialize objects that were serialized on Java 11
    byte[] java11Serialized = loadFromFile("test-data/user-java11.ser");
    User user = deserialize(java11Serialized);
    assertEquals("John", user.getName());
}

8.2 Integration Tests

Focus on the areas most likely to be affected:

  • Database layer — JDBC drivers, connection pools, ORM behavior
  • HTTP clients and servers — TLS handshakes, HTTP/2 behavior
  • Serialization — JSON, XML, and Java serialization compatibility
  • External service integrations — LDAP, SMTP, message queues

8.3 Performance Testing

Java 17 should be faster than 11 in most scenarios, but verify with your workload:

  • Run load tests with production-like traffic patterns
  • Compare GC pause times (G1GC improved significantly)
  • Monitor memory footprint — Java 17 generally uses less memory
  • Check startup time — CDS (Class Data Sharing) improvements help

9. Docker and CI/CD

9.1 Dockerfile Updates

# Before (Java 11)
FROM eclipse-temurin:11-jre-alpine
COPY target/app.jar /app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]

# After (Java 17)
FROM eclipse-temurin:17-jre-alpine
COPY target/app.jar /app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]

# Multi-stage build for smaller images
FROM eclipse-temurin:17-jdk-alpine AS build
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn clean package -DskipTests

FROM eclipse-temurin:17-jre-alpine
COPY --from=build /app/target/app.jar /app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app.jar"]

# With --add-opens if needed
ENTRYPOINT ["java", \
    "--add-opens", "java.base/java.lang=ALL-UNNAMED", \
    "-jar", "/app.jar"]

9.2 CI/CD Pipeline Updates

# GitHub Actions example
name: Build and Test
on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Java 17
        uses: actions/setup-java@v3
        with:
          distribution: 'temurin'
          java-version: '17'

      - name: Build and test
        run: mvn clean verify

      # Optional: Test on both Java 11 and 17 during migration
  compatibility-test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        java-version: [11, 17]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v3
        with:
          distribution: 'temurin'
          java-version: ${{ matrix.java-version }}
      - run: mvn clean verify

9.3 Container Image Comparison

Image Base OS Approximate Size Use Case
eclipse-temurin:17-jre-alpine Alpine Linux ~150 MB Production (smallest)
eclipse-temurin:17-jre-jammy Ubuntu 22.04 ~260 MB Production (better compatibility)
eclipse-temurin:17-jdk-alpine Alpine Linux ~330 MB CI/CD builds
amazoncorretto:17-alpine Alpine Linux ~200 MB AWS deployments

10. Best Practices

10.1 Incremental Adoption

Do not try to adopt every Java 17 feature at once. Follow this order:

  1. Week 1: Get the application compiling and all tests passing on Java 17 without using any new features
  2. Week 2-3: Start using text blocks in new code (lowest risk, highest readability gain)
  3. Week 3-4: Adopt pattern matching instanceof during regular code changes
  4. Month 2: Use switch expressions in new code
  5. Month 2-3: Create new DTOs as records
  6. Month 3+: Evaluate sealed classes for new type hierarchies

10.2 Migration Dos and Don’ts

Do Don’t
Update dependencies before changing JDK Change JDK and dependencies simultaneously
Run jdeps to find internal API usage Assume your code does not use internal APIs
Test on Java 17 in CI before production Deploy to production without thorough testing
Use --add-opens as a temporary fix Leave --add-opens permanently without a plan to remove
Adopt new features incrementally Rewrite everything in new syntax at once
Keep Java 11 builds running during transition Remove Java 11 CI jobs before migration is complete
Document all --add-opens flags with reasons Add --add-opens flags without understanding why
Benchmark before and after migration Assume Java 17 is faster for your specific workload

10.3 Feature Flags for New Syntax

When adopting new language features in a team, consider these guidelines:

  • Text blocks: Use everywhere immediately — purely cosmetic, zero risk
  • Pattern matching instanceof: Use in new code and during refactoring — simple and safe
  • Switch expressions: Use in new code — slightly higher learning curve for team members unfamiliar with arrow syntax
  • Records: Use for new DTOs and value objects — do not convert existing classes unless you have comprehensive tests
  • Sealed classes: Use for new type hierarchies — do not retroactively seal existing class hierarchies without careful analysis

The key principle is: new code uses new features, existing code is migrated only during regular maintenance. Do not create a separate “migration sprint” for syntax changes. Instead, adopt the boy scout rule — leave each file a little better than you found it.




Subscribe To Our Newsletter
You will receive our latest post and tutorial.
Thank you for subscribing!

required
required


Leave a Reply

Your email address will not be published. Required fields are marked *