skip to content

Explain the JPMS escape-hatch flags --add-exports, --add-opens, --add-modules, and --add-reads. When is each needed during migration?

level: principalimportance: should knowfreq 44%

answer

  1. exports=use public API; opens=deep reflection
  2. add-modules=force resolution (e.g. removed java.xml.bind)
  3. add-reads=ad-hoc requires edge
  4. ALL-UNNAMED targets classpath code
  5. Manifest Add-Opens/Add-Exports; JDK_JAVA_OPTIONS; argfile
  6. Bridges, not architecture — review every upgrade

basics

~20 s

These command-line flags relax module rules without editing module-info. --add-exports lets you use a package that isn't exported; --add-opens grants deep reflection into a package; --add-modules forces a module into the graph even if nothing requires it; --add-reads lets one module read another it didn't declare. They are temporary bridges during migration.

solid answer

~50 s

JPMS provides four key escape hatches to relax encapsulation and resolution without changing module descriptors. --add-exports <module>/<package>=<target> makes a non-exported package's public types accessible (compile and run) — for using internal or unexported API. --add-opens <module>/<package>=<target> additionally grants deep reflective access (setAccessible on private members) — frameworks doing serialization, DI, or mocking need this. --add-modules <module> adds a module to the root set so it is resolved even when no requires references it — needed for modules removed from the default set (like the old java.xml.bind / Java EE modules dropped in Java 11) or service providers discovered only at runtime. --add-reads <source>=<target> adds a readability edge so source can read target without a requires clause. Use ALL-UNNAMED as a target to grant access to classpath code, ALL-SYSTEM where applicable. They can live on the command line, in JDK_JAVA_OPTIONS, or jar manifest entries (Add-Exports/Add-Opens). Treat all four as transitional bridges, not permanent architecture, and prefer real module-info declarations or supported APIs once available.

go deeper

for a junior

Recognizes that these flags relax module restrictions and can keep an app running after an upgrade.

for a middle

Correctly maps each flag to its purpose and applies --add-opens for a reflection failure or --add-modules for a missing JDK module.

for a senior

Distinguishes the four precisely, knows the java.xml.bind removal story, and uses manifest entries / JDK_JAVA_OPTIONS appropriately.

for a principal

Governs escape-hatch usage as technical debt: tracks each flag to a cause, prevents flag rot, drives descriptor/API fixes, and sets upgrade-time review policy across the org.

## Why escape hatches exist JPMS enforces **strong encapsulation** (only exported packages are visible; only opened packages allow deep reflection) and **reliable configuration** (only resolved modules are present, the graph is acyclic). During migration, real-world code and frameworks frequently need to break these rules *temporarily* — before every dependency has a correct `module-info`. The escape-hatch flags relax specific rules **without editing any module descriptor**, which is essential when you do not control the offending module. Each flag's target can be a specific module name, the special **`ALL-UNNAMED`** (all classpath code in the unnamed module), and for reads, **`ALL-SYSTEM`** etc. ## --add-exports <module>/<package>=<target> - **What it does:** makes the **public** types of a *non-exported* package readable by the target, at **compile and run** time. - **When:** you must call into a package a module does not `exports` — e.g. a JDK-internal `sun.*` package, or another module's implementation package. - **Limitation:** grants access to public members only; it does **not** enable reflection on private members. ## --add-opens <module>/<package>=<target> - **What it does:** **opens** a package to the target for **deep reflection** — `setAccessible(true)` on non-public fields/methods/constructors. - **When:** serialization libraries, dependency-injection containers, ORMs, mocking frameworks that reflect into private members of standard or third-party modules. Since Java 16/17 default-deny, these now fail loudly without it. - **Relation to exports:** opens is strictly about reflection; a package can be opened without being exported and vice versa. ## --add-modules <module> - **What it does:** adds the named module(s) to the **root set** so they are **resolved** even though no `requires` (or the default root computation) pulls them in. - **When:** - Modules **removed from the default module set** — e.g. the Java EE / CORBA modules (`java.xml.bind`, `java.activation`, `java.xml.ws`, …) deprecated in Java 9 and **removed in Java 11**; if you still need them as JDK modules (pre-removal) you had to add them. Post-removal you switch to standalone artifacts. - **Service providers** only discovered at runtime that you want present. - `--add-modules ALL-MODULE-PATH` to resolve every module on the path. ## --add-reads <source-module>=<target-module> - **What it does:** adds a **readability edge** so `source` can read `target` without declaring `requires`. - **When:** white-box testing (test code reading a module it shouldn't), or quickly wiring a dependency you can't yet add to `module-info`. Often paired with `--patch-module` in testing. ## Where the flags can live - Directly on `java`/`javac` command lines. - In environment variables **`JDK_JAVA_OPTIONS`** (java) / `JAVA_TOOL_OPTIONS`. - In the **jar manifest** as `Add-Opens:` / `Add-Exports:` entries, so a library can self-declare what it needs (e.g. an agent jar). - In an `@argfile`. ## Discipline: bridges, not architecture The danger is **flag rot**: escape hatches silently pile up in launch scripts and never get removed, so the app effectively runs with encapsulation disabled. Principal-level practice: - Track each flag to a **specific dependency and reason**; review them every upgrade. - Prefer the **durable fix**: add proper `exports`/`opens`/`requires` to descriptors you own, migrate off internal APIs (`jdeps --jdkinternals`), or adopt libraries that declare what they need in their manifest. - Centralize and document them rather than scattering across scripts. ## Key takeaways - **exports** = use a package's public API; **opens** = deep reflection; **add-modules** = force a module to resolve; **add-reads** = add a requires edge ad hoc. - `ALL-UNNAMED` targets classpath code. - All four are migration **bridges**; the endpoint is correct descriptors and supported APIs.

  • You upgraded to Java 11 and a library that used JAXB now throws ClassNotFoundException for javax.xml.bind. What happened and how do you fix it?
    The Java EE modules (java.xml.bind, java.activation, etc.) were deprecated in Java 9 and removed from the JDK in Java 11, so they are no longer resolvable. The durable fix is to add the standalone Jakarta JAXB API + implementation jars as normal dependencies; --add-modules no longer helps because the JDK module is gone.
  • Why might --add-exports be insufficient even though it grants access to a package?
    --add-exports only exposes public types. If the code reflects into private fields/methods (setAccessible), you also need --add-opens, which grants deep reflective access; exporting alone does not permit reflection on non-public members.

saying these in an interview costs you the question

  • Treating --add-exports and --add-opens as interchangeable
  • Thinking --add-modules can resurrect java.xml.bind in Java 11+ (it was removed, not just unresolved)
  • Leaving escape-hatch flags in launch scripts indefinitely without review
  • Believing manifest Add-Opens is unavailable and flags must always be CLI
  • Forgetting ALL-UNNAMED is needed to reach classpath code

context