JPMS Package Access Boundary Under JDK 16: Exporting Library API Without Opening Packages to Reflection
0 reputation · 19 Jan 2026, 02:11 UTC
0 reputation · 19 Jan 2026, 02:11 UTC
A library targeting the module path must declare its package boundaries explicitly in module-info.java. With the JDK 16 strong-encapsulation default introduced by JEP 396, reflective access to internal JDK packages is denied unless explicitly opened, and the same boundary logic applies to a library's own packages: public alone does not make a type accessible. A package must be exportsed for other named modules to compile against its types, while opens or an open module is required for deep reflective access such as setAccessible.
The unresolved decision is whether a library should exports a package for API consumers and separately opens it for frameworks, or keep the package closed and expose only interfaces. Exporting alone leaves reflection blocked, but opening a package also exposes all members to the unnamed module if unqualified. Qualified directives do not apply to classpath consumers, so the descriptor choice becomes a compatibility contract that differs between module-path and classpath deployment.
When a library needs both compile-time API visibility and framework-driven reflection on the same package, what is the most conservative descriptor strategy that avoids accidental exposure while remaining usable on the classpath? Should the package be exported and opened together, or should reflective access be isolated to a separate internal package? How does this choice change when the library must also support consumers on the classpath under JDK 16 and later?
35275 reputation · 19 Jan 2026, 04:41 UTC
Do not export and open the same package. Keep the compile-time API in an unqualified exports package, and move framework-mutable or reflective types into a separate non-exported internal package. Open that internal package only to the specific framework modules that need deep reflection. If classpath consumers must reflect without command-line flags, the only descriptor-level fallback is an unqualified opens on a minimal internal package; treat that as a last resort.
An unqualified exports com.acme.api; gives named-module consumers compile-time and normal runtime access to public API types, and it also works for classpath consumers because the unnamed module can read exported packages of modules on the module path. Putting implementation classes in the same exported package turns them into accidental API: consumers compile against them, and later encapsulation changes become breaking.
Deep reflection—setAccessible on non-public members, field injection, or annotation scanning—does not follow exports. Public members of an exported package remain reflectively invocable, but frameworks that force access to private state require an opens directive. A qualified opens com.acme.internal to framework.module; grants that only to the named framework module, keeping the boundary narrow.
Qualified directives name specific modules. Classpath consumers live in the unnamed module, which has no name, so exports ... to and opens ... to never apply to them. For classpath compatibility, API packages must be exported unqualified. If those consumers also need deep reflection into the library, the descriptor options are limited:
--add-opens flag; this keeps the library descriptor conservative.opens com.acme.internal; on a minimal internal package only when reflection from the classpath is unavoidable. This exposes every member of that package to all named modules and the unnamed module.An open module is not a conservative choice: it opens every package to every module, including the unnamed module.
When the modular JAR is placed on the classpath, its module-info is ignored and the classes join the unnamed module. Pre-JPMS reflection into the library works in that deployment, but JDK 16+ strong encapsulation of JDK internals still applies. Do not use classpath behavior to justify a module-path boundary.
jar --describe-module --file library.jar and confirm the exports and opens directives match the intended boundary.requires the library and calls only exported types; run it on the module path to prove API visibility without opening the API package.InaccessibleObjectException until the qualified opens is added.opens do not apply to the unnamed module.JDK 16 is the version-sensitive part because JEP 396 made strong encapsulation of JDK internals the default. The library's own exports and opens semantics are stable JPMS behavior, but verify the exact framework module names and probe results on each JDK release you support.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.