Java modules offer strong(er) encapsulation and put the onus on the Java programs to explicitly declare, at compile-time and run-time, whether they need special access to APIs (i.e., packages and their classes) hidden in some specific modules. These special accesses can be declared at both compile and run times and, of course, must be documented along with the programs. However, at run time, you may still want to hide some APIs that are not (yet) in a module, and conversely, you may still want to access hidden APIs and inform the program users that they forgot to allow this access. You can do both in pure Java by having some fun with reflection!
Context
Java introduced modules with Java 9 and the JSR-376, itself supported by several JEPs:
- JEP-200: The Modular JDK
- JEP-201: Modular Source Code
- JEP-220: Modular Run-time Images
- JEP-260: Encapsulate Most Internal APIs
- JEP-261: Module System
- JEP-282:
jlink: The Java Linker
This JSR and JEP were part of the Jigsaw project, started almost 10 years prior, in 2008. It took a long time to implement modules, mostly because it took a long time to modularise the existing JDK.
The introduction of Java modules had several goals:
- Ever-growing and Indivisible Java Runtime
- JAR/Classpath Hell
- Unexpressed Dependencies
- Transitive Dependencies
- Shadowing
- Version Collisions
- Complex Class Loading
- Weak Encapsulation Across Packages
- Manual Security
- Startup Performance
It mostly succeeded in its goals (except for version collisions, but that's a story for another time), but also brought some compatibility problems due to the modularisation of the JDK and the strong(er) encapsulation offered by modules, checked by the Java compiler and enforced by the JVM. Indeed, while the JDK is modularised, many existing Java programs are not. And some of these programs may be used to access now hidden API, either at compile or run times (or both).
Java is now promoting integrity by default, which means that third-party libraries cannot break encapsulation without the program users explicitly allowing it. To allow accessing hidden APIs at compile and run times, the Java compiler and virtual machine offer the --add-exports command-line option. This option allows declaring explicitly that some API in some module is now visible from another module. The JVM also offers the --add-opens command-line option to allow so-called "deep" reflection, i.e., calls to setAccessible(). Hence, --add-opens implies --add-exports. In Ptidej, we use --add-exports at compile time and --add-opens at run-time (see below for more explanations).
A typical use of the --add-exports option is:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<release combine.self="override" />
<source>24</source>
<target>24</target>
<compilerArgs>
<arg>-proc:none</arg>
<arg>--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED</arg>
<arg>--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg>
<arg>--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED</arg>
<arg>--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>
<arg>--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>
</compilerArgs>
</configuration>
</plugin>which tells the compiler that this specific project requires access to the classes in the (hidden) package com.sun.tools.javac.util of module jdk.compiler to compile, for example.
Another typical use of the --add-opens option is:
javaw.exe
--add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED
--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED
--add-opens=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED
--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED
--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
-classpath ...
-XX:+ShowCodeDetailsInExceptionMessages ptidej.viewer.ProjectViewerwhich tells the JVM that the program ProjectViewer (the GUI of Ptidej) requires access (including deep reflection) to the classes in the (hidden) package com.sun.tools.javac.api of module jdk.compiler to run, for example.
Modules and Ptidej
Currently, Ptidej is not modularised (yet!), although it can compile and run with Java 24 (and soon 25, released in Sep. 2025).
Besides, Ptidej uses some now-hidden APIs, such as those related to the javac compiler, the reference implementation of the Java compiler, in Java, cf. the PADL Creator JavaFile (JavaC) Parser Eclipse project.
Consequently, Ptidej needs:
- A way to protect APIs that should be hidden in modules. Such APIs are public only because they must be shared with other internal projects. They shouldn't be accessed by users directly.
- A way to inform Ptidej users that they must not forget to allow some accesses at run-time explicitly, else Ptidej would throw exceptions because it tries to access hidden APIs.
We can achieve both needs using Java reflection!
Protecting APIs Not (Yet) in Modules
The problem is: how can we identify, at run time, that a method, say padl.kernel.impl.CodeLevelModel.create(ICodeLevelModelCreator) is being called from a method that is not allowed?
The solution is: in the methods to be protected, call a method, say util.lang.ConcreteReceiverGuard.checkCallingClassName(String, String) that'll check whether their callers are allowed or not.
The next problem is: how can util.lang.ConcreteReceiverGuard.checkCallingClassName(String, String) identify the caller of a method?
The next solution is: throw and catch an exception that'll give access to the stack! 💡
The class util.lang.ConcreteReceiverGuard (available here) implement exactly this solution, shown below, a bit simplified:
public final class ConcreteReceiverGuard {
private static ConcreteReceiverGuard UniqueInstance;
public static ConcreteReceiverGuard getInstance() {
if (ConcreteReceiverGuard.UniqueInstance == null) {
ConcreteReceiverGuard.UniqueInstance = new ConcreteReceiverGuard();
}
return ConcreteReceiverGuard.UniqueInstance;
}
private ConcreteReceiverGuard() {
}
private void doCheck(
final String aConcreteReceiverClassToEnforce,
final String anErrorMessage) {
class ConcreteReceiverGuardThrownException extends RuntimeException {
private static final long serialVersionUID = -4100342857707204144L;
}
try {
throw new ConcreteReceiverGuardThrownException();
}
catch (final ConcreteReceiverGuardThrownException e) {
final StackTraceElement[] stackTrace = e.getStackTrace();
if (stackTrace.length < 4) {
// Some error message
}
else {
// Some comments
final String nameOfTheMethodDirectlyCallingTheGuard =
stackTrace[2].getMethodName();
int positionOfNextInterestingMethodCall;
for (positionOfNextInterestingMethodCall = 3; positionOfNextInterestingMethodCall < stackTrace.length
&& stackTrace[positionOfNextInterestingMethodCall]
.getMethodName()
.equals(nameOfTheMethodDirectlyCallingTheGuard); positionOfNextInterestingMethodCall++)
;
positionOfNextInterestingMethodCall--;
final StringBuffer buffer = new StringBuffer();
buffer.append(stackTrace[positionOfNextInterestingMethodCall]
.getClassName());
buffer.append('.');
buffer.append(stackTrace[positionOfNextInterestingMethodCall]
.getMethodName());
final String nameOfGuardedMethod = buffer.toString();
final String nameOfClassCallingGuardedMethod =
stackTrace[positionOfNextInterestingMethodCall + 1]
.getClassName();
boolean legit = false;
if (nameOfClassCallingGuardedMethod
.equals(aConcreteReceiverClassToEnforce)) {
legit = true;
}
if (!legit) {
// Somme friendly message
}
}
}
}
public void checkCallingClassName(
final String aConcreteReceiverClassToEnforce,
final String anErrorMessage) {
this.doCheck(aConcreteReceiverClassToEnforce, anErrorMessage);
}
public void checkCallingClassName(
final Class<?> aConcreteReceiverClassToEnforce,
final String anErrorMessage) {
this.checkCallingClassName(
aConcreteReceiverClassToEnforce.getName(),
anErrorMessage);
}
}which is called, for example, in padl.kernel.impl.CodeLevelModel.create(ICodeLevelModelCreator) as:
public void create(final ICodeLevelModelCreator aCodeLevelModelCreator)
throws CreationException {
ConcreteReceiverGuard.getInstance().checkCallingClassName(
"padl.generator.helper.ModelGenerator",
"Please use the methods in \"padl.generator.helper.ModelGenerator\" to obtain code-level models.");
aCodeLevelModelCreator.create(this);
}Accessing APIs in Modules (and Warning Users)
The problem is: how can we identify, at run time, that a project, say PADL Creator JavaFile (JavaC) has access to some hidden classes?
The solution is: in a "constructor" of the entry class of the project, say padl.creator.javafile.javac.JavaFileCreator.initialise(List, String, String[]), call a method, say util.lang.OpenedModulesGuard.checkOpenedModules(), that'll check whether the hidden classes are available.
The next problem is: how can util.lang.OpenedModulesGuard.checkOpenedModules() checks whether a hidden class is available?
The next solution is: attempt to make accessible the default, private constructor of such a class! 💡
The class util.lang.OpenedModulesGuard (available here) implement exactly this solution, shown below, a bit simplified:
public class OpenedModulesGuard {
private static OpenedModulesGuard UNIQUE_INSTANCE;
public static OpenedModulesGuard getInstance() {
if (OpenedModulesGuard.UNIQUE_INSTANCE == null) {
OpenedModulesGuard.UNIQUE_INSTANCE = new OpenedModulesGuard();
}
return OpenedModulesGuard.UNIQUE_INSTANCE;
}
private List<PackageInfo> list = new ArrayList<>();
private OpenedModulesGuard() {
}
public void addOpenedModuleCheck(final String aModuleName,
final String aClassName) {
this.list.add(new PackageInfo(aModuleName, aClassName));
}
public Optional<String> checkOpenedModules() {
for (final PackageInfo packageInfo : this.list) {
try {
final Class<?> clazz = Class.forName(packageInfo.className);
final Constructor<?>[] constructors = clazz
.getDeclaredConstructors();
final Constructor<?> constructor = constructors[0];
constructor.setAccessible(true);
}
catch (final ClassNotFoundException
| InaccessibleObjectException e) {
return Optional.of("Module " + packageInfo.moduleName
+ " does not opens " + packageInfo.packageName
+ "\nDid you forget to add \"--add-opens="
+ packageInfo.moduleName + '/' + packageInfo.packageName
+ "=ALL-UNNAMED\" in Maven, run configuration, or command line?");
}
}
return Optional.empty();
}
}which is called, for example, in padl.creator.javafile.javac.JavaFileCreator.initialise(List, String, String[]) as:
private void initialise(final List<String> options,
final String aSourcePath, final String[] someFilesInThePath) {
// Some other code
OpenedModulesGuard.getInstance().addOpenedModuleCheck("jdk.compiler",
"com.sun.tools.javac.api.JavacTool");
OpenedModulesGuard.getInstance().addOpenedModuleCheck("jdk.compiler",
"com.sun.tools.javac.code.Symbol");
OpenedModulesGuard.getInstance().addOpenedModuleCheck("jdk.compiler",
"com.sun.tools.javac.model.JavacElements");
OpenedModulesGuard.getInstance().addOpenedModuleCheck("jdk.compiler",
"com.sun.tools.javac.tree.JCTree");
OpenedModulesGuard.getInstance().addOpenedModuleCheck("jdk.compiler",
"com.sun.tools.javac.util.Pair");
final Optional<String> check = OpenedModulesGuard.getInstance()
.checkOpenedModules();
if (check.isPresent()) {
throw new RuntimeException(check.get());
}
}As shown in the code, we use deep reflection, i.e., setAccessible() to check if the default constructor of some class is available, hence we must use --add-opens. We also must use --add-opens with the serialisers, e.g., PADL Serialiser DB4O, because, intrinsically, they need reflective access to private members, including of Java base classes like HashSet, for example. This need to use explicitly --add-opens=java.base/java.util=ALL-UNNAMED illustrates well that the user is in control and that a library, e.g., DB4O, cannot break encapsulation on its own.
Conclusion
As usual in software engineering, we've added one level of indirection to allow more flexibility. And as often in Java, we've used reflection to perform interesting computations at run-time.
In this blog, we used reflection to warn users that they are calling a method that should be hidden and is public only because not yet in a module. We also used reflection to check whether users exported/opened the API hidden in modules but required by Ptidej at run-time.