Partner – Microsoft – NPI (cat=Java)
announcement - icon

Microsoft JDConf 2024 conference is getting closer, on March 27th and 28th. Simply put, it's a free virtual event to learn about the newest developments in Java, Cloud, and AI.

Josh Long and Mark Heckler are kicking things off in the keynote, so it's definitely going to be both highly useful and quite practical.

This year’s theme is focused on developer productivity and how these technologies transform how we work, build, integrate, and modernize applications.

For the full conference agenda and speaker lineup, you can explore JDConf.com:

>> RSVP Now

1. Overview

Before Java 9, the Java Reflection API has a superpower: It could gain access to the non-public class members without limitation. After Java 9, the modular system wants to limit the Reflection API to a reasonable extent.

In this tutorial, we’ll inspect the relationship between the module system and reflection.

2. Modular System and Reflection

Even though reflection and the module system make their appearance at different times in Java’s history, they need to work together to build a reliable platform.

2.1. The Underlying Model

One of the goals of the Java module system is strong encapsulation. The strong encapsulation mainly consists of readability and accessibility:

  • The readability of modules is a coarse concept and concerns whether one module has a dependency on another module.
  • The accessibility of modules is a finer concept and cares if one class can access another class’s field or method. It is provided by class boundary, package boundary, and module boundary.
j1

The relationship between these two rules is that readability comes first, and accessibility builds upon readability. For example, if a class is public but not exported, the readability will prevent further use. And, if a non-public class is in an exported package, the readability will allow its passing, but the accessibility will reject it.

To increase the readability, we can use the “requires” directive in the module declaration, specify the “–add-reads” option on the command line, or invoke the Module.addReads method. In the same way, to break the boundaries encapsulation, we can use the “opens” directive in the module declaration, specify the “–add-opens” option on the command line, or invoke the Module.addOpens method.

Even reflection can’t break the readability and accessibility rules; otherwise, it will lead to corresponding errors or warnings. One thing to note: When using reflection, the runtime will automatically set up a readability edge between two modules. That also implies that if something goes wrong, it’s because of accessibility.

2.2. Different Reflection Use Cases

In the Java module system, there are different module types, for example, named module, unnamed module, platform/system module, application module, and so on:

j2

To be clear, the two concepts “module system” and “system module” may sound confusing. So, let’s use the “platform module” concept instead of the “system module”.

Considering the above module types, there exist quite a few combinations between different module types. Generally, an unnamed module can’t be read by named modules except for automatic modules. Let’s only inspect three typical scenarios where illegal reflective access happens:

j3

In the above picture, deep reflection means using the Reflection API to get access to non-public members of a class by invoking the setAccessible(flag) method. When using reflection to access a named module from another named module, we’ll get an IllegalAccessException or InaccessibleObjectException. Similarly, when using reflection to access an application named module from an unnamed module, we get the same errors.

However, when using reflection to access the platform module from an unnamed module, we’ll get an IllegalAccessException or a warning. And the warning message is useful to help us to find where the problem happens and to make further remedies:

WARNING: Illegal reflective access by $PERPETRATOR to $VICTIM

In the above warning message form, the $PERPETRATOR represents the reflecting class information and the $VICTIM represents the reflected class information. And, this message is attributed to the relaxed strong encapsulation.

2.3. Relaxed Strong Encapsulation

Before Java 9, many third-party libraries utilize the reflection API to do their magic work. However, the strong encapsulation rules of the module system would invalidate most of that code, especially those using deep reflections to access JDK internal APIs. That would be undesirable. For a smooth migration from Java 8 to Java 9’s modular system, a compromise is made: relaxed strong encapsulation.

The relaxed strong encapsulation provides a launcher option –illegal-access to control the runtime behavior. We should note that the –illegal-access option only works when we use reflection to access platform modules from unnamed modules. Otherwise, this option has no effect.

The –illegal-access option has four concrete values:

  • permit: opens each package of platform modules to unnamed modules and shows a warning message only once
  • warn: is identical to “permit“, but shows a warning message per illegal reflective access operation
  • debug: is identical to “warn“, and also prints the corresponding stack trace
  • deny: disables all illegal reflective access operations

From Java 9, the –illegal-access=permit is the default mode. To use other modes, we can specify this option on the command line:

java --illegal-access=deny com.baeldung.module.unnamed.Main

In Java 16, the –illegal-access=deny becomes the default mode. Since Java 17, the –illegal-access option is entirely removed.

3. How to Fix Reflection Illegal Access

In the Java module system, a package needs to be open to allow deep reflection.

3.1. In Module Declaration

If we’re the code author, we can open the package in the module-info.java:

module baeldung.reflected {
    opens com.baeldung.reflected.opened;
}

To be more cautious, we can use the qualified opens:

module baeldung.reflected {
    opens com.baeldung.reflected.internal to baeldung.intermedium;
}

When migrating our existing code to the modular system, for convenience, we can open the whole module:

open module baeldung.reflected {
    // don't use opens directive
}

We should note that an open module doesn’t allow inner opens directives.

3.2. On the Command-Line

If we’re not the code author, we can use the –add-opens option on the command line:

--add-opens java.base/java.lang=baeldung.reflecting.named

And, to add opens to all unnamed modules, we can use the ALL-UNNAMED:

java --add-opens java.base/java.lang=ALL-UNNAMED

3.3. At Runtime

To add opens at runtime, we can use the Module.addOpens method:

srcModule.addOpens("com.baeldung.reflected.internal", targetModule);

In the above code snippet, the srcModule opens the “com.baeldung.reflected.internal” package to the targetModule.

One thing to note: the Module.addOpens method is caller-sensitive. This method will succeed only when we call it from the module being modified, from the modules it has granted open access to, or from the unnamed module. Otherwise, it will lead to an IllegalCallerException.

Another way to add opens to the target module is using the Java agent. In the java.instrument module, the Instrumentation class has added a new redefineModule method since Java 9. This method can be used to add extra reads, exports, opens, uses, and provides:

void redefineModule(Instrumentation inst, Module src, Module target) {
    // prepare extra reads
    Set<Module> extraReads = Collections.singleton(target);

    // prepare extra exports
    Set<String> packages = src.getPackages();
    Map<String, Set<Module>> extraExports = new HashMap<>();
    for (String pkg : packages) {
        extraExports.put(pkg, extraReads);
    }

    // prepare extra opens
    Map<String, Set<Module>> extraOpens = new HashMap<>();
    for (String pkg : packages) {
        extraOpens.put(pkg, extraReads);
    }

    // prepare extra uses
    Set<Class<?>> extraUses = Collections.emptySet();

    // prepare extra provides
    Map<Class<?>, List<Class<?>>> extraProvides = Collections.emptyMap();

    // redefine module
    inst.redefineModule(src, extraReads, extraExports, extraOpens, extraUses, extraProvides);
}

In the above code, we first utilize the target module to construct the extraReads, extraExports, and extraOpens variables. Then, we invoke the Instrumentation.redefineModule method. As a result, the src module will be accessible to the target module.

4. Conclusion

In this tutorial, we first introduced the readability and accessibility of the module system. Then, we looked at different illegal reflective access use cases and how relaxed strong encapsulation helps us to migrate from Java 8 to the Java 9 module system. Finally, we provided different approaches to solve the illegal reflective access.

As usual, the source code for this tutorial can be found over on GitHub.

Course – LS (cat=Java)

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
2 Comments
Oldest
Newest
Inline Feedbacks
View all comments
Comments are closed on this article!