Keywords: Java 9 | Illegal Reflective Access | Module System | Warning Suppression | Unsafe API
Abstract: This paper investigates methods to suppress "Illegal reflective access" warnings in Java 9 and later versions through programming approaches rather than JVM arguments. It begins by analyzing the generation mechanism of these warnings and their significance in the modular system. The paper then details two primary code-level solutions: redirecting error output streams and modifying internal loggers using the sun.misc.Unsafe API. Additionally, it supplements these with an alternative approach based on Java Agent module redefinition. Each method is accompanied by complete code examples and in-depth technical analysis, helping developers understand implementation principles, applicable scenarios, and potential risks. Finally, the paper discusses practical applications in frameworks like Netty and provides best practice recommendations.
With the introduction of the module system (JPMS) in Java 9, reflective access to internal APIs has been subject to stricter restrictions. When code attempts to access non-exported packages or private members via reflection, the JVM outputs an "Illegal reflective access" warning. While these warnings do not affect program execution, they can interfere with log analysis or console output in production environments. Traditional solutions involve JVM arguments such as --illegal-access=deny, but certain scenarios require more flexible suppression strategies at the code level.
Warning Mechanism and Modular Background
Java 9's module system encapsulates JDK internal APIs within non-exported modules to enhance security and maintainability. When code accesses these APIs via reflection, the IllegalAccessLogger records and outputs warnings to the standard error stream (System.err). For example, Netty's ReflectionUtil accessing the java.nio.DirectByteBuffer constructor triggers this warning. Although current versions only output warnings, future releases may completely prohibit such access, necessitating gradual migration to public APIs.
Simple Method: Redirecting Error Stream
A straightforward suppression method involves closing the standard error stream and redirecting it to the standard output stream. This approach leverages the warning output to System.err:
public static void disableWarning() {
System.err.close();
System.setErr(System.out);
}
This method closes the error stream via System.err.close() and then redirects error output to standard output using System.setErr(System.out). Note that this merges error and output streams, potentially affecting log separation. Additionally, since IllegalAccessLogger saves a reference to the error stream early in JVM bootstrap, merely calling System.setErr may not fully redirect warning messages.
Advanced Method: Using Unsafe API
A more refined approach utilizes sun.misc.Unsafe to directly modify the internal state of IllegalAccessLogger. Although Unsafe itself is an internal API, it remains accessible without warnings in JDK 9:
public static void disableWarning() {
try {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe u = (Unsafe) theUnsafe.get(null);
Class cls = Class.forName("jdk.internal.module.IllegalAccessLogger");
Field logger = cls.getDeclaredField("logger");
u.putObjectVolatile(cls, u.staticFieldOffset(logger), null);
} catch (Exception e) {
// Ignore exception
}
}
This code first obtains an Unsafe instance via reflection, locates the IllegalAccessLogger.logger field, and sets it to null using putObjectVolatile. This disables the warning logger, preventing subsequent illegal reflective accesses from outputting warnings. Note that this method relies on undocumented JDK internal classes and may not be compatible across versions.
Supplementary Approach: Java Agent Module Redefinition
As an alternative, Java Agent can dynamically redefine modules to export or open required packages to the unnamed module. This method does not depend on stream redirection or internal APIs but requires additional Agent configuration:
void exportAndOpen(Instrumentation instrumentation) {
Set<Module> unnamed = Collections.singleton(ClassLoader.getSystemClassLoader().getUnnamedModule());
ModuleLayer.boot().modules().forEach(module -> instrumentation.redefineModule(
module,
unnamed,
module.getPackages().stream().collect(Collectors.toMap(
Function.identity(),
pkg -> unnamed
)),
module.getPackages().stream().collect(Collectors.toMap(
Function.identity(),
pkg -> unnamed
)),
Collections.emptySet(),
Collections.emptyMap()
));
}
After obtaining an Instrumentation instance via ByteBuddyAgent.install() or traditional Agent methods, calling this function opens all module packages, legalizing reflective access. This approach aligns better with modular design principles but adds deployment complexity.
Practical Applications and Best Practices
In frameworks like Netty, illegal reflective access is often used for performance optimization or backward compatibility. For instance, DirectByteBuffer reflection may involve memory management. Consider prioritizing the following steps:
- Assess Necessity: Determine if reflective access is truly required or if public API alternatives exist.
- Short-Term Suppression: Use the Unsafe method during transition periods, noting version compatibility.
- Long-Term Migration: Gradually refactor code to remove dependencies on internal APIs.
- Testing Validation: Conduct comprehensive testing in Java 9+ environments to ensure functionality remains unaffected.
In summary, while code-level suppression methods offer flexibility, developers should treat them as temporary solutions and actively adapt to the modular system to ensure long-term application stability and maintainability.