Understanding module-info.java: Requires, Exports, and Opens Explained with Real Examples

Illustration for Understanding module-info.java: Requires, Exports, and Opens Explained with Real Examples
By Last updated:

A common mistake developers make when modularizing Java applications is forgetting to properly configure module-info.java, leading to errors like “package is not visible” or frameworks failing at runtime due to missing reflection access. These issues confuse teams migrating from the classpath world, where everything was visible everywhere, to the module path where visibility is strictly controlled.

Understanding the three key directives—requires, exports, and opens—is essential for building modular applications. These determine how your modules depend on others, what parts of your code are shared, and how frameworks can still use reflection when needed. In large systems, mastering this prevents spaghetti-like dependencies and strengthens security.


The Role of module-info.java

The module-info.java file defines the metadata of a module. Its core purpose is to:

  • Declare dependencies (requires)
  • Expose packages to other modules (exports)
  • Allow reflective access for frameworks (opens)
  • Configure services (uses, provides)

Without it, a JAR is treated as an unnamed or automatic module, offering little benefit from JPMS.


Requires: Declaring Dependencies

The requires directive specifies which modules a module depends on.

Example

module com.example.order {
    requires com.example.user;
}
  • The order module depends on the user module.
  • Classes in order can access exported packages from user.

Variants

  • requires transitive – Re-exports the dependency.
module com.example.payment {
    requires transitive com.example.order;
}

Now, any module that requires payment also gets access to order.

  • requires static – Dependency needed at compile-time but optional at runtime.

Exports: Sharing Packages

The exports directive controls what packages are visible to other modules.

Example

module com.example.user {
    exports com.example.user.api;
}
  • Only com.example.user.api is accessible to other modules.
  • Internal packages like com.example.user.internal remain hidden.

This enforces strong encapsulation and prevents accidental misuse.

Pitfall

If you forget to export a package, dependent modules will throw compilation errors.


Opens: Allowing Reflection

Some frameworks (e.g., Hibernate, Spring) rely heavily on reflection. JPMS blocks reflection on non-exported packages unless explicitly opened.

Example

module com.example.user {
    opens com.example.user.entity to hibernate.core;
}
  • opens makes com.example.user.entity available for reflection.
  • Restricting it to a specific module (like Hibernate) is safer than opening it globally.

Open Module vs Opens

  • open module → All packages open for reflection (not recommended for security).
  • opens → Granular, package-specific reflection access.

Real-World Example

A payment processing system with three modules:

// user module
module com.example.user {
    exports com.example.user.api;
}

// order module
module com.example.order {
    requires com.example.user;
    exports com.example.order.api;
}

// payment module
module com.example.payment {
    requires transitive com.example.order;
    exports com.example.payment.api;
    opens com.example.payment.entity to hibernate.core;
}
  • payment requires order, which requires user.
  • order doesn’t re-export user, so only payment with requires transitive makes it available further down.
  • Entities in payment are open only for Hibernate, not all modules.

Pitfalls & Misuse Cases

  1. Exporting Everything – Defeats encapsulation, like classpath behavior.
  2. Open Modules – Makes the entire module reflective, increasing attack surfaces.
  3. Requires Transitive Overuse – Creates hidden dependencies and coupling.
  4. Forgetting Requires Static – Leads to runtime failures when optional libraries are absent.

Best Practices

  • Export only API packages, not internal ones.
  • Use opens sparingly and restrict it to specific modules.
  • Avoid open module unless prototyping.
  • Document dependencies clearly when using requires transitive.
  • Combine JPMS with build tools like Maven/Gradle for consistency.

📌 What's New in Java Versions?

  • Java 5 → N/A (modules introduced in Java 9)
  • Java 9 → Introduction of JPMS, module-info.java, modular JDK
  • Java 11 → Refinements and tooling improvements (javac, jlink)
  • Java 17 → Performance and security refinements for modules
  • Java 21 → No significant updates across Java versions for this feature

Analogy

Think of a library system:

  • requires → Your library subscribes to another’s collection.
  • exports → You decide which sections of your library are open to others.
  • opens → You allow inspectors (frameworks) behind the scenes for special checks.

This keeps boundaries clear while allowing controlled collaboration.


Summary + Key Takeaways

  • requires defines dependencies between modules.
  • exports shares only intended packages, hiding internals.
  • opens enables controlled reflection for frameworks.
  • Overusing open or exports weakens modularization.
  • JPMS improves clarity, maintainability, and security for modern Java projects.

FAQs

Q1. What is the difference between the classpath and module path?
Classpath exposes everything globally; module path enforces encapsulation with exports/requires.

Q2. Why do I get “package is not visible” errors when using modules?
Because the package wasn’t exported in module-info.java.

Q3. What is the purpose of requires transitive?
To re-export dependencies so downstream modules don’t need to declare them explicitly.

Q4. How do open and opens differ in reflection?

  • open module opens everything.
  • opens opens a specific package, often restricted to frameworks.

Q5. What are automatic modules, and should I use them?
JARs without descriptors treated as modules. Use temporarily in migration, not in long-term solutions.

Q6. How does JPMS improve security compared to classpath?
By hiding internal packages and limiting reflection access.

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 one modularized JAR and migrate others progressively.

Q9. How do I handle libraries that aren’t modularized?
Use them as automatic modules or keep them on the classpath.

Q10. Do frameworks like Spring/Hibernate fully support modules?
Partial support. They often require opens for reflection to work.