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.
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
- Preserve concise evidence: failing task, exception, requested toolchain, test runtime, and relevant logs.
-
Confirm Wrapper/Gradle/runtime JVM:
./gradlew --version. -
Inspect declared toolchain/target configuration:
Java/Kotlin build blocks and
options.release. -
Inspect available toolchains:
javaToolchains; do not assumeJAVA_HOMEtells the whole story. -
Inspect compile/processor/runtime graphs:
especially
annotationProcessorversusruntimeClasspath. -
Inspect generated files and bytecode:
generated-source directory,
javap -verbose, test report. - Apply the least destructive correction: fix the requested toolchain/target/dependency edge, not global caches.
- 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?
The requested task toolchain layer, not the Gradle runtime JVM layer.
What does class-file major 65 mean for a Java 17 support claim?
It is Java 21 bytecode and a Java 17 runtime cannot load it; the target is broken until rebuilt to major 61 or lower.
Why is changing Kotlin target validation to
ignore usually the wrong repair?
It suppresses evidence of incompatible Java/Kotlin output targets instead of aligning the artifact contract.
A processor disappears from generated output. What should you inspect before writing a replacement class by hand?
The annotationProcessor dependency graph, processor
discovery/service metadata, compile logs, and generated-source
directory.
Why can adding a vendor to a Java toolchain spec cause a new CI failure?
It narrows matching installations; a compatible Java version from another vendor no longer satisfies the specification.
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, andjavaToolchainsdiagnostics. - 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 legacykotlinOptions. - 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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.