Keywords: Java Module System | Reflection Access | InaccessibleObjectException | Strong Encapsulation | Command-Line Arguments
Abstract: This article provides an in-depth analysis of the InaccessibleObjectException in Java 9's module system, explaining its causes and two main scenarios. It offers solutions using command-line arguments for reflective calls into JDK modules and module descriptor modifications for reflection over application code, supported by code examples. The discussion includes framework adaptation strategies and best practices.
With the introduction of the module system in Java 9 and later versions, stronger encapsulation has altered the behavior of reflection operations. When code attempts to access restricted class members via reflection, it may encounter java.lang.reflect.InaccessibleObjectException with the typical error message "Unable to make {member} accessible: module {A} does not 'opens {package}' to {B}". This exception is particularly common in frameworks like Spring, Hibernate, and JAXB that heavily rely on reflection.
Analysis of Exception Mechanism
The core cause of this exception lies in the strong encapsulation enforced by the Java Platform Module System (JPMS). The module system requires that access must meet specific conditions: the type being accessed must be public, and the owning package must be explicitly exported. Reflection operations are subject to the same restrictions, especially when the setAccessible method is called to bypass access controls.
From a technical implementation perspective, the exception typically occurs in code patterns similar to the following:
static void setAccessible(final AccessibleObject ao, final boolean accessible) {
if (System.getSecurityManager() == null)
ao.setAccessible(accessible);
else {
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
ao.setAccessible(accessible);
return null;
}
});
}
}
When setAccessible(true) is invoked, the module system checks whether the current module has permission to access the target member. If access is denied, InaccessibleObjectException is thrown.
Main Scenarios and Solutions
Scenario 1: Reflective Call Into JDK
This scenario typically occurs when third-party libraries or frameworks attempt to access JDK internal APIs via reflection. For example, the Javassist case presented in the question:
Unable to make java.lang.ClassLoader.defineClass accessible: module java.base does not "opens java.lang" to unnamed module @1941a8ff
Since JDK modules are immutable for application developers, the only solution is to relax access restrictions through command-line arguments. The correct approach is to use the --add-opens argument:
java --add-opens java.base/java.lang=ALL-UNNAMED
The syntax for this argument is {A}/{package}={B}, where:
{A}is the target module name{package}is the package to be opened{B}is the module permitted to access
If the reflecting code is in a named module, ALL-UNNAMED can be replaced with the specific module name. In practical deployment, these arguments may need to be set via environment variables:
export JAVA_TOOL_OPTIONS="$JAVA_TOOL_OPTIONS --add-opens=java.base/java.lang=ALL-UNNAMED"
For particularly complex cases requiring numerous arguments, consider using the encapsulation kill switch --permit-illegal-access, but note that this option is only effective in Java 9 and significantly reduces module system security.
Scenario 2: Reflection Over Application Code
This scenario is common when frameworks perform reflection operations on application code, such as Spring injecting beans or Hibernate accessing entity classes. Here, since the target module descriptor can be modified, more flexible solutions are available.
In the module descriptor (module-info.java), the following options can be chosen:
// Option 1: Export package to all modules
exports com.example.mypackage;
// Option 2: Export package to specific module
exports com.example.mypackage to org.springframework.core;
// Option 3: Open package to all modules
opens com.example.mypackage;
// Option 4: Open package to specific module
opens com.example.mypackage to org.hibernate.core;
// Option 5: Open entire module
open module com.example.app {
requires java.base;
requires org.springframework.boot;
}
These options vary in terms of security and flexibility:
exportsallows access at both compile time and runtime, but only to public membersopensallows access only at runtime, including via reflection to non-public members- Versions restricted to specific modules provide finer-grained access control
Framework Adaptation and Best Practices
For applications using ORM frameworks like Hibernate, in addition to the above solutions, consider enabling build-time bytecode enhancement. Starting from Hibernate 5.0.0, support is available through Gradle, Maven, or Ant plugins to perform bytecode modifications during the build phase, thus avoiding runtime reflection operations.
However, this approach requires careful testing, as build-time enhancement may produce different behavior compared to runtime enhancement. Some long-stable unit tests might start failing with issues like LazyInitializationException when build-time enhancement is enabled.
In the long term, relying on command-line arguments like --add-opens should be considered a temporary workaround. Framework developers should gradually migrate to module-system-compatible APIs, such as using MethodHandles.Lookup instead of traditional reflection access. For frameworks that need to access private members of their consumers (e.g., JPA implementations), they should require consumers to explicitly open the relevant packages in their module descriptors.
Multi-Version Compatibility Considerations
In real-world projects, it is often necessary to support both Java 8 and Java 9+ versions. Note that Java 7 and 8 JVMs will immediately abort when encountering unrecognized --add-opens arguments. Therefore, cross-version compatibility cannot be achieved by simply statically modifying batch files or scripts.
The recommended strategy is to dynamically adjust startup arguments based on the runtime Java version, or use conditional compilation to ensure code compatibility across different Java versions.
As the Java ecosystem evolves, mainstream frameworks are actively adapting to the module system. Developers should follow official documentation of their used frameworks for the latest modular support information and update dependencies timely for better compatibility and performance.