Chapter 23Lesson 04~240 minutes

Java Toolchains, Compiler Configuration, Annotation Processing, Kotlin/JVM, and Cross-Version Builds: Diagnostics, Failure Modes, Security, and Performance

Diagnose runtime/toolchain confusion, unsupported compiler targets, processor leakage, unavailable toolchains, Kotlin/Java target drift, and older-runtime failures from preserved evidence.

DiagnosticsUnsupportedClassVersionErrorProcessor pathVendor selectionTarget drift

Learning objectives

  • Use an evidence-preserving diagnostic sequence for JVM/toolchain/compiler failures.
  • Differentiate a Gradle runtime JVM problem from a missing compiler/test toolchain.
  • Detect processor leakage and generated-source failures from the correct classpath/output state.
  • Interpret Java/Kotlin target incompatibility and older-runtime class-version failures without suppressing warnings.
  • Correct the narrowest state and verify with a controlled rebuild.

1. Diagnostic sequence

  1. Preserve concise evidence: failing task, exception, requested toolchain, test runtime, and relevant logs.
  2. Confirm Wrapper/Gradle/runtime JVM: ./gradlew --version.
  3. Inspect declared toolchain/target configuration: Java/Kotlin build blocks and options.release.
  4. Inspect available toolchains: javaToolchains; do not assume JAVA_HOME tells the whole story.
  5. Inspect compile/processor/runtime graphs: especially annotationProcessor versus runtimeClasspath.
  6. Inspect generated files and bytecode: generated-source directory, javap -verbose, test report.
  7. Apply the least destructive correction: fix the requested toolchain/target/dependency edge, not global caches.
  8. Rebuild in the isolated lab and re-run the affected matrix cell.

2. Failure: Gradle runtime is compatible, compiler toolchain is not

Suppose ./gradlew --version succeeds on JDK 21 but :library:compileJava says no matching Java 17/21 installation is available. The Gradle runtime requirement has already been satisfied; the failure belongs to the task toolchain selection layer.

./gradlew --version
./gradlew -q javaToolchains
./gradlew :library:compileJava --info --stacktrace

Do not “fix” this by changing JAVA_HOME blindly or deleting ~/.gradle. Either make the requested JDK available, correct an erroneous toolchain specification, or deliberately configure a reviewed provisioning resolver. Preserve the requested language/vendor details in the failure report.

3. Intentionally broken example: newer bytecode than the supported runtime

Change only the library compile release from 17 to 21, rebuild, and inspect the class file. This is safe because it mutates only the disposable build.

tasks.withType<JavaCompile>().configureEach {
    // BROKEN for a project that promises Java 17 runtime compatibility.
    options.release.set(21)
}
./gradlew -g "$PWD/.gradle-user-home" clean :library:compileJava
javap -verbose library/build/classes/java/main/dev/academy/library/CompatibilityLibrary.class   | grep 'major version'

# If a JDK 17 runtime is available, force the incompatible cell.
./gradlew -g "$PWD/.gradle-user-home" :library:testOn17 --stacktrace

Java 21 class files use major version 65. A Java 17 runtime supports up to major 61 and will reject major 65 with an UnsupportedClassVersionError. If JDK 17 is unavailable, major 65 is the documented simulation evidence for the broken cell; it is not a passing runtime test. Repair by restoring release 17, rebuilding cleanly, and proving major 61.

4. Failure: annotation processor leaks onto runtimeClasspath

A common shortcut is to place a processor on implementation. The build may compile, but consumers/runtime now carry build-time executable code they do not need.

dependencies {
    // Wrong: expands runtime graph with compiler tooling.
    implementation(project(":processor"))
}
./gradlew :library:dependencies --configuration runtimeClasspath
./gradlew :library:dependencies --configuration annotationProcessor

The correction is structural, not a cache deletion: restore compileOnly(project(":processor")) for the annotation type and annotationProcessor(project(":processor")) for the processor implementation. Then compare the two reports again and verify generated source still appears.

5. Failure: generated class disappears

If BuildInfo is missing, inspect the processor graph and service registration before editing production source. A missing META-INF/services/javax.annotation.processing.Processor, wrong processor coordinate, or processor exception can prevent generation.

./gradlew :library:dependencies --configuration annotationProcessor
./gradlew :library:compileJava --info --stacktrace
find library/build/generated/sources/annotationProcessor -type f -print 2>/dev/null || true

Do not commit a hand-written replacement into the generated package merely to make compilation pass; that hides the build-tool failure and creates two sources of truth.

6. Failure: Java and Kotlin bytecode targets diverge

In an optional Kotlin/JVM build, a Java 17 target paired with Kotlin JVM 21 output is a compatibility defect. Current Kotlin Gradle plugin behavior validates related compile-task targets and defaults to an error on Gradle 8+.

import org.jetbrains.kotlin.gradle.dsl.JvmTarget

kotlin {
    compilerOptions {
        // BROKEN when related Java compilation targets 17.
        jvmTarget.set(JvmTarget.fromTarget("21"))
    }
}

Repair the target, not the validation mode. Setting the validation mode to warning/ignore merely conceals a graph inconsistency unless you have a documented special case with separate artifacts/source sets.

7. Failure: CI cannot satisfy requested vendor/version

A toolchain spec can constrain both language version and vendor. That is useful when vendor-specific behavior is a real requirement, but it also narrows the candidate set. If CI has Java 21 from another vendor and no resolver is configured, a request for a specific vendor may fail even though “Java 21” exists.

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
        vendor = JvmVendorSpec.ADOPTIUM
    }
}

Inspect javaToolchains first. If the vendor requirement is policy, provision/preinstall it deliberately. If it was accidental cargo-cult configuration, remove only the unnecessary vendor constraint and preserve the language-version requirement.

8. Failure: tests pass on the newer runtime only

A testOn21 success proves one matrix cell. If testOn17 fails due to a runtime API assumption, resource/provider difference, or class version, the support claim remains broken even if the default test task is green. CI should make minimum-runtime coverage an explicit gate, not a dashboard note.

9. Security and performance without destructive folklore

Toolchain resolver plugins, JDK downloads, annotation processors, Kotlin/compiler plugins, and global Gradle init scripts execute or influence build code. Review them as supply-chain inputs. Performance investigations should distinguish JDK/toolchain discovery, dependency/plugin resolution, compilation, annotation processing, Kotlin compilation, test JVM startup, and cache effects. A slow first run after adding a toolchain is not proof that compilation itself regressed.

Do not purge normal caches: Use the project-local .gradle-user-home to compare fresh state. Never delete normal ~/.gradle or installed JDKs as a first diagnostic step.

10. Summary and bridge

Compatibility incidents become tractable when each JVM/compiler/runtime identity has its own evidence. Lesson 5 turns that diagnostic model into a migration dossier with predictions, matrix results, one deliberate mismatch, repair, and rollback.

Knowledge check

Gradle starts successfully on JDK 21 but compileJava says no matching toolchain. Which layer failed?

What does class-file major 65 mean for a Java 17 support claim?

Why is changing Kotlin target validation to ignore usually the wrong repair?

A processor disappears from generated output. What should you inspect before writing a replacement class by hand?

Why can adding a vendor to a Java toolchain spec cause a new CI failure?

Official references and version notes

  • Gradle Compatibility Matrix — Gradle 9.7.1 currently requires JVM 17–26 to run; supported toolchain compile/test versions are a separate concern.
  • Toolchains for JVM projects — Java toolchain selection, --release, vendor selection, discovery, provisioning, and javaToolchains diagnostics.
  • JavaToolchainSpec API — valid toolchain specifications and language-version/vendor semantics.
  • Gradle Java Plugin — annotationProcessor, annotation processor path isolation, generated sources, incremental annotation processing, and Java compilation behavior.
  • Building Java & JVM Projects — current guidance for toolchains, release, and legacy source/target compatibility.
  • Kotlin Gradle project configuration — Kotlin/JVM plugin 2.4.10 examples, JVM toolchain behavior, and Java/Kotlin JVM-target compatibility checks.
  • Kotlin compiler options — typed compilerOptions, JvmTarget, and the deprecation of legacy kotlinOptions.
  • Kotlin releases — Kotlin 2.4.10 is the current stable line used only in the optional Kotlin/JVM lane.

Version-sensitive behavior was rechecked against current Gradle and Kotlin primary documentation on 2026-08-24. Mandatory labs use Gradle 9.7.1 through the previously verified Wrapper, JDK 21 as the Gradle runtime and Java compiler toolchain, Java 17 as the strict --release target, JUnit 6.1.3 for the Java test fixture, and an isolated GRADLE_USER_HOME. Kotlin/JVM 2.4.10 is optional because applying it may require plugin resolution. Toolchain auto-provisioning is not assumed: an unavailable JDK cell is recorded/simulated unless the learner has deliberately configured a reviewed resolver.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.