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.
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.
--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?
No. That erases the production/test boundary. Move production behavior to main sources or keep the dependency inside tests.
What does NoClassDefFoundError after successful
compilation suggest?
The runtime classpath or runtime packaging/platform contract is missing a class that was available earlier.
Why is requesting --release 22 on JDK 21 a useful
lab failure?
It creates a deterministic, local toolchain/target mismatch with a clear compiler error and no need for destructive changes or a second runtime.
A resource file is in Git but not in the JAR. What should you inspect?
The resource source directory, resource-processing output, and final JAR contents. Being versioned does not mean the build consumes it.
Why try an isolated Maven repository/Gradle User Home before deleting normal caches?
It tests the cache-state hypothesis while preserving normal developer state and gives a reversible, scoped cleanup path.
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.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.