Week 2 - Build Tools, Dependencies, and JPMS¶
Conceptual Core¶
You will live in Maven or Gradle for the rest of your career. Pick one and learn it deeply; the other is then 80% transferable. Switching mid-project loses 2-4 weeks of velocity - pick once.
JPMS (Java Platform Module System, JEP 261) is the thing every Java developer knows exists and nobody outside library authors uses. Most applications never need it. The tax: recognizing when a module-system issue (an --add-opens requirement, an "unnamed module" warning, a missing requires in module-info.java) explains a failure you'd otherwise spend hours debugging.
Mechanical Detail¶
- Maven lifecycle:
validate → compile → test → package → verify → install → deploy. Each phase runs itself plus all earlier ones.mvn packageis the daily driver.mvn dependency:treeshows transitives;mvn dependency:analyzeflags declared-but-unused and used-but-undeclared deps. - Maven inheritance: every project inherits from
super-pom. Multi-module projects add aparentpom for shared config. BOMs (e.g.spring-boot-dependencies) manage versions of an artifact constellation; import via<dependencyManagement>+<scope>import</scope>. - Gradle: task graph (
gradle Xruns X's dependency tasks first). The configuration cache (org.gradle.configuration-cache=true) skips readingbuild.gradleon unchanged builds - large speedup, but breaks plugins that do I/O at configuration time. Version catalogs (gradle/libs.versions.toml) centralize versions across modules. - JPMS:
module-info.javadeclaresrequires,exports,opens.requires transitivere-exports a dependency.openspermits reflective access (Jackson, Spring, JPA need it). Two failure symptoms: "illegal reflective access" (needsopensor--add-opens); "module X not found" (needsrequiresor to be on the module path). jdeps Yourapp.jarlists modules needed;jlink --add-modules <list> --output runtime --strip-debug --no-man-pagesproduces a 35-50MB JRE instead of the 200MB stock JDK. Useful for containers.
The trap
Not pinning Maven plugin versions. The default resolves "whatever is latest available" - a build that worked yesterday may behave differently today. Always pin <version> in pluginManagement and let Renovate update them deliberately.
Lab¶
Take any small library. Modularize it: write module-info.java, run jdeps to find required modules, jlink --add-modules <list> --output runtime/ to build a custom runtime. Measure size before (du -sh $JAVA_HOME) vs after (du -sh runtime/). Then mvn install to a local repo and consume from a separate Gradle project via mavenLocal().
Idiomatic Drill¶
Read 10 random pom.xml files from popular libraries (Caffeine, Resilience4j, Micrometer, Jackson). Spot the patterns: BOMs via <dependencyManagement>, explicit <scope> (compile / test / provided / runtime), plugin versions in pluginManagement not inline.
Production Hardening Slice¶
For reproducible builds, lock down:
- Java version via <maven.compiler.release>21</...> (or Gradle JavaLanguageVersion.of(21)).
- Parameter names via -parameters (Maven <maven.compiler.parameters>true</...>) so Spring / Jackson can read them at runtime.
- Every plugin version in pluginManagement.
- Renovate or Dependabot for dependency PRs.