Access Control in Java Modules: Deep Reflection Issues Explained with Examples

Illustration for Access Control in Java Modules: Deep Reflection Issues Explained with Examples
By Last updated:

When developers migrate to Java Modules (JPMS), one of the most frustrating issues they encounter is reflection not working as expected. Code that worked flawlessly on the classpath suddenly throws exceptions like:

java.lang.IllegalAccessException: class com.example.framework.ReflectionUtil 
cannot access class com.example.user.entity.User (in module com.example.user)

This happens because JPMS enforces strong encapsulation, and reflection must explicitly be allowed with opens. Understanding how access control and reflection work in modules is critical for building modern enterprise systems, especially when using frameworks like Spring, Hibernate, or Jackson, which rely heavily on reflection.

This tutorial explains deep reflection issues in JPMS, how to fix them, and best practices to keep applications both secure and functional.


Access Control Basics in JPMS

1. Exports vs Opens

  • exports → Makes a package visible to other modules at compile-time and runtime.
  • opens → Makes a package available for deep reflection at runtime, but not at compile-time.

Example

module com.example.user {
    exports com.example.user.api; // Public API
    opens com.example.user.entity to hibernate.core; // Reflection for ORM
}
  • Other modules can compile against com.example.user.api.
  • Hibernate can reflectively access com.example.user.entity, but other modules can’t.

2. Open Modules

open module com.example.test {
    exports com.example.test.api;
}
  • An open module means all its packages are open to reflection.
  • Useful in testing or debugging, but dangerous in production.

3. Deep Reflection in Frameworks

Frameworks often require reflective access to private fields, methods, and constructors. JPMS blocks this unless opens is used.

Example with Hibernate

module com.example.user {
    opens com.example.user.entity to org.hibernate.orm.core;
}

This allows Hibernate to perform ORM mapping via reflection without exposing the package as API.


Pitfalls in Reflection with JPMS

  1. Confusing Exports and Opens

    • exports doesn’t allow reflection.
    • opens doesn’t allow compile-time use.
  2. Overusing open modules

    • Leads to security risks by exposing everything.
  3. Forgetting Targeted Opens

    • Always open packages to specific frameworks, not globally.
  4. Relying on Automatic Modules

    • Reflection can behave unpredictably when using unnamed or automatic modules.

Best Practices for Reflection in JPMS

  • ✅ Export only stable APIs.
  • ✅ Use opens selectively for reflection-heavy frameworks.
  • ✅ Avoid open module in production.
  • ✅ Organize entities and reflection-dependent classes into separate packages.
  • ✅ Document why a package is opened in module-info.java.
  • ✅ Test with frameworks (Hibernate, Spring) to ensure correct module boundaries.

📌 What's New in Java Versions?

  • Java 5 → N/A (modules introduced in Java 9)
  • Java 9 → JPMS introduced strong encapsulation and reflection rules (exports, opens)
  • Java 11 → Tooling support for debugging reflection issues improved
  • Java 17 → Security and performance improvements for reflective access
  • Java 21 → No significant updates across Java versions for this feature

Analogy

Think of reflection in JPMS like a secure office building:

  • Normal visitors (other modules) can only access public areas (exports).
  • Auditors (frameworks) need special badges (opens) to look into private files.
  • Making the entire building open (open module) is like giving everyone unrestricted access—convenient but dangerous.

Summary + Key Takeaways

  • JPMS enforces strong encapsulation, breaking old reflection assumptions.
  • Use exports for API visibility, opens for reflection.
  • Avoid open module; prefer targeted opens.
  • Organize reflection-heavy code into dedicated packages.
  • JPMS makes applications more secure, maintainable, and framework-friendly when configured properly.

FAQs

Q1. What is the difference between the classpath and module path?
Classpath exposes everything globally; module path enforces access rules.

Q2. Why do I get “package is not visible” errors?
Because the package was not exported in module-info.java.

Q3. Why does reflection fail in JPMS?
Because the package was not opened with opens.

Q4. What is the difference between open and opens?

  • open module → all packages open for reflection.
  • opens → only specific packages are open for reflection.

Q5. What is the purpose of requires transitive?
It re-exports dependencies for downstream modules.

Q6. What are automatic modules, and should I use them?
They’re JARs treated as modules. Good for migration, not production.

Q7. How does JPMS improve security compared to classpath?
By hiding internals, restricting reflection, and preventing split packages.

Q8. When should I use jlink vs jmod?

  • jlink → Custom runtime images.
  • jmod → Package modules for distribution.

Q9. Can I migrate legacy projects incrementally?
Yes, start with automatic modules and gradually modularize.

Q10. Do frameworks like Spring and Hibernate fully support JPMS?
They work, but usually need selective opens for reflective access.