Many developers hit a wall when transitioning to JPMS because they assume it works exactly like the classpath. They’re often puzzled by errors like:
Error: module not found
Error: package is not visible
This happens because JPMS has a completely different internal resolution model compared to the classpath. Instead of a flat world where everything can see everything, JPMS introduces a graph of modules, dependencies, and encapsulation rules.
Understanding JPMS internals is crucial for building enterprise systems, modularizing legacy apps, and creating custom runtime images with tools like jlink
.
Core JPMS Internals
At its heart, JPMS introduces several concepts that govern how code is compiled and executed.
1. Module Descriptors
Defined in module-info.java
, they declare dependencies and exports.
module com.example.user {
exports com.example.user.api;
requires com.example.order;
}
This tells the JPMS resolver:
- The module is named
com.example.user
. - It exports
com.example.user.api
. - It requires another module
com.example.order
.
2. Module Resolution Graph
Unlike the classpath (flat, global visibility), JPMS builds a directed graph of modules at startup.
- Nodes → modules.
- Edges → dependencies (
requires
).
If a dependency isn’t found, the graph build fails and the JVM won’t start the application.
3. Layers and Class Loaders
Modules are loaded in layers.
- Boot Layer → standard JDK modules (
java.base
,java.sql
, etc.). - Application Layer → user-defined modules.
Each layer has its own classloader, ensuring strict isolation.
4. Strong Encapsulation
By default, only packages explicitly exported in module-info.java
are visible. Internals remain hidden. This solves the problem of accidental dependencies in large systems.
module com.example.payment {
exports com.example.payment.api; // public
// com.example.payment.internal is hidden
}
5. Reflection and Openness
Frameworks that rely on reflection (Spring, Hibernate) often fail unless packages are opened.
module com.example.payment {
opens com.example.payment.entity to hibernate.core;
}
exports
→ compile-time + runtime visibility.opens
→ runtime reflective visibility.
6. Services with ServiceLoader
JPMS integrates with the ServiceLoader mechanism for dynamic discovery.
module com.example.email {
provides com.example.notification.api.Notifier
with com.example.email.EmailNotifier;
}
module com.example.app {
uses com.example.notification.api.Notifier;
}
Pitfalls in JPMS Internals
- Classpath fallback → Mixing classpath and module path leads to confusing errors.
- Split packages → Not allowed across modules.
- Overusing open modules → Breaks encapsulation.
- Transitive chaos → Overusing
requires transitive
creates unwanted dependencies. - Non-modularized libraries → Still a challenge; need automatic or unnamed modules.
Best Practices for JPMS Internals
- Always use explicit requires/exports.
- Keep APIs in dedicated packages separate from internals.
- Avoid
open module
in production; useopens
sparingly. - Use
jdeps
to analyze dependencies and modular boundaries. - Prefer services (
provides/uses
) over hard dependencies for extensibility.
📌 What's New in Java Versions?
- Java 5 → N/A (modules introduced in Java 9)
- Java 9 → JPMS introduced: module descriptors, strong encapsulation, service loader integration
- Java 11 → Refinements and tooling improvements (
jdeps
, IDE support) - Java 17 → Performance and security refinements for modular systems
- Java 21 → No significant updates across Java versions for this feature
Analogy
Think of JPMS as a city with neighborhoods (modules):
- Each neighborhood has gates (exports) for outsiders.
- Some gates allow only auditors (frameworks) in (
opens
). - Roads (requires) connect neighborhoods.
- A central city planner (resolver) checks if all roads and gates are valid before the city opens.
This prevents random construction (classpath chaos) and ensures order.
Summary + Key Takeaways
- JPMS builds a module graph at startup.
- Modules expose only what they declare, keeping internals hidden.
- Reflection requires
opens
. - Services enable loose coupling with
provides
anduses
. - Best practice: keep APIs clean, internals hidden, and avoid unnecessary openness.
FAQs
Q1. What is the difference between the classpath and module path?
Classpath exposes everything globally; module path enforces explicit boundaries.
Q2. Why do I get “package is not visible” errors in JPMS?
Because the package wasn’t exported in the module descriptor.
Q3. What is the purpose of requires transitive
?
It re-exports dependencies so downstream modules automatically inherit them.
Q4. How do open
and opens
differ?
open module
→ all packages reflective.opens
→ only specific packages reflective.
Q5. What are automatic modules, and should I use them?
They’re non-modular JARs treated as modules. Use during migration, but not long-term.
Q6. How does JPMS improve security compared to classpath?
By hiding internals, enforcing strict resolution, and limiting reflection.
Q7. When should I use jlink
vs jmod
?
jlink
→ Create custom runtime images.jmod
→ Package and distribute modules.
Q8. Can I modularize incrementally?
Yes, use automatic and unnamed modules as stepping stones.
Q9. How do I handle third-party libraries that aren’t modularized?
Use as automatic modules or on the classpath temporarily.
Q10. Do frameworks like Spring and Hibernate fully support JPMS?
Partial support; many need opens
for reflection.