name: bump-java-8-to-11
description: Migrate a Maven or Gradle project one Java LTS step from Java 8 to Java 11: it must compile under JDK 11, conserve every previously-passing test, and raise the effective compiler target to 11, using only standard tools (JDKs, Maven or Gradle, OpenRewrite recipes from Maven Central; no project-specific scripts). Use for an 8→11 Java LTS bump.
Bump Java 8 → 11
Migrate the project in your working directory from Java 8 to Java 11. GOAL: it COMPILES and passes its TESTS under JDK 11, conserving every test that passed under JDK 8, with the effective compiler target raised to 11 (a project that merely compiles under 11 but still targets 8 is NOT a bump). Work autonomously until done.
Tools: standard only (JDKs 8 and 11, 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 8 with the Java-11 recipe artifacts:
- Maven:
JAVA_HOME=<jdk8> 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.
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 11)
- Prefer the off-the-shelf transforms: the unparametrized meta-recipe + setting the Java version to 11 + (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 11 is a free hop-fixed intent (no penalty), and the combined gate credits the bump only once the effective bytecode target actually reaches 11.
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(<11) to of(11), plus any sourceCompatibility/targetCompatibility/options.release/JavaVersion.VERSION_* below 11, 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>11</release> or maven.compiler.release.)
- Treat every manual edit and every project-specific dependency/plugin change as a LIABILITY, more code to get right, more risk. 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: verify the Java target actually landed (Gradle/Maven pin the recipe leaves at 8; silent FAIL_target_not_bumped)
UpgradeJavaVersion frequently does NOT touch the build file's target pin, so the build compiles cleanly to OLD (Java-8, major 52) bytecode with NO error and silently scores FAIL_target_not_bumped. After applying the recipe, grep every build file for a pin still < 11 and hand-bump it BEFORE the JDK-11 build. Use a SIMPLE grep (do NOT improvise a find -exec, a malformed one wastes turns and can get you stuck): grep -rnE 'JavaLanguageVersion.of|VERSION_1[._]?8|sourceCompat|targetCompat|options.release|<source>|<target>|<release>|maven.compiler|jvmTarget' . 2>/dev/null. Bump EVERY pin you find (a project can have two): Gradle java { toolchain { languageVersion = JavaLanguageVersion.of(8) } } → of(11); sourceCompatibility/targetCompatibility = JavaVersion.VERSION_1_8 (or '1.8') → VERSION_11/'11'; options.release = 8 → 11; Maven <source>1.8/<target>1.8/<release>8 / maven.compiler.source → 11; Kotlin jvmTarget "1.8" → "11". This applies to PLAIN-Java projects too (not just Kotlin). Counts as a free hop-fixed intent. (Proven: jmini/asciidoctorj-dynamic-include, plain-Java, sole pin java { toolchain { JavaLanguageVersion.of(8) } } the recipe left untouched: of(8)→of(11) flipped FAIL_target_not_bumped target 8 → VERDICT PASS target 11, 35 tests, 0 edits.)
START HERE: write rewrite.yml, then apply it
type: specs.openrewrite.org/v1beta/recipe
name: com.bjv.Bump
recipeList:
- org.openrewrite.java.migrate.Java8toJava11
- org.openrewrite.java.migrate.UpgradeJavaVersion:
version: 11
Then compile under JDK 11. If it compiles, test under JDK 11. If tests pass and none are lost, you are done.
Reflect loop: if compile or test under 11 fails, read the error, fix the FIRST wall, re-run (no iteration limit)
Keep going until it passes or you have exhausted real options.
Wall → fix:
-
package javax.xml.bind/javax.annotation.Generated/JAXBException (EE modules removed in JDK 11): the Java8toJava11 recipe already re-adds these (jaxb-api/jaxb-runtime/javax.activation/javax.annotation-api 2.3.1) and floors surefire to 2.22.2, trust it. ONLY if it persists during ANNOTATION PROCESSING (<annotationProcessorPaths>): add jaxb-api + javax.annotation-api as <path> entries there too. Remove any leftover --add-modules java.xml.bind/java.se.ee flag.
-
Obsolete/incompatible build plugin (animal-sniffer "requires ASM7", or one enforcing a Java-8 floor): add org.openrewrite.maven.RemovePlugin {groupId, artifactId} to rewrite.yml, apply again.
-
Dependency too old for JDK 11 (lombok < 1.18.20, byte-buddy < 1.10, mockito < 3): add org.openrewrite.java.dependencies.UpgradeDependencyVersion {groupId, artifactId, newVersion}.
-
Gradle wrapper: Gradle 5.0+ ALREADY runs on JDK 11. Do NOT bump just for the Java version. Only bump if the apply fails to LOAD on the current wrapper (needs ~Gradle 6.0+). When you must, target 6.9 (NOT 7.x) via the org.openrewrite.gradle.UpdateGradleWrapper {version: "6.9"} recipe, or set gradle-wrapper.properties to gradle-6.9 by hand. NEVER jump to Gradle 7.x. It REMOVED compile/testCompile (breaks old build files: Could not find method compile()). NEVER point distributionUrl at a file:// path. If a build truly needs Gradle 7+, also rewrite compile→implementation/testCompile→testImplementation.
-
Spring Boot 1.x can't run on 11: add org.openrewrite.java.spring.boot2.UpgradeSpringBoot_2_7.
-
RUNTIME ArrayIndexOutOfBoundsException: Index 1 out of bounds for length 1 from a <clinit> (Jadira etc. parsing java.version the old 1.x way): bump the lib (Jadira → 7.0.0.CR1); no JDK-11 release → bail.
-
package <internal> is not visible (project uses java.awt.peer / sun.security.* / sun.misc.): no clean recipe, hand-edit the build file to add --add-exports=<module>/<package>=ALL-UNNAMED to the compiler args (java.awt.peer→java.desktop, sun.security./sun.misc.*→java.base).
-
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 = set the SAME target in every module (root allprojects/subprojects).
-
A removed/changed JDK API in the project's OWN source: hand-edit the source minimally.
-
Kotlin project, Could not find org.jetbrains.kotlin:kotlin-stdlib-jre8:<newver> (or -jre7): the -jre8/-jre7 stdlib artifacts were RENAMED to -jdk8/-jdk7 in Kotlin 1.2 and no longer publish for newer Kotlin. If you (or a recipe) raised kotlinVersion to >= 1.2, rename kotlin-stdlib-jre8->kotlin-stdlib-jdk8 and kotlin-stdlib-jre7->kotlin-stdlib-jdk7 in the build file.
-
Unresolved reference: fastinfoset / import com.sun.xml.internal.fastinfoset... (project source imports a com.sun.xml.internal.* class, the JDK-bundled FastInfoset/JAXB internals, present in JDK 8 but REMOVED in 11): rewrite the import to the external package (com.sun.xml.internal.fastinfoset -> com.sun.xml.fastinfoset) and add the external dep com.sun.xml.fastinfoset:FastInfoset:1.2.18 (Gradle implementation/compile, Maven <dependency>). Same shape as any removed-internal-JDK-API wall.
-
Kotlin RUNTIME NoClassDefFoundError: kotlin/reflect/KotlinReflectionInternalError (often surfaces as Could not initialize class <X>Kt when a Kotlin/Jackson factory inits): a DEPENDENCY jar was compiled against an old Kotlin (<= 1.1) and calls the top-level kotlin.reflect.KotlinReflectionInternalError, which was MOVED to kotlin.reflect.jvm.internal and deleted from the top level in newer kotlin-reflect, so it vanishes once kotlinVersion is raised. Find the stale lib (check each Kotlin dep's POM for kotlin-reflect 1.1.x: e.g. com.natpryce:konfig:1.6.1.0 -> bump to 1.6.10.0; an OLD jackson-module-kotlin like 2.8.x -> bump the whole jackson stack to ~2.12.7) and bump it to a build compiled against Kotlin >= 1.2.
-
Kotlin target not raised (FAIL ... target / bytecode still 8): the Java sourceCompatibility/UpgradeJavaVersion recipe does NOT touch Kotlin. Raise kotlinOptions.jvmTarget (every compileKotlin/compileTestKotlin block) from "1.8" to "11" (or set kotlin { jvmToolchain(11) }). Kotlin 1.4+ accepts jvmTarget "11". Cover ALL the pin forms: kotlinOptions.jvmTarget = "1.8" -> "11"; the typed compilerOptions { jvmTarget.set(JvmTarget.JVM_1_8) } / jvmTarget = JvmTarget.JVM_1_8 -> JvmTarget.JVM_11; and kotlin { jvmToolchain(8) } -> (11). Pure-Kotlin Gradle projects often have NO kotlinOptions/jvmTarget at all, the only pin is the Java toolchain java { toolchain { languageVersion.set(JavaLanguageVersion.of(8)) } }, from which the Kotlin plugin DERIVES its jvmTarget; raise that JavaLanguageVersion.of(8) -> of(11) and the Kotlin bytecode follows (no separate jvmTarget needed, so no "Inconsistent JVM-target" error). If BOTH a Java toolchain AND an explicit Kotlin jvmTarget are present, raise both to 11 to match or you hit "Inconsistent JVM-target compatibility". (Verified: featurevisor/featurevisor-kotlin, Gradle 8.2.1. Sole pin was JavaLanguageVersion.of(8), no jvmTarget block; of(11) -> bytecode major 55, 6 tests conserved.)
-
maven-surefire-plugin:<2.22 ... NullPointerException (or ArrayIndexOutOfBoundsException) at the test goal: surefire < 2.22 parses java.version the old 1.x way and NPEs/AIOOBs the moment it runs under JDK 9+. The Java8toJava11 recipe floors surefire to 2.22.2, but it does NOT override a version the project HARD-PINS in <pluginManagement> (e.g. <artifactId>maven-surefire-plugin</artifactId><version>2.20.1</version>), hand-bump that pinned <version> to 2.22.2 (a JDK-11-capable, offline-resolvable floor). Common in multi-module reactors: an old surefire pinned in the root POM NPEs the FIRST module, aborting the whole reactor and wiping every test (post 0 lost N), fixing the pin lets the reactor complete. (Verified: jaxxy-rs/jaxxy 8->11, root POM pinned surefire 2.20.1; bumping to 2.22.2 cleared the jaxxy-util NPE.)
-
Could not resolve dependencies ... org.openjfx:javafx.base:jar:11.0.0-SNAPSHOT was not found: an old hibernate-validator (pulled by spring-boot 1.x/2.0.x BOMs, e.g. 6.0.11.Final) has a jdk11+ profile (<jdk>[9,)</jdk>/[11,)) that adds org.openjfx:javafx.base:${java.version}, under JDK 11 that resolves to the bogus javafx.base:11.0.0-SNAPSHOT (NOTE the DOT, not the real javafx-base artifact), which is not in any mirror, so the build cannot resolve deps. Fix: override hibernate-validator to >= 6.2.0.Final (it switched to the real org.openjfx:javafx-base at provided scope). If the module imports a BOM (spring-boot-starter-parent) that re-pins the old version, declare the hibernate-validator <version> FIRST in that module's own <dependencyManagement> so it wins over the imported BOM. (Verified: jaxxy-rs/jaxxy jaxxy-example, overriding 6.0.11.Final -> 6.2.0.Final removed the phantom javafx.base node.)
General discipline (these stop you chasing non-problems)
- 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, NOT in your conserve-set. Only tests that PASSED under 8 must still pass.
- OOM during tests is usually DOWNSTREAM of an earlier real error. Fix the FIRST error first.
- Stale generated classes (
ClassAlreadyExistsException, jsonschema2pojo): ./gradlew clean / rm -rf target, re-run.
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 of the originally-passing tests is checked independently.
When the project passes its tests under JDK 11 with no tests lost and the effective target is 11, say you are done and summarize what you changed.
-
Apply the recipe ONLY via bapply (transient init-script). NEVER add the OpenRewrite plugin to a build file. If you add id('org.openrewrite') / apply plugin: 'org.openrewrite.rewrite' / a rewrite { } block / rewrite(...) deps to build.gradle to apply the recipe. It PERSISTS into the gate build, which resolves plugins from the OFFLINE mirror and dies: Plugin Repositories (could not resolve plugin artifact 'org.openrewrite:org.openrewrite.gradle.plugin:N') -> FAIL_build_post. bapply applies the recipe transiently and leaves no residue; if you already added it, REMOVE the plugin + rewrite{} block + rewrite(...) deps before the gate. (rr_8_93 aartiPl/tablevis: agent left id('org.openrewrite') version '6.40.0' -> gate couldn't resolve it offline.)
-
Gradle wrapper jar MISSING (./gradlew -> Error: Could not find or load main class org.gradle.wrapper.GradleWrapperMain / ClassNotFoundException): the repo committed gradlew but not gradle/wrapper/gradle-wrapper.jar (often the .properties too) - the wrapper CANNOT bootstrap. The baseline usually passed via SYSTEM gradle because gradlew was non-executable (the harness tool() uses ./gradlew only if it is executable). So do NOT chmod +x gradlew here - the wrapper-floor chmod forces the harness onto the broken wrapper and fails the gate; leave gradlew non-executable so it falls back to system gradle, OR regenerate the wrapper (gradle wrapper --gradle-version <pin> to create the jar). Only chmod +x gradlew when gradle/wrapper/gradle-wrapper.jar actually exists. (rr_8_102 akosogova/chessRating: empty gradle/wrapper/; leaving gradlew non-exec + bumping the toolchain of(8)->of(11) -> PASS 17/17 target 11.)