History. Prior to Java 9, reflection could arbitrarily circumvent access modifiers via setAccessible(true), breaking encapsulation at will. The introduction of the Java Platform Module System (JPMS) established strong encapsulation by default, where modules must explicitly grant permission for deep reflective access to their internal packages.
Problem. When code in one module attempts to use MethodHandles or core reflection to access a non-public field in another module's package, the JVM performs a rigorous accessibility check. This verification ensures that the target package has been explicitly opened to the caller's module. Without this permission, the JVM throws an InaccessibleObjectException (or IllegalAccessException for legacy reflection), regardless of whether a SecurityManager is installed or the field is accessed via VarHandle.
Solution. The module must declare opens package.name [to specific.module]; in its module-info.java, or the application must be launched with the --add-opens source.module/package.name=target.module flag. This directive dynamically modifies the module's internal accessibility graph, adding the target module to the set of modules authorized to perform deep reflection on that package's private members.
// Module: app.core (module-info.java) module app.core { // Package com.app.internal is not opened exports com.app.api; } // Module: framework.inject public class Injector { public void inject(Object target) throws Throwable { MethodHandles.Lookup lookup = MethodHandles.privateLookupIn( target.getClass(), MethodHandles.lookup() ); // Throws InaccessibleObjectException without --add-opens VarHandle handle = lookup.findVarHandle( target.getClass(), "secretField", String.class ); handle.set(target, "injected"); } }
A development team migrated their monolithic Spring-based application to the Java Module System, partitioning the codebase into the core business logic module (app.core) and a separate dependency injection framework module (framework.inject). Immediately following deployment, the application crashed during bean initialization with an InaccessibleObjectException when the framework attempted to inject configuration values into private fields residing within app.core's internal com.app.internal package.
Three potential architectural solutions were evaluated. The first approach involved relocating all injectable classes into exported packages within app.core. While this would resolve the immediate access violation, it would fundamentally violate encapsulation principles by exposing internal implementation details to all other modules, thereby increasing the maintenance burden and expanding the attack surface for future security audits. The second solution proposed using the --add-exports JVM argument to expose the internal packages to the framework module. However, while --add-exports grants compile-time and runtime visibility to public types, it explicitly does not permit deep reflection on private members, rendering it insufficient for Spring's field injection mechanisms which require modifying private state. The third option utilized the targeted command-line argument --add-opens app.core/com.app.internal=framework.inject. This approach maintained strict source-level encapsulation for all other modules while explicitly granting only the injection framework the necessary privileges to perform deep reflection on the specific internal package.
The team ultimately selected the third option, documenting the required --add-opens directives in their deployment scripts and Docker configurations. This solution preserved the integrity of the module system during development while allowing the framework to function correctly, resulting in a successful migration with explicitly controlled access boundaries.
Why does setAccessible(true) fail on a private field within an exported package when accessed from a different module, despite the absence of a SecurityManager?
Candidates frequently conflate package exportation with openness. The exports directive only makes public types and members accessible for standard compilation and invocation; it does not grant the ReflectPermission required to suppress Java language access checks. JPMS strong encapsulation operates independently of the SecurityManager, enforced directly by the JVM's access control mechanisms. To enable setAccessible(true) on non-public members, the package must be explicitly declared as open, or the entire module must be declared as an open module.
How does the MethodHandles.Lookup capture mechanism influence cross-module accessibility, and why might invoking MethodHandles.lookup().in(targetClass) degrade the lookup's capabilities?
A Lookup object encapsulates the access privileges of its creator's module and package context. When Lookup.in(targetClass) is invoked, the JVM re-evaluates the lookup's privileges based on the target class's module. If the target class resides in a different module that has not opened its package to the lookup's module, the lookup is "degraded" to PUBLIC mode, stripping it of PRIVATE and MODULE access capabilities. To maintain full access rights across modules, the target module must explicitly open the package to the lookup's module, or the code must utilize privateLookupIn, which requires the target class to be within the same module or accessible via the module graph.
What fundamental distinction exists between --add-exports and --add-opens at the JVM level, and why does the former cause IllegalAccessException during dependency injection even when compilation succeeds?
The --add-exports flag adds a package to the module's exported list, enabling the target module to access public types at both compile-time and runtime. However, this directive does not modify the module's "open" set, which controls deep reflection. The Java Language Specification strictly separates readability (exports) from reflectability (opens). Dependency injection frameworks require the latter to manipulate private fields via Reflection or VarHandle. Consequently, while --add-exports satisfies the compiler and allows method invocation, runtime attempts to modify private state will still fail. Only --add-opens adds the package to the set of packages accessible for deep reflection, thereby permitting the framework to alter private field values.