A common frustration for developers migrating to Java Modules is that applications running perfectly on the classpath suddenly break on the module path. Errors like “package is not visible”, “module not found”, or conflicts with split packages often appear.
This matters because in large enterprise systems, microservices, and cloud-native deployments, strong encapsulation and custom runtime images are essential. Migrating from the classpath to the module path ensures better maintainability, security, and performance—but it requires careful planning.
This migration guide provides a step-by-step process, with code examples, best practices, and pitfalls to avoid.
Step 1: Analyze Your Existing Application
Before modularizing, identify:
- All existing JAR dependencies.
- Any split packages (same package across multiple JARs).
- Libraries that rely heavily on reflection (Spring, Hibernate, Jackson).
Tooling Tip
Use jdeps
to analyze dependencies:
jdeps --class-path libs/* app.jar
This shows which packages depend on which modules.
Step 2: Start with Automatic Modules
Place third-party libraries on the module path, even if they are not modularized. JARs without module-info.java
become automatic modules.
java --module-path mods -m com.example.app/com.example.Main
- Automatic modules are a temporary bridge.
- Module names are derived from JAR filenames (fragile in the long term).
Step 3: Introduce module-info.java
for Your Code
Start modularizing your own code by adding a module-info.java
file.
Example
module com.example.app {
requires com.example.user;
requires com.example.order;
exports com.example.app.api;
}
- Declare dependencies with
requires
. - Export only public APIs with
exports
. - Hide internals for better encapsulation.
Step 4: Handle Split Packages
If two JARs declare the same package, JPMS will fail.
Resolution Strategies
- Refactor package structure (preferred).
- Merge code into a single common module.
- Use services (
provides
anduses
) to separate responsibilities.
Step 5: Address Reflection Issues
Frameworks like Spring and Hibernate often fail due to blocked reflection. Use opens
to allow controlled access.
module com.example.user {
opens com.example.user.entity to hibernate.core;
exports com.example.user.api;
}
opens
grants reflection to specific frameworks.- Avoid using
open module
unless absolutely necessary.
Step 6: Compile and Run with javac
and java
Compile with module awareness:
javac -d out --module-source-path src $(find src -name "*.java")
Run the modular app:
java --module-path out -m com.example.app/com.example.app.Main
Step 7: Optimize with jlink
and jmod
Once migration is complete:
- Use
jlink
to build custom runtime images. - Use
jmod
to package modules for distribution.
jlink --module-path $JAVA_HOME/jmods:out --add-modules com.example.app --output custom-runtime
Pitfalls & Misuse Cases
- Leaving everything on classpath → Defeats modularization.
- Overusing automatic modules → Leads to fragile builds.
- Exporting all packages → Removes encapsulation benefits.
- Open modules in production → Creates security risks.
- Ignoring split packages → Compilation/runtime failures.
Best Practices
- Modularize gradually, starting with your own code.
- Export only API packages; keep internals hidden.
- Use
requires transitive
sparingly for stable APIs. - Replace automatic modules with fully modularized dependencies over time.
- Document your migration steps for team-wide consistency.
📌 What's New in Java Versions?
- Java 5 → N/A (modules introduced in Java 9)
- Java 9 → JPMS introduced:
module-info.java
, module path support, modular JDK - Java 11 → Improved
jdeps
,jlink
, and IDE support - Java 17 → Refinements in JPMS security and performance
- Java 21 → No significant updates across Java versions for this feature
Analogy
Think of moving from the classpath to the module path like reorganizing a chaotic office:
- On the classpath, every employee can access every file in every cabinet.
- On the module path, each department controls its own files and shares only what’s necessary.
This organization reduces confusion and strengthens security.
Summary + Key Takeaways
- Migration to the module path requires handling automatic modules, split packages, and reflection issues.
- Start with
module-info.java
for your own code, then gradually modularize dependencies. - Use
opens
for frameworks, but avoidopen module
in production. - JPMS enables encapsulation, security, and deployment optimization with tools like
jlink
.
FAQs
Q1. What is the difference between the classpath and module path?
Classpath exposes everything globally; module path enforces strict dependencies and exports.
Q2. Why do I get “package is not visible” errors when migrating?
Because the package wasn’t exported in module-info.java
.
Q3. What is the purpose of requires transitive
?
It re-exports dependencies so downstream modules don’t need to declare them explicitly.
Q4. How do open
and opens
differ in reflection?
open module
→ All packages are reflective.opens
→ Specific packages reflective, often restricted to frameworks.
Q5. What are automatic modules, and should I use them?
They’re JARs without descriptors treated as modules. Useful temporarily during migration.
Q6. How does JPMS improve security compared to classpath?
By hiding internals, restricting reflection, and preventing split package conflicts.
Q7. When should I use jlink
vs jmod
?
jlink
→ Builds custom runtime images.jmod
→ Packages modules for distribution.
Q8. Can I migrate a legacy project to modules incrementally?
Yes. Start with your code, then modularize dependencies gradually.
Q9. How do I handle third-party libraries that aren’t modularized?
Use them as automatic modules or place them on the classpath temporarily.
Q10. Do frameworks like Spring or Hibernate fully support modules?
Partial support—most require opens
for reflection.