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.
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 |
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:
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
| 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 |
// 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
Follow this checklist in order. Each step builds on the previous one.
Before changing any JDK version, check that all your dependencies support Java 17. This is the single most important step.
org.apache.maven.plugins maven-compiler-plugin 3.11.0 17 org.apache.maven.plugins maven-surefire-plugin 3.1.2
| 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+ |
# 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
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
# 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
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
After compilation succeeds, run your entire test suite. Pay special attention to:
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
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
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
// 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))
}
}
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:
javax.* imports with jakarta.*WebSecurityConfigurerAdapter with SecurityFilterChain beanHibernate 6 is the default JPA provider in Spring Boot 3.x. Key changes:
javax.persistence to jakarta.persistence namespace| 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+) |
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+ |
A thorough testing strategy is essential for a safe migration. Here is the approach that works for production systems:
// 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());
}
Focus on the areas most likely to be affected:
Java 17 should be faster than 11 in most scenarios, but verify with your workload:
# 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"]
# 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
| 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 |
Do not try to adopt every Java 17 feature at once. Follow this order:
instanceof during regular code changes| 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 |
When adopting new language features in a team, consider these guidelines:
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.