JPMS — the Java Platform Module System, introduced in Java 9 in 2017 — is the Java language feature everyone has an opinion about and few have actually adopted. Most enterprise Java in 2024 still runs in the unnamed module world, with classpath-based dependency resolution that is functionally identical to Java 8. This is not necessarily a problem, but it means the supply chain benefits JPMS is capable of delivering are mostly left on the table. For teams willing to adopt modules — and the population of those teams is slowly growing — the posture improvement is real. This post walks through what JPMS actually gives you in supply chain terms.
What encapsulation means for dependency risk
Pre-JPMS, any class in any JAR on the classpath can access any public class in any other JAR. A malicious dependency can reach into your internal APIs, read private fields via reflection with minimal friction, and generally has the run of the JVM. Encapsulation in JPMS changes this: a module must explicitly exports a package for consumers to access it, and opens a package for reflection access.
In practice this means:
- A
provides/usesservice-loader relationship between modules is the supported way to extend behavior, with clear boundaries. - Reflective access to internals requires an explicit
--add-opensat the JVM level or anopensdeclaration in the module. This is a reviewable, audit-visible event. - A dependency can no longer silently reach into
sun.*orjdk.internal.*packages; those are strongly encapsulated by default.
For supply chain, this reduces the blast radius of a compromised library. Reduced — not eliminated. A module still gets full access to what it imports.
How JPMS interacts with Maven and Gradle
Maven and Gradle both support JPMS, but with different ergonomics:
Maven: the maven-compiler-plugin picks up module-info.java automatically. Dependencies become JPMS modules if they have module-info, or automatic modules derived from the JAR filename if they don't. The transitive interaction of automatic modules is the most common source of confusion.
Gradle: requires explicit configuration of the java-modules plugin (or equivalent) to get clean JPMS behavior. The modularity plugin handles most of it.
The common failure mode: a project declares itself a module but pulls in non-module dependencies, creating a mixed environment where JPMS enforcement is partial. The posture benefit degrades with the degree of mixing.
The split-package problem is real
Two modules cannot export the same package. Legacy dependencies sometimes split a package across multiple JARs (e.g., javax.xml.bind appearing in both jaxb-api and a backport). JPMS refuses to load both. The migration cost of consolidating split packages is usually small but tedious.
Practical advice: when you decide to modularize, budget a day per legacy JAR conflict. The work is mechanical but hits enough edge cases that it isn't free.
Reflective access and open vs exports
exports allows compile-time access. opens additionally allows reflective access. Frameworks like Spring and Hibernate rely heavily on reflection to set private fields; these frameworks require opens declarations to work properly in a modularized application.
The supply chain read: every opens declaration is a controlled expansion of access. When reviewing a module-info.java, the opens lines are the high-leverage ones — they grant dangerous capabilities to named modules. A line like:
opens com.example.internal to spring.core, spring.beans;
is a clear, auditable statement that specific trusted modules can reflect into internals. Without JPMS, this access was implicit and unconstrained.
--illegal-access is dead in newer JDKs
JDK 17 removed the --illegal-access=permit option that had allowed classpath-based code to reflectively access JDK internals with a warning. Applications that relied on this access now fail at startup unless they have explicit --add-opens flags. This has accelerated modularization of legacy code that had been stuck on pre-JPMS patterns. The supply chain implication: the forced upgrade is a forcing function for better encapsulation.
JLink changes distribution posture
jlink builds a custom Java runtime containing only the modules your application needs. The resulting image is smaller (often 40–60% smaller than a full JDK) and has fewer unused attack surface modules.
A typical jlink command:
jlink --module-path $JAVA_HOME/jmods:modules \
--add-modules com.myapp.main,java.sql,java.logging \
--compress=2 \
--no-header-files \
--no-man-pages \
--output myapp-runtime
The resulting myapp-runtime is self-contained. For container deployments this is a direct container-size win with a secondary posture benefit — fewer modules means fewer potential CVEs in the runtime.
Modular artifacts in supply chain inventory
SBOMs for modular Java applications can capture module names in addition to Maven coordinates, which improves traceability. CycloneDX 1.5+ has a bom-ref concept that cleanly represents module-JAR relationships. Early SBOM tooling didn't handle modules well; modern versions (cyclonedx-maven-plugin 2.7+, cyclonedx-gradle-plugin 1.8+) do.
When does modularization pay back?
Four cases where JPMS is worth the adoption cost:
- Library authors wanting to provide a clear public API with enforceable boundaries.
- Security-sensitive applications where reduced reflective access surface is a real threat-model concern.
- Containerized deployments wanting
jlink-reduced images. - Long-lived enterprise codebases where legacy access patterns are accumulating technical debt.
Four cases where it isn't:
- Short-lived applications where the migration cost exceeds the benefit.
- Spring-heavy applications where framework integration is painful enough that the cost-benefit tips.
- Monolithic codebases where the module boundaries are not clear.
- Applications with legacy dependencies that will never be modularized.
The honest adoption rate across enterprise Java in 2024 is low but growing, and the path of least resistance remains the unnamed module world.
How Safeguard Helps
Safeguard's Java support captures module boundaries when present and correlates them with the reachability graph, so a modularized Java codebase has more precise reachability and tighter policy gates. SBOM generation emits both Maven coordinates and JPMS module names. Griffin AI identifies opens declarations in the module graph and flags unusual reflective grants that grew without explicit review. For Java teams that have modularized or are considering it, Safeguard treats JPMS as a first-class signal rather than an edge case.