JAVA / PLATFORM, BUILDS AND TESTING
Modules, requires and the encapsulated JDK
Declare a module with requires and exports, predict when access is refused, and unblock JDK internals with --add-exports or --add-opens.
What you will learn
- Write module-info.java with the right requires and exports lines for a small library
- Tell readability (requires) apart from accessibility (exports/opens) when access fails
- Inspect the resolved module graph at runtime with getModule, isExported and getDescriptor
- Unblock JDK internals with --add-exports or --add-opens module/package=ALL-UNNAMED
Understanding Modules, requires and the encapsulated JDK
A module is a jar or a directory with a name and one extra file at its root, module-info.class, compiled from module-info.java. That declaration lists requires, the other modules this one is allowed to read, and exports, the packages this one lets outsiders compile and link against. The two keywords answer different questions, and both must say yes before public even matters: does my module read yours, and does yours export the exact package the class sits in? A package you never export is invisible from outside no matter how public its classes are.
Resolution happens before main runs. The launcher starts from the root module, walks requires edges across the module path, and fails immediately if a required module is missing or if two modules on the path contain the same package. You never write requires java.base; the compiler inserts it. Write requires transitive when a dependency shows up in your own signatures, the way java.sql does for java.xml because some of its methods hand back XML types, and requires static when a dependency is needed to compile but may be absent at runtime.
The classpath did not disappear. Classes loaded from it belong to the unnamed module, which reads every resolved module and exports all of its own packages, which is why non-modular code still compiles and runs unchanged. What changed for everyone is the JDK itself: jdk.internal.misc, sun.security.util and most of com.sun are neither exported nor open, so setAccessible against them throws InaccessibleObjectException from JDK 16 onward instead of printing the warning it did in 9 through 15. sun.misc.Unsafe is the famous survivor, exported on purpose by the jdk.unsupported module; everything else needs --add-exports to compile and link or --add-opens for deep reflection, which puts the decision in the hands of whoever launches the application rather than the library.
import java.lang.reflect.Field;
import java.lang.reflect.InaccessibleObjectException;
public class ModuleProbe {
public static void main(String[] args) throws NoSuchFieldException {
Module base = String.class.getModule();
Module self = ModuleProbe.class.getModule();
System.out.println("String's module: " + base.getName());
System.out.println("our module: " + self.getName() + " (named=" + self.isNamed() + ")");
System.out.println("we read java.base: " + self.canRead(base));
System.out.println("java.lang exported: " + base.isExported("java.lang"));
System.out.println("jdk.internal.misc exported: " + base.isExported("jdk.internal.misc"));
Field value = String.class.getDeclaredField("value");
try {
value.setAccessible(true);
System.out.println("String.value opened");
} catch (InaccessibleObjectException e) {
System.out.println("String.value: " + e.getClass().getSimpleName());
}
}
}Access in a modular JVM needs two permissions your code cannot grant itself: a read edge created by requires, and an export or opens of the exact package from the other side.
Worked examples
Encapsulation cannot be lifted from inside the code
Shows that reading a module is not the same as being granted access, and that a package can only be opened by its own module or by the launcher.
public class OpenFromInside {
public static void main(String[] args) {
Module base = String.class.getModule();
Module here = OpenFromInside.class.getModule();
System.out.println("canRead java.base: " + here.canRead(base));
System.out.println("isExported jdk.internal.misc to us: " + base.isExported("jdk.internal.misc", here));
System.out.println("isOpen java.lang to us: " + base.isOpen("java.lang", here));
try {
base.addOpens("java.lang", here);
System.out.println("we opened it ourselves");
} catch (IllegalCallerException e) {
System.out.println("addOpens refused: " + e.getClass().getSimpleName());
}
}
}Example explained
Line 1canRead is true because classpath code lives in the unnamed module, and the unnamed module reads every resolved module.
Line 2isExported is false for jdk.internal.misc, so the read edge buys nothing: java.base hands that package only to a fixed list of JDK modules.
Line 3isOpen is false for java.lang, which is exported but never opened, so deep reflection into it is refused.
Line 4addOpens throws IllegalCallerException because only java.base could open one of its own packages, which is exactly why --add-opens has to be a launcher flag.
Reading requires and requires transitive at runtime
Prints the resolved dependence list of the java.sql module on JDK 17 to show what mandated and transitive edges look like.
public class SqlRequires {
public static void main(String[] args) {
Module sql = ModuleLayer.boot().findModule("java.sql").orElseThrow();
Module xml = ModuleLayer.boot().findModule("java.xml").orElseThrow();
sql.getDescriptor().requires().stream()
.map(r -> r.name() + " " + r.modifiers())
.sorted()
.forEach(System.out::println);
System.out.println("java.sql reads java.xml: " + sql.canRead(xml));
}
}Example explained
Line 1ModuleLayer.boot() is the graph the launcher resolved at startup, and findModule succeeds even for classpath code because java.se modules are resolved by default.
Line 2java.base carries MANDATED because the compiler writes that dependence into every module declaration for you.
Line 3The TRANSITIVE entries are implied readability: any module that requires java.sql also reads java.xml and java.logging, since java.sql's own API mentions their types.
Line 4canRead answers a question about the resolved graph, the same information java --describe-module java.sql prints from the command line.
Important notes
isExported with a single argument means exported unconditionally, so a qualified export such as exports jdk.internal.misc to java.management reports false, which is why the probe printed false for a package some JDK modules may legally use.
Flag position matters: --add-opens java.base/java.lang=ALL-UNNAMED must appear before -jar or the main class name, otherwise the JVM hands it to your program as an ordinary argument and the access stays blocked.
Common mistakes
Writing a package name after requires, such as requires java.sql.DriverManager or requires java.util: javac stops with a module-not-found error because requires only accepts module names, and java.util is not a module at all.
Adding module-info.java while leaving third-party jars on -cp: a named module cannot read the unnamed module, so every one of those imports fails with package is not visible even though the jar is right there on the classpath.
Assuming exports is enough for a reflective library: the build succeeds and the app starts, then dies with InaccessibleObjectException the moment the serialiser touches a private field, because the package needed opens rather than exports.
Try it yourself
Change, predict, then run
Write a single-file program that prints java.base's package count from getDescriptor().packages().size(), then counts how many of those names return true for isExported(name) and prints three names that return false.
Open the Java workspaceCheck your understanding
A library on the module path serialises your class com.acme.api.User by reflection. Your module declares exports com.acme.api; and the run still fails with InaccessibleObjectException on a private field. What is missing?
- requires com.acme in the library's module declaration, so it can read your module
- exports applies only at compile time, so the run also needs --add-exports
- opens com.acme.api (or --add-opens), because exports does not permit setAccessible on non-public members
- a public getter for each private field, since reflection cannot see fields inside a named module
Show answer
exports satisfies the accessibility check for public API, so the library can already call public methods on User; setAccessible on a private field is deep reflection and additionally requires the package to be open, either through opens com.acme.api; in module-info.java or --add-opens at launch. The first option is tempting because readability is the other half of module access, but a serialiser never names your types at compile time, and a missing read edge shows up as a not-visible or class-not-found failure rather than InaccessibleObjectException.