🔴 The Error You're Seeing
Confirm this matches your console output. If it does, you're in the right place.
// CAPTURED — OpenJDK 25 Temurin: Field.setAccessible(true) on String.value from classpath code
Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make field private final byte[] java.lang.String.value accessible: module java.base does not "opens java.lang" to unnamed module @2b2fa4f7
at java.base/java.lang.reflect.AccessibleObject.throwInaccessibleObjectException(AccessibleObject.java:353)
at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:329)
at java.base/java.lang.reflect.Field.setAccessible(Field.java:194)⚡ Quick Fix Works 80% of the time
Add the precise open the message asks for — module, package, and target — to your JVM flags, wired into build tools and containers alike.
# From the message: module java.base, package java.lang
java --add-opens java.base/java.lang=ALL-UNNAMED -jar app.jar
# Maven surefire/failsafe:
<argLine>--add-opens java.base/java.lang=ALL-UNNAMED</argLine>
# Gradle:
tasks.test { jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED") }
# Dockerfile:
ENTRYPOINT ["java", "--add-opens", "java.base/java.lang=ALL-UNNAMED", "-jar", "app.jar"]🧠 Why this Happens
Tap to expand the deep technical explanation
Since modules arrived, setAccessible(true) performs a second check beyond modifiers: the DECLARING class's module must open the containing package to the caller's module. The message decodes mechanically — Unable to make FIELD X accessible names the member, module java.base does not "opens java.lang" identifies the refusing side, and unnamed module @hash is classpath code (the hex suffix is its identity hash). JEP 396 made strong encapsulation the default in JDK 16 and JEP 403 removed the last permit mode in 17, so libraries deep-reflecting into JDK internals began throwing this the moment runtimes crossed 17+.
The HITEC City Parking Spot Analogy:
Hotel keycards: housekeeping's master key used to open every floor. The executive floor switched to per-guest authorization — now the front desk must explicitly register cleaning staff (--add-opens) before that floor unlocks.
🔁 How to Reproduce Confirm this is your error
Field f = String.class.getDeclaredField("value"); f.setAccessible(true); from any plain classpath application on JDK 17+. (Lab capture: OpenJDK 25.)
🛠️ Solutions (5 Ways to Fix)
Grant the specific --add-opens named by the message
👉 Use this when/if a third-party library you cannot patch needs deep reflection into a named module.
Read the message left to right: opens PACKAGE goes FROM module TO target. Target ALL-UNNAMED covers all classpath code. Add the exact flag to every runtime surface — application JVM args, test forks via argLine/jvmArgs, container ENTRYPOINT — because each JVM process needs its own grant.
java --add-opens java.base/java.lang=ALL-UNNAMED \
--add-opens java.base/java.util=ALL-UNNAMED \
-jar app.jarUpgrade the offending library to a JPMS-clean release
👉 Use this when/if the throwing dependency has a newer version advertising Java 17+ support.
Flags are debt with interest — every flag weakens encapsulation forever. Modern releases of serializers, ORMs, and mocking libraries replaced deep reflection with supported APIs, deleting the need for the open entirely.
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.18.x</version> <!-- JPMS-clean: no add-opens needed -->
</dependency>Remove the hack from YOUR code with supported APIs
👉 Use this when/if the stack trace lands in your own util classes doing String.value or Unsafe-style tricks.
Reading String internals, mutating final fields, and sun.misc.Unsafe offsets were always unsupported. VarHandles give safe field manipulation, MemorySegment (FFM, final in 22) covers off-heap access, and records expose components officially.
VarHandle VALUE = MethodHandles.privateLookupIn(String.class,
MethodHandles.lookup())
.findVarHandle(String.class, "value", byte[].class); // still needs the open
// Better: redesign so String internals stay String's business.Scan with jdeps BEFORE upgrading JDKs
👉 Use this when/if a runtime bump is scheduled and you want the opens list before production finds it.
jdeps --jdk-internals lists every internal-API usage across your jars, giving the complete --add-opens inventory plus upgrade targets. Pair it with a CI job running the newest LTS so stragglers surface months early, not during an incident.
jdeps --jdk-internals libs/*.jar
# javax.crypto -> replace usage | sun.misc.Unsafe -> VarHandle/FFM
# Collect flags into one reviewed file: run-args.add-opens.txtDEV ONLY: blanket opens while diagnosing, then narrow
👉 Use this when/if a legacy app throws dozens of distinct opens during initial Java 21 migration triage.
Temporarily opening many packages gets a migration branch running so you can enumerate what ACTUALLY needs opening. This weakens encapsulation broadly and masks design problems — treat strictly as scaffolding and remove before merging.
# DEV ONLY — diagnosis scaffolding, never ship this line
java --add-opens java.base/java.lang=ALL-UNNAMED \
--add-opens java.base/java.util=ALL-UNNAMED \
--add-opens java.base/java.lang.invoke=ALL-UNNAMED -jar app.jar
# Then bisect down to the minimal set and document owners per flag.📋 Version Notes
No modules — this exception does not exist; everything accessible.
Modules land; illegal deep reflection allowed with a warning (--illegal-access=permit).
Strong encapsulation becomes default (JEP 396): warnings become InaccessibleObjectException.
Permit mode deleted (JEP 403): --add-opens is the only door, unchanged through Java 21.
🛡️ How to Prevent This Next Time
Track every --add-opens flag as technical debt with an owner and expiry note, run CI on the newest LTS early, prefer libraries publishing JPMS-clean releases, and gate builds with jdeps/archunit checks that forbid internal-API imports.