Chapter 02Lesson 04~95 minutes

Java and JVM Project Structure, Source Sets, Compilation, Testing, Packaging, and Toolchains: Diagnostics, Failure Modes, Security, and Performance

Diagnose classpath, bytecode, resource, toolchain, and IDE-versus-CLI failures from evidence instead of deleting caches or changing unrelated settings.

DiagnosticsClasspathsBytecodeResourcesIDE vs CLI

Learning objectives

  • Apply an evidence-first diagnostic sequence to JVM project-layout, classpath, bytecode, resource, and toolchain failures.
  • Diagnose a production source that accidentally depends on test-only code without adding test output to the main classpath.
  • Differentiate compile-success/runtime-failure classpath problems from compiler target incompatibility.
  • Detect missing packaged resources and IDE-versus-CLI JDK drift from generated state.
  • Use isolated project state and controlled rebuilds instead of deleting normal Maven/Gradle caches.
Version baseline — verified 2026-08-23. Labs use JDK 21 as the common build-runtime JDK, Apache Maven 3.9.16, Maven Compiler Plugin 3.15.0, Maven Toolchains Plugin 3.2.0 where toolchain discovery is demonstrated, Gradle 9.7.1, and JUnit Jupiter 5.13.4. Production bytecode is deliberately targeted to Java 17 with the compiler --release mechanism so the lesson can separate “JDK that runs the build” from “Java release the artifact targets.” Maven 4 preview behavior is not required in this chapter. Re-check current versions before reusing the examples in production.

1. Diagnostic sequence: classify before correcting

Preserve the smallest useful evidence first: command, wrapper/tool version, JVM identity, failure message, changed file, and relevant generated output. Then locate the boundary: source set, compile classpath, runtime classpath, compiler/toolchain, resource processing, packaging, or IDE/CLI model.

1. Record ./mvnw -v or ./gradlew -version
2. Record the exact failing command and first causal error
3. Inspect declared/effective build configuration
4. Inspect source set / dependency / task or lifecycle state
5. Inspect generated classes, resources, reports, and JAR contents
6. Reproduce with isolated disposable local state if cache corruption is plausible
7. Apply the smallest correction
8. Rebuild and verify the original invariant

2. Failure A — production code depends on a test-only class

Create src/test/java/academy/greeting/TestNames.java, then incorrectly import it from Main.java. A correct conventional build should fail during main compilation. Do not “fix” this by adding test output to the production compile classpath.

package academy.greeting;

import academy.greeting.TestNames; // class exists only under src/test/java

public final class Main {
    public static void main(String[] args) {
        System.out.println(TestNames.DEFAULT);
    }
}

Expected diagnosis: the main compiler cannot see TestNames because test compilation happens later and test output is not part of the main compile classpath. Correct by moving genuinely production behavior into src/main/java, or by keeping the dependency inside test code if it is only a fixture.

3. Failure B — compilation succeeds but runtime classpath is incomplete

This class of failure occurs when a type is available to compilation but not to the JVM that launches the program. Maven provided-style dependencies and Gradle compileOnly dependencies are common intentional examples: a container/platform is expected to supply the library at runtime. If that assumption is false, startup fails.

Compilation: SUCCESS
Packaging:   SUCCESS
Startup:     java.lang.NoClassDefFoundError: some/library/Type

Question: was the missing module present on the runtime classpath,
or was it only present during compilation?

Repair the runtime delivery contract, not the compiler. Either declare the dependency in a runtime-bearing configuration/scope when the application must ship it, or ensure the documented platform actually supplies it.

4. Failure C — artifact bytecode is newer than the deployment runtime

The classic runtime symptom is UnsupportedClassVersionError. The application was compiled to a class-file level the deployment JVM cannot load. The safest diagnosis records both ends: inspect the class with javap -verbose and record the deployment JVM with java -version.

javap -verbose path/to/Main.class | grep "major version"
java -version

Correct the build contract with an appropriate release target and/or update the supported runtime. Do not patch class-file bytes or hide the error.

5. Failure D — requested release cannot be produced by the selected compiler

This is the controlled failure used in the checkpoint. On a JDK 21 compiler, temporarily request release 22. The build should fail clearly because that compiler cannot target a future Java release. This is safer than requiring a second older JVM merely to create a runtime failure.

<maven.compiler.release>22</maven.compiler.release>
tasks.withType<JavaCompile>().configureEach {
    options.release = 22
}

Expected error language differs by tool/compiler, but the causal message is equivalent to “release version 22 not supported.” Restore 17, rebuild, and verify major version 61.

6. Failure E — resource exists in the repository but is missing from the JAR

Move app.properties from the conventional src/main/resources directory to an unconfigured directory such as assets/. Compilation and tests that do not load the resource may still pass, yet jar tf reveals that the package does not contain it.

jar tf target/*.jar | grep app.properties || echo "resource missing"
jar tf build/libs/*.jar | grep app.properties || echo "resource missing"

The least surprising repair is to restore the conventional resource directory. If a custom layout is a real requirement, configure it explicitly in the build and verify both processed output and final package.

7. Failure F — IDE build passes, wrapper build fails

An IDE may use a different JDK, compiler release, test runner, or build delegation mode. Start by comparing identities rather than changing dependencies.

java -version
./mvnw -v        # Maven project
./gradlew -version  # Gradle project
./gradlew javaToolchains  # Gradle project

Then inspect the IDE’s project SDK and whether it delegates builds/tests to Maven/Gradle. The repository wrapper build is the portable contract; configure the IDE to agree with it, not the reverse.

8. Cache suspicion without destructive cleanup

If a failure looks machine-specific, do not immediately delete ~/.m2/repository or ~/.gradle/caches. First reproduce using an isolated lab location.

mkdir -p .lab-m2/repository
./mvnw -Dmaven.repo.local="$PWD/.lab-m2/repository" clean test
mkdir -p .lab-gradle-home
GRADLE_USER_HOME="$PWD/.lab-gradle-home" ./gradlew clean test

If the isolated run succeeds, you have evidence about local state. You still need to identify the corrupted/stale item before broad deletion. Cleanup may remove only .lab-m2 and .lab-gradle-home after verifying the working directory.

9. Performance diagnosis: name the stage

A “slow build” can mean dependency download, project configuration/model construction, Java compilation, tests, resource processing, packaging, daemon startup, or cache misses. Record cold versus warm state before comparing tools or options. This chapter does not prescribe parallelism or caches yet; it teaches the decomposition that makes later optimization meaningful.

Observation Likely boundary Evidence
first build slow, second fast dependency downloads / daemon warm-up network logs + local repository/cache growth
tests dominate test execution test report durations
compile dominates after source change compiler/incremental boundary task/lifecycle timing and changed inputs
package missing file despite fast build resource/package correctness, not performance JAR listing

Knowledge check

Main compilation fails because it imports a class from src/test/java. Should you add test output to the main classpath?

What does NoClassDefFoundError after successful compilation suggest?

Why is requesting --release 22 on JDK 21 a useful lab failure?

A resource file is in Git but not in the JAR. What should you inspect?

Why try an isolated Maven repository/Gradle User Home before deleting normal caches?

Summary

Diagnose by boundary: source set, compile classpath, runtime classpath, class-file target, resource processing, toolchain, IDE/CLI identity, or cache state. Preserve concise evidence, apply the smallest correction, and verify the original invariant in a controlled rebuild.

Next lesson

Integrate the chapter into one evidence package

Lesson 5 builds a complete small project, maps every source/resource to output, verifies bytecode and package identity, injects a target mismatch, and closes with cleanup plus production handoff evidence.

Official references and version notes

These lessons were finalized against current primary documentation on 2026-08-23. Build-tool, plugin, JDK, repository, and IDE behavior is version-sensitive; verify the exact versions used by your project and CI before applying a production policy.

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.