| name | bump-java-11-to-17 |
| description | Migrate a Maven or Gradle project one Java LTS step from Java 11 to Java 17: it must compile under JDK 17, conserve every previously-passing test, and raise the effective compiler target to 17, using only standard tools (JDKs, Maven or Gradle, OpenRewrite recipes from Maven Central; no project-specific scripts). Use for an 11→17 Java LTS bump. |
Bump Java 11 → 17
Migrate the project in your working directory from Java 11 to Java 17. GOAL: it COMPILES and passes its TESTS under JDK 17, conserving every test that passed under JDK 11, with the effective compiler target raised to 17 (a project that merely compiles under 17 but still targets 11 is NOT a bump). Work autonomously until done.
Tools: standard only (JDKs 11 and 17, Maven or Gradle, OpenRewrite from Maven Central)
Three operations recur below; run each with the two JDKs, no project-specific scripts:
FIRST detect the build tool and use ONLY it for every operation: pom.xml present → Maven; otherwise (build.gradle/build.gradle.kts) → Gradle. NEVER introduce the other build system: do NOT create a pom.xml in a Gradle project (or a build.gradle in a Maven one). The build/test gate compiles whatever build file is present, so adding the wrong one silently breaks dependency resolution (deps declared in the project's real build tool show up as package … does not exist).
- compile under JDK N: Maven:
JAVA_HOME=<jdkN> mvn -B -ntp -DskipTests compile; Gradle: ./gradlew -Dorg.gradle.java.home=<jdkN> compileJava (also compileKotlin/compileTestJava).
- test under JDK N: Maven:
JAVA_HOME=<jdkN> mvn -B -ntp test; Gradle: ./gradlew -Dorg.gradle.java.home=<jdkN> test.
- apply the OpenRewrite program: write
rewrite.yml (below), then run it under JDK 11 with the Java-17 recipe artifacts:
- Maven:
JAVA_HOME=<jdk11> mvn -B -ntp -U -Denforcer.skip=true org.openrewrite.maven:rewrite-maven-plugin:6.40.0:run -Drewrite.configLocation=$(pwd)/rewrite.yml -Drewrite.activeRecipes=com.bjv.Bump -Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-migrate-java:3.35.0,org.openrewrite.recipe:rewrite-spring:6.31.0,org.openrewrite.recipe:rewrite-java-dependencies:1.55.0, the absolute configLocation ($(pwd)/rewrite.yml) is required, else multi-module submodules report Recipe(s) not found.
- Gradle: apply the OpenRewrite plugin through an init-script with
rewrite-migrate-java:3.35.0 / rewrite-spring:6.31.0 / rewrite-java-dependencies:1.55.0 on the rewrite configuration, then run rewriteRun.
- NEVER add the OpenRewrite plugin to a build file (
id('org.openrewrite') / apply plugin: 'org.openrewrite.rewrite' / a rewrite { } block / rewrite(...) deps): it PERSISTS into the gate build, which resolves plugins from the OFFLINE mirror and dies with Could not find org.openrewrite:rewrite-gradle-plugin:N (or could not resolve plugin artifact ... org.openrewrite.gradle.plugin) -> FAIL_build_post. Apply the recipe ONLY via the transient init-script above; if you already added the plugin, REMOVE it plus the rewrite{} block and rewrite(...) deps before the gate. (Proven: rr_11_48 kennedykori/jutils left rewrite-gradle-plugin:6.40.0 -> gate post 0; the clean bump, toolchain of(11)->of(17), is PASS 41/41.)
CRITICAL. NEVER time-box these builds. Cold Gradle/Maven runs download distributions + the OpenRewrite jars and take MINUTES; let them finish. An apply that was cut off means the recipe was NOT applied (tests may still pass at the OLD Java level, but the bytecode-target check FAILS). After applying, confirm BUILD SUCCESS and that the build files actually changed.
How to work (graded on a CORRECT migration that conserves tests: nothing else counts if a test is lost or the bytecode isn't really at 17)
- Prefer the off-the-shelf transforms: the unparametrized meta-recipes + setting the Java version to 17 + (only if needed) the pinned Gradle wrapper are the clean path. Use them first.
- On Gradle, land the target with a direct build-file edit; the reward follows the target, not the tool. Setting the target to 17 is a free hop-fixed intent (no penalty), and the combined gate credits the bump only once the effective bytecode target actually reaches 17.
UpgradeJavaVersion often leaves a Gradle toolchain (JavaLanguageVersion.of(N)) or sourceCompatibility/targetCompatibility untouched, and the rewrite-gradle-plugin init-script can fail to resolve offline or clash with the repo's Gradle version, so a run that loops on it while the target never lands earns nothing (it scores FAIL_target_not_bumped with edits 0). The dependable path to the reward is to set the target yourself: JavaLanguageVersion.of(<17) to of(17), plus any sourceCompatibility/targetCompatibility/options.release/JavaVersion.VERSION_* below 17, across the root and every module (allprojects/subprojects). Keep OpenRewrite for the heavier transforms (Spring, javax->jakarta, dependency floors); if rewriteRun will not resolve, the direct edit has already secured the target. (On Maven the rewrite-maven-plugin route is reliable; the direct edit is <release>17</release> or maven.compiler.release.)
- Treat every manual edit and every project-specific dependency/plugin change as a LIABILITY. Make the FEWEST changes that genuinely work; reach for a hand edit or a project-specific recipe only when a real wall demands it.
- Never touch or weaken test code (see FORBIDDEN).
Proactive step: Gradle wrapper floor (run BEFORE the first JDK-17 build; gated on a structural signal, not an error)
If the build tool is Gradle, read the wrapper version in gradle/wrapper/gradle-wrapper.properties (the gradle-<N>-bin.zip in distributionUrl). If it is below 7.3 (the JDK-17 floor), bump distributionUrl to gradle-7.6-bin.zip (the pinned value) before applying the recipe / building under JDK 17. Never wait for the error. Gradle itself runs on the build JDK, and Gradle < 7.3 cannot run on JDK 17: it dies during configuration with Unsupported class file major version 61 while compiling settings.gradle/build.gradle, before any project code is touched. That v61 text is the SAME signature JaCoCo and old Spring/ASM emit, so reactive error-matching cannot reliably attribute it to the wrapper, but the structural trigger (wrapper version < 7.3) is unambiguous, which is exactly why this is proactive (same test as the Lombok proactive step). Apply via org.openrewrite.gradle.UpdateGradleWrapper {version: "7.6"} or by editing distributionUrl directly (never a file:// path). Counts as a free hop-fixed intent, like setting the target. The reactive Troubleshooting row below stays as a back-stop.
Proactive step: env-mutating test lib needs --add-opens (run BEFORE the first JDK-17 test; gated on a dependency, not an error)
If a test dependency mutates the process environment by deep reflection, JDK 17 strong encapsulation blocks it at runtime and EVERY test using it fails with InaccessibleObjectException (the test class fails as initializationError, so the loss is large and silent at compile time). This is a reliable STRUCTURAL trigger, not a guess: it fires whenever the project depends on org.junit-pioneer:junit-pioneer (@SetEnvironmentVariable/@ClearEnvironmentVariable, whose EnvironmentVariableUtils reflects into java.lang.ProcessEnvironment), com.github.stefanbirkner:system-lambda or ...:system-rules (withEnvironmentVariable/EnvironmentVariables), or a project util that reflects into ProcessEnvironment / Collections$UnmodifiableMap. When you see any of these in the test classpath, add the env-map opens to every test fork (prefer root subprojects {}, see below), BEFORE the first JDK-17 test run. Never wait for the error. The env map lives in java.lang.ProcessEnvironment + java.util.Collections$UnmodifiableMap, so the minimal set is exactly --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED. Gradle: preferably in root subprojects { tasks.withType<Test>().configureEach { jvmArgs("--add-opens=java.base/java.util=ALL-UNNAMED", "--add-opens=java.base/java.lang=ALL-UNNAMED") } } to cover ALL modules, a per-module placement UNDER-COVERS: env-var tests in sibling modules stay broken (proven: jwuang/my-robocode loses 2 of 47 with a bot-api/java-only placement, 0 with root subprojects). Maven: append the two tokens to maven-surefire-plugin <argLine> (preserve any existing @{argLine}). This whole arg block = ONE edit; it counts like the wrapper floor. The reactive row below stays as a back-stop for opens the error names that this proactive set does not cover.
Proactive step: Gradle target pin the recipe leaves at 11 (verify the bump actually landed; gated on a structural signal, not an error)
On Gradle projects the UpgradeJavaVersion recipe frequently does NOT touch the build file's target pin, a java { toolchain { languageVersion = JavaLanguageVersion.of(11) } } block (Groovy or Kotlin DSL), a tasks.compileJava { options.release = 11 } / options.release.set(11), a sourceCompatibility/targetCompatibility = JavaVersion.VERSION_11, or, on a Kotlin project, a kotlin { jvmToolchain { languageVersion.set(JavaLanguageVersion.of(11)) } } plus a kotlinOptions.jvmTarget = "11". The build then compiles cleanly but to the OLD bytecode (effective target stays 11), so there is NO error to react to. It silently scores FAIL_target_not_bumped. So after applying the recipe, grep every build file (build.gradle, build.gradle.kts) for JavaLanguageVersion.of(, JavaVersion.VERSION_, sourceCompatibility/targetCompatibility, options.release, and (Kotlin) jvmTarget: if any still names a version < 17, hand-edit it to 17 (JavaLanguageVersion.of(17), JavaVersion.VERSION_17, options.release = 17, jvmTarget = "17") before the JDK-17 build, and bump every pin you find, a project can have two (e.g. a toolchain block AND an options.release, or a jvmToolchain block AND a kotlinOptions.jvmTarget) and missing one leaves the bytecode at 11. This is proactive precisely because the failure is silent, the structural trigger (a target pin left < 17 in a build file) is unambiguous. For a Kotlin project the Java toolchain alone is usually enough (the Kotlin plugin derives jvmTarget from it), but if the build sets an explicit kotlinOptions.jvmTarget/jvmTarget it OVERRIDES the toolchain. Bump that too. Counts as a free hop-fixed intent, like setting the target. (Proven: viktoriia-sokolenko/exploding-kittens of(11) + options.release = 11, masson-rafael/R4.02_TestsPokeBagarre of(11), dbottillo/NotionAssistantIntegration of(11) + jvmTarget = "11", all three FAIL_target_not_bumped target 11 with 0 edits, all three -> VERDICT PASS target 17 reward 1.0 after hand-bumping the pins.)
START HERE: write rewrite.yml, then apply it
type: specs.openrewrite.org/v1beta/recipe
name: com.bjv.Bump
recipeList:
- org.openrewrite.java.migrate.UpgradePluginsForJava17
- org.openrewrite.java.migrate.UpgradeBuildToJava17
- org.openrewrite.java.migrate.UpgradeJavaVersion:
version: 17
Then compile under JDK 17. If it compiles, test under JDK 17. If tests pass and none are lost, you are done.
Reflect loop: if compile or test under 17 fails, read the error, fix the FIRST wall, re-run (no iteration limit)
JDK-17 class-file version is 61: a tool that reads bytecode via ASM must be new enough for v61.
- Lombok breaks javac:
NoSuchFieldError: JCTree$JCImport.qualid / Could not initialize class lombok.javac.Javac. Floor Lombok to 1.18.30 via org.openrewrite.java.dependencies.UpgradeDependencyVersion {org.projectlombok, lombok, 1.18.30} (and its annotationProcessor).
- Test fork strong-encapsulation:
InaccessibleObjectException / module {A} does not "opens {pkg}" to unnamed module (deep reflection via setAccessible). Hand-edit the test fork args (Maven maven-surefire-plugin <argLine>, Gradle tasks.test { jvmArgs(...) }) adding the opens the error names, one token each joined with =, e.g. --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED (also commonly java.lang.reflect, java.text, java.io, java.nio, java.time, sun.nio.ch). Read the cause chain for the exact package: an env-var test (junit-pioneer EnvironmentVariableUtils / system-lambda withEnvironmentVariable, error reads InaccessibleObjectException at AccessibleObject.java: wrapped in ExtensionConfigurationException at EnvironmentVariableUtils.java) is the process-env map → the fix is exactly --add-opens=java.base/java.util=ALL-UNNAMED + --add-opens=java.base/java.lang=ALL-UNNAMED (env lives in java.lang.ProcessEnvironment + java.util.Collections$UnmodifiableMap); the proactive step above should already have added these. Preserve any existing argLine (e.g. JaCoCo's @{argLine}). This whole arg block = ONE edit.
- Spring component-scan fails:
Unsupported class file major version 61, OR a bare IllegalArgumentException at ClassReader wrapped in BeanDefinitionStoreException → SimpleMetadataReader (no "major version" text). Spring < 5.3 (SB 2.0-2.4) bundles an ASM that can't read v61 → add org.openrewrite.java.spring.boot2.UpgradeSpringBoot_2_7. Do NOT hand-pick an intermediate < SB 2.5. It fails with the IDENTICAL error and looks like "the bump didn't help".
- Mockito
cannot mock/Cannot define class using reflection/sun.misc.Unsafe.defineClass: its shaded ByteBuddy is too old. Bump Mockito (≥ 5.x) or force net.bytebuddy:byte-buddy(:agent) to 1.14.12 (a plain bump is often overridden by the Spring BOM. Force it). UpgradeDependencyVersion.
- JaCoCo
Unsupported class file major version 61: floor jacoco to 0.8.12 (Maven jacoco-maven-plugin, Gradle jacoco { toolVersion }).
- Pure-Groovy Gradle project pinned to Groovy 2.5 (
id 'groovy', .groovy main sources, often a Hubitat/hubitat_ci app): Groovy 2.5 CANNOT run on JDK 17, :compileGroovy dies with NoClassDefFoundError: Could not initialize class org.codehaus.groovy.vmplugin.v7.Java7 (its vmplugin tops out at v7/v9). Reaching Java-17 bytecode REQUIRES Groovy ≥ 3.0.9 + Spock 2.x-groovy-3.0 (useJUnitPlatform()). Two gotchas after bumping: (1) Groovy 3 split the fat groovy-all jar. Switch to modular org.codehaus.groovy:groovy:3.0.x and add EVERY non-core module the sources import (groovy-json for JsonSlurper/JsonOutput, groovy-xml, etc.), else unable to resolve class groovy.json.JsonSlurper; add a configurations.all { resolutionStrategy.eachDependency { if (it.requested.group=='org.codehaus.groovy') it.useVersion '3.0.x' } } to evict transitive 2.5. (2) On JDK 17 the test harness reflects into JDK internals → IllegalAccessError ... sun.util.calendar.ZoneInfo, fixed by the canonical test-task --add-opens set. BUT if a test dependency is itself a Groovy-2.5-only library with no Groovy-3 release (e.g. me.biocomp.hubitat_ci:0.17), Groovy 3 breaks it at runtime with NoSuchMethodError: org.codehaus.groovy.runtime.DefaultGroovyMethods.plus(String,String) (a removed-in-3.0 ABI) on every test that compiles a script through it, and there is NO target-JDK that satisfies both. That is the abandoned-dependency bail below: you cannot get target 17 AND conserve the tests; report it honestly, do not force. (Proven on ljbotero/hubitat-flair-vents 11→17: clean clone bytecode is major 55; agent removing the compileGroovy{ targetCompatibility } blocks let Groovy 2.5 default-emit Java-7 major-51, the original FAIL_target_not_bumped target 7; the real bump compiles to target 17 but loses 106/108 tests to hubitat_ci's Groovy-3 ABI break.)
- Spock/Groovy TEST sources on JDK 17 (main is plain Java/Kotlin, but
src/test is .groovy Spock specs, id 'groovy' only pulls in test specs): the :compileTestGroovy (or :compileGroovy) task dies with NoClassDefFoundError: Could not initialize class org.codehaus.groovy.vmplugin.v7.Java7 / Could not initialize class org.codehaus.groovy.reflection.ReflectionCache / a bare GroovyBugError, the bundled Groovy 2.x cannot run on JDK 17 (its vmplugin tops out at v7/v9). This is the TEST-only cousin of the pure-Groovy main row above (cross-ref it for the groovy-all split + the abandoned-dependency bail), here the production bytecode is already fine, so this is purely a test-compile wall. Fix = bump Spock to 2.3-groovy-4.0 (or 2.3-groovy-3.0) + Groovy to 4.0.x (org.apache.groovy:groovy) or 3.0.x (org.codehaus.groovy:groovy): pick the pair that resolves from the mirror. Switch any fat groovy-all to the modular groovy artifact the specs import, and add test { useJUnitPlatform() } (Spock 2.x runs on the JUnit Platform, not the old JUnit-4 runner, without it :test finds 0 specs). Spock 2.x ALSO needs the JaCoCo floor (the agent attaches to the test JVM and throws Unsupported class file major version 61 / IllegalClassFormatException while instrumenting JDK-17 CLDR classes, silently killing date/locale-touching specs. Bump jacoco { toolVersion } per the JaCoCo row). FINAL gotcha that masquerades as a test LOSS: Spock 1.x named unrolled iterations feature[0], but Spock 2.x defaults to feature [param: value, ..., #N], the embedded #N breaks the scorer's class#method split so EVERY parametric iteration looks renamed -> FAIL_test_conservation lost <K> even though the build is GREEN. Restore the 1.x naming with a classpath-root config file src/test/resources/SpockConfig.groovy containing unroll { defaultPattern '#featureName[#iterationIndex]' } (a config file, NOT a test source - it does not change the test set). NOTE: --rerun-tasks (now baked into the harness jvmjob) is what makes such a masked test-COMPILE failure visible at all - without it every task goes UP-TO-DATE under JDK 17 and the specs never recompile/run, scoring a deceptive post 0. (Proven on qaware/cloud-cost-fitness 11->17: Spock 1.3-groovy-2.5 + org.codehaus.groovy:groovy-all:2.5.4 + jacoco 0.8.6 -> after Spock 2.3-groovy-4.0 + org.apache.groovy:groovy:4.0.21 + useJUnitPlatform() + jacoco 0.8.8 + the SpockConfig.groovy unroll pattern: VERDICT PASS target 17 reward 1.0, all 47 tests conserved.)
- google-java-format / fmt-maven-plugin / Spotless crash under JDK 16+: at the format goal (
com.coveo:fmt-maven-plugin:*:format ... ArrayIndexOutOfBoundsException: Index 1 out of bounds for length 0, or ... format failed: An API incompatibility was encountered ... java.lang.IllegalAccessError, or Spotless/google-java-format IndexOutOfBounds/ClassCastException naming com.sun.tools.javac.*). google-java-format reflects into com.sun.tools.javac.*, which JDK 16 strong-encapsulated. A plugin/version floor alone does NOT fix it: proven on ONSdigital/ssdc-rm-exception-manager that fmt-maven-plugin 2.13 (with google-java-format 1.13.0 AND a forced override to 1.15.0) BOTH still throw IllegalAccessError: null under JDK 17. The real fix is the javac add-exports/opens the formatter needs, given to the Maven build JVM (plugins run in it) via a .mvn/jvm.config file (Gradle: the equivalent jvmArgs on the format/check task). Write .mvn/jvm.config with: --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED --add-opens jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED --add-opens jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED (space- or newline-separated, one file, ONE edit). This is .mvn/jvm.config, NOT surefire <argLine>, the formatter runs at the format goal in the Maven JVM, not in a test fork. (Proven: ssdc-rm-exception-manager fmt:2.13:format IllegalAccessError FAIL_build_post post 0 lost 34 reward 0.0 -> with .mvn/jvm.config VERDICT PASS reward 1.0, 34 tests conserved.)
- maven-plugin-plugin descriptor double-registers
help (only on a project that itself builds a Maven plugin, <packaging>maven-plugin</packaging>): Error extracting plugin descriptor: 'Goal: help already exists in the plugin descriptor for prefix: <X>' / Existing implementation is: ...HelpMojo / Conflicting implementation is: ...HelpMojo at maven-plugin-plugin:<v>:descriptor (default-descriptor). The pom configures BOTH the helpmojo goal (an explicit <execution> with <goal>helpmojo</goal>, which GENERATES a HelpMojo.java for goal help into target/generated-sources/plugin) AND the auto-bound descriptor goal, which under JDK 17 ALSO scans that generated source and registers a SECOND help goal → collision. Tell from the log: under JDK 11 the java-annotations extractor finds N descriptors and BUILD SUCCESS; under JDK 17 it finds N+1 (the extra generated HelpMojo) and aborts. Bumping maven-plugin-plugin does NOT fix it: 3.7.0/3.8.2 emit the identical error; 3.9.0 only renames the generated package (...autojdk_maven_plugin.HelpMojo) but still collides on goal help. Fix = DELETE the redundant helpmojo execution from the pom (the <execution> whose <goal>helpmojo</goal>); the descriptor goal already supplies help. ONE hand edit, no plugin bump needed. (Proven on causalnet/autojdk-maven-plugin: FAIL_build_post → VERDICT PASS reward 1.0, 53 tests conserved.)
- Embedded compiler
target level should be in '1.1'...'N' (AspectJ ajc / Eclipse ECJ doing the compiling): bump it (aspectjtools/aspectjrt/plugin ≥ 1.9.8 for 17), not the JDK flags.
- Gradle wrapper below the JDK-17 floor (7.3): bump via
org.openrewrite.gradle.UpdateGradleWrapper {version: "7.6"} (the pinned value), or set gradle-wrapper.properties by hand. Two wrapper-too-old signatures: Unsupported class file major version N while Gradle CONFIGURES (in _BuildScript_), and Could not determine java version from '17.0.x'. NEVER point distributionUrl at a file:// path.
- Gradle + Kotlin:
Inconsistent JVM-target compatibility … compileJava (17) and compileKotlin (M) → set kotlin { jvmToolchain(17) }, not just the Java toolchain.
cannot find symbol: WebSecurityConfigurerAdapter etc. needing Spring Security 6 → that requires SB 3 (a 17→21-era jump); on the 11→17 hop, prefer staying on SB 2.7.
- Multi-module: a JDK bump is per-BUILD, not per-module.
Dependency resolution is looking for a library compatible with JVM runtime version N, but 'project :X' is only compatible with M = you set the target in some modules but not others. Set the SAME target in every module (root allprojects/subprojects).
Cannot find a Java installation … matching {languageVersion=N} (often a foojay resolver timeout, no network): point Gradle at the installed JDKs -Porg.gradle.java.installations.paths=<jdk11>,<jdk17> and DROP any vendor/implementation pins from toolchain{}, languageVersion alone is enough.
Entry <path> is a duplicate but no duplicate handling strategy has been set (Gradle 7 made this a hard error): tasks.withType(Copy).configureEach { duplicatesStrategy = DuplicatesStrategy.EXCLUDE }.
- A removed/changed JDK API in the project's OWN source: hand-edit minimally.
General discipline (these stop you chasing non-problems)
- VERIFY THE TARGET LANDED: a clean JDK-17 build is NOT proof of a real bump, Gradle
JavaLanguageVersion.of(N) toolchains, options.release, JavaVersion.VERSION_*, Kotlin jvmTarget, and soft-pinned Maven source/target/release can leave bytecode at 11 with zero errors. After the build, confirm no build file still pins a version < 17 (see the proactive target-pin step). If nothing forced a target edit, the recipe under-applied. Fix the version declaration(s) by hand.
- EDIT HYGIENE: after EVERY build-file edit, BEFORE rebuilding, validate it,
./gradlew help -q (Maven mvn -q validate). If that fails naming the file you just edited, YOUR edit broke the script: fix/revert it, do not chase it as a migration error. Make minimal, validated edits.
- NOT YOUR TESTS: tests needing Docker/Testcontainers/a database/Selenium/the network were already failing in the baseline. They are NOT in your conserve-set. Don't try to fix them and don't count them as regressions; only tests that PASSED under 11 must still pass.
- OOM during tests is usually DOWNSTREAM of an earlier real error. Fix the FIRST error; only raise the test
-Xmx if it's genuinely heap.
- Stale generated classes (
ClassAlreadyExistsException, jsonschema2pojo): ./gradlew clean / rm -rf target, then re-run.
- Use
--add-opens for deep reflection (setAccessible), --add-exports only for compile/link access to a non-exported API. Do NOT use --illegal-access=permit, JEP 403 removed it in JDK 17 (no-op/error). Canonical opens set if you can't tell which: --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 --add-opens=java.base/java.text=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.nio=ALL-UNNAMED --add-opens=java.base/java.time=ALL-UNNAMED --add-opens=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.desktop/java.awt.font=ALL-UNNAMED --add-opens=java.management/java.lang.management=ALL-UNNAMED.
When to bail (report honestly instead of forcing)
- A Spring-Boot-1.x app whose custom code calls SB-2-removed APIs (won't compile on SB2).
- A semantic JDK change in the project's own code (its annotation processor crashes /
java.beans.Introspector-derived behavior changed). Confirm with a pristine-checkout probe failing the same way under JDK 17.
- An abandoned dependency with no JDK-17 release.
- Native (CMake/JNI) modules that don't build in a plain JVM.
Absolutely forbidden (cheating: fails the run)
Do NOT delete/skip/disable/weaken any test or assertion; no -DskipTests/<skip>true; do NOT delete test files; do NOT touch test code to make it pass. Conservation is checked independently.
When the project passes its tests under JDK 17 with no tests lost and the effective target is 17, say you are done and summarize what you changed.