🔴 The Error You're Seeing
Confirm this matches your console output. If it does, you're in the right place.
// DOC-DERIVED — wording stable across modern JDKs; names vary with your code
Exception in thread "main" java.lang.IllegalAccessException: class com.devinhyderabad.ReflectiveClient can not access a member of class com.devinhyderabad.SecretService with modifiers "private"
at java.base/java.lang.reflect.AccessibleObject.slowCheckMemberAccess(AccessibleObject.java:625)
at java.base/java.lang.reflect.Field.get(Field.java:430)
at com.devinhyderabad.ReflectiveClient.main(ReflectiveClient.java:9)⚡ Quick Fix Works 80% of the time
Call setAccessible(true) on the Field or Method object right after lookup — then get/set/invoke succeed.
Field token = SecretService.class.getDeclaredField("token");
token.setAccessible(true); // needs an open package under JPMS
Object value = token.get(service);🧠 Why this Happens
Tap to expand the deep technical explanation
Reflection performs the same access check the compiler would: the JVM compares the caller class recorded at the reflection site against the member's modifiers (private, protected, package-private) and the module exports of both sides. Failing that comparison throws IllegalAccessException naming exactly who tried, which member, and which modifiers blocked it — that triple is the whole diagnosis. Nested classes compiled together share private access through nestmate records, which is why the same read succeeds from inside the enclosing class but fails from another top-level class.
The HITEC City Parking Spot Analogy:
You found the office door — it exists — but your badge does not open it. setAccessible(true) asks security for an override pass; since Java 16 security only issues passes for buildings your module may enter.
🔁 How to Reproduce Confirm this is your error
Put SecretService with a private String token in its own compilation unit, then read SecretService.class.getDeclaredField("token").get(instance) from ReflectiveClient without setAccessible. Trap: doing it from a nested class works because nestmates share private access.
🛠️ Solutions (5 Ways to Fix)
Call setAccessible(true) immediately after lookup
👉 Use this when/if your own framework, mapper, or test legitimately needs non-public access to classes you ship.
setAccessible marks the member accessible so later get/set/invoke skip the language-level check. Under JPMS it additionally requires the declaring package to be open to your module — otherwise you graduate to InaccessibleObjectException. Never aim it at libraries you do not control.
Field token = SecretService.class.getDeclaredField("token");
token.setAccessible(true);
String value = (String) token.get(service);Add the missing public API instead of reflecting
👉 Use this when/if you own SecretService and reflection was papering over a design gap.
If production code needs the value, the class is telling you its API is incomplete. A getter, builder, or explicit data carrier removes the reflective hop, restores compile-time checking, and makes the access visible in reviews.
public final class SecretService {
private String token;
public String token() { return token; } // boring beats clever
}Place tests in the same package — zero reflection
👉 Use this when/if the only caller is a unit test poking package-private internals.
The long-standing Java convention of mirroring packages under src/test/java gives the test genuine package-level access. No setAccessible, no module opens, and refactors keep compiling because the compiler sees the access like any other.
src/main/java/com/devinhyderabad/SecretService.java
src/test/java/com/devinhyderabad/SecretServiceTest.java // same package = legal accessExpose an SPI interface for plugin boundaries
👉 Use this when/if external modules load code into your process and currently reflect onto your internals.
Plugins should implement an interface you designed, not reach into your classes. Reflection then touches only public interface methods, internals stay private for real, and the IllegalAccessException disappears because the access is legitimate.
public interface SecretPlugin extends Plugin {
String readToken(); // the only thing plugins may call
}
// loader side: clazz.getDeclaredConstructor().newInstance() typed as SecretPluginUse VarHandles / MethodHandles.Lookup for repeated access
👉 Use this when/if the same field is read or written in tight loops and Field.get overhead shows up in profiles.
A plain lookup() carries only your own caller's access rights, so it can never bind a private field of another class — first escalate with privateLookupIn(SecretService.class, lookup()) to get a Lookup holding private credentials. findVarHandle then binds access ONCE at creation, returning a VarHandle whose get/set skips per-call checks and inlines well under JIT. Classpath app classes need no module flags; cross-module targets still require the open.
VarHandle TOKEN = MethodHandles.privateLookupIn(SecretService.class,
MethodHandles.lookup())
.findVarHandle(SecretService.class, "token", String.class);
String v = (String) TOKEN.get(service);📋 Version Notes
setAccessible(true) succeeds everywhere; SecurityManager policies are the only brake.
JPMS lands: cross-module reflective access needs opened packages even after setAccessible.
Strong encapsulation becomes the default (JEP 396) — deep reflection into JDK internals fails loudly.
JEP 403 removes the permit path entirely; --add-opens is the only door.
🛡️ How to Prevent This Next Time
Keep reflective access pointed at your own packages only, centralize setAccessible calls in one audited place, prefer interfaces, accessors, and records over field reflection, and give tests same-package placement instead of reflective hacks.