setAccessible 访问私有成员
Java 的封装机制用 private 关键字筑起了一道墙:private 成员只能在同一个类内部访问。
但反射开了个后门。
setAccessible(true) 就是这把钥匙。
为什么需要 setAccessible?
正常情况下,访问 private 成员会怎样?
java
public class WithoutSetAccessible {
private String secret = "password123";
public static void main(String[] args) throws Exception {
WithoutSetAccessible obj = new WithoutSetAccessible();
Field field = obj.getClass().getDeclaredField("secret");
// 不加 setAccessible(true) 试试?
String value = (String) field.get(obj); // 抛异常!
}
}执行结果:
Exception in thread "main" java.lang.IllegalAccessException:
class WithoutSetAccessible cannot access a member of class WithoutSetAccessible
with modifiers "private"Java 的访问控制检查会阻止你。setAccessible(true) 的作用就是跳过这个检查。
基本用法
java
public class SetAccessibleBasics {
static class Vault {
private String password = "123456";
private int attempts = 3;
private String decrypt(String input) {
return new StringBuilder(input).reverse().toString();
}
private Vault(String password) {
this.password = password;
}
private Vault() {
this.password = "default";
}
}
public static void main(String[] args) throws Exception {
Vault vault = new Vault();
// === 访问私有字段 ===
Field passwordField = Vault.class.getDeclaredField("password");
passwordField.setAccessible(true); // 打开后门
System.out.println("原始密码: " + passwordField.get(vault));
passwordField.set(vault, "999999");
System.out.println("修改后密码: " + passwordField.get(vault));
// === 调用私有方法 ===
Method decryptMethod = Vault.class.getDeclaredMethod("decrypt", String.class);
decryptMethod.setAccessible(true);
String decrypted = (String) decryptMethod.invoke(vault, "dlrow");
System.out.println("解密结果: " + decrypted); // world
// === 调用私有构造 ===
Constructor<Vault> privateCtor = Vault.class.getDeclaredConstructor(String.class);
privateCtor.setAccessible(true);
Vault newVault = privateCtor.newInstance("new_password");
System.out.println("新 Vault 密码: " + passwordField.get(newVault));
}
}读写各类字段
基本类型 vs 引用类型
java
public class FieldTypesDemo {
static class Data {
private int intValue = 42;
private long longValue = 123L;
private double doubleValue = 3.14;
private boolean boolValue = true;
private String stringValue = "hello";
private Object objValue = new Object();
}
public static void main(String[] args) throws Exception {
Class<?> clazz = Data.class;
Data data = new Data();
for (Field f : clazz.getDeclaredFields()) {
f.setAccessible(true);
// 快捷方法 —— 基本类型
switch (f.getType().getSimpleName()) {
case "int":
System.out.println(f.getName() + " = " + f.getInt(data));
f.setInt(data, 100);
break;
case "long":
System.out.println(f.getName() + " = " + f.getLong(data));
f.setLong(data, 999L);
break;
case "double":
System.out.println(f.getName() + " = " + f.getDouble(data));
f.setDouble(data, 2.718);
break;
case "boolean":
System.out.println(f.getName() + " = " + f.getBoolean(data));
f.setBoolean(data, false);
break;
default: // 引用类型
System.out.println(f.getName() + " = " + f.get(data));
f.set(data, null);
}
}
}
}静态字段
静态字段不属于任何实例,读取和写入时传 null 作为对象:
java
public class StaticFieldAccess {
static class Config {
private static String API_KEY = "sk-original";
private static int MAX_RETRY = 3;
}
public static void main(String[] args) throws Exception {
Class<?> clazz = Config.class;
Field apiKeyField = clazz.getDeclaredField("API_KEY");
apiKeyField.setAccessible(true);
System.out.println("原始 API_KEY: " + apiKeyField.get(null)); // null 传给静态字段
apiKeyField.set(null, "sk-new-key");
System.out.println("修改后 API_KEY: " + apiKeyField.get(null));
Field retryField = clazz.getDeclaredField("MAX_RETRY");
retryField.setAccessible(true);
retryField.setInt(null, 5);
System.out.println("修改后 MAX_RETRY: " + retryField.getInt(null));
}
}绕过 final 限制
这是一个高危操作,但有时候不得不做。
场景一:修改 String 常量
String 是不可变的,但通过反射可以修改内部的 char 数组——这会导致奇怪的 bug:
java
public class BypassFinalString {
public static void main(String[] args) throws Exception {
String original = "Hello";
// String 的底层存储
Field valueField = String.class.getDeclaredField("value");
valueField.setAccessible(true);
char[] value = (char[]) valueField.get(original);
System.out.println("原始值: " + original);
System.out.println("原始 char[]: " + Arrays.toString(value));
// 修改 char 数组
value[0] = 'X';
value[1] = 'Y';
System.out.println("修改后值: " + original); // Xello —— 原字符串被改了!
}
}警告:修改 String 内部数组是自找麻烦。字符串常量池、GC 安全性全部失效。永远不要在生产代码中这样做。
场景二:修改 final 字段
java
public class BypassFinalField {
static class Counter {
private final int count = 100; // final 字段
private final String label = "counter";
}
public static void main(String[] args) throws Exception {
Counter counter = new Counter();
// 如果只是普通的 final,理论上可以修改
Field countField = Counter.class.getDeclaredField("count");
countField.setAccessible(true);
// 获取 countField 在内存中的实际位置
Field modifierField = Field.class.getDeclaredField("modifiers");
modifierField.setAccessible(true);
modifierField.setInt(countField, countField.getModifiers() & ~Modifier.FINAL);
countField.setInt(counter, 999);
System.out.println("count = " + countField.getInt(counter));
}
}重要:JIT 编译器会内联 final 常量。如果 final 字段是编译期常量(基本类型或 String 字面量),即使修改了字段值,代码中的引用也可能不会改变——因为 JIT 直接把值内联到调用处了。
实用工具封装
实际开发中,反射操作通常会封装成工具类:
java
import java.lang.reflect.*;
public class ReflectionUtil {
// 获取字段值(支持父类)
public static Object getFieldValue(Object target, String fieldName) throws Exception {
Class<?> clazz = target.getClass();
while (clazz != null && clazz != Object.class) {
try {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(target);
} catch (NoSuchFieldException e) {
clazz = clazz.getSuperclass(); // 父类继续找
}
}
throw new NoSuchFieldError(fieldName);
}
// 设置字段值(支持父类)
public static void setFieldValue(Object target, String fieldName, Object value) throws Exception {
Class<?> clazz = target.getClass();
while (clazz != null && clazz != Object.class) {
try {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(target, value);
return;
} catch (NoSuchFieldException e) {
clazz = clazz.getSuperclass();
}
}
throw new NoSuchFieldError(fieldName);
}
// 调用任意方法(支持父类)
public static Object invokeMethod(Object target, String methodName,
Class<?>[] paramTypes, Object... args) throws Exception {
Class<?> clazz = target.getClass();
while (clazz != null && clazz != Object.class) {
try {
Method method = clazz.getDeclaredMethod(methodName, paramTypes);
method.setAccessible(true);
return method.invoke(target, args);
} catch (NoSuchMethodException e) {
clazz = clazz.getSuperclass();
}
}
throw new NoSuchMethodError(methodName);
}
// 使用示例
public static void main(String[] args) throws Exception {
class Inner {
private int secret = 42;
private String compute(int x) { return String.valueOf(x * secret); }
}
Inner obj = new Inner();
// 读取私有字段
int value = (int) getFieldValue(obj, "secret");
System.out.println("secret = " + value);
// 修改私有字段
setFieldValue(obj, "secret", 100);
System.out.println("secret after = " + getFieldValue(obj, "secret"));
// 调用私有方法
String result = (String) invokeMethod(obj, "compute", new Class<?>[]{int.class}, 5);
System.out.println("compute(5) = " + result); // 500
}
}JDK 9+ 模块系统限制
从 JDK 9 开始,引入了模块系统。跨模块访问默认被禁止,setAccessible(true) 可能失效。
java
public class ModuleRestriction {
public static void main(String[] args) throws Exception {
// JDK 9+ 中,尝试访问 java.util.ArrayList 的私有字段
ArrayList<String> list = new ArrayList<>();
Field elementDataField = ArrayList.class.getDeclaredField("elementData");
elementDataField.setAccessible(true); // 可能抛异常!
// 在严格模块化模式下,这会抛出 InaccessibleObjectException
// 需要 JVM 参数: --add-opens java.base/java.util=ALL-UNNAMED
}
}解决方案:
bash
# 方式一:启动参数
java --add-opens java.base/java.util=ALL-UNNAMED your.app.Main
# 方式二:代码中设置(需要 SecurityManager 权限)
System.setProperty("jdk.module.illegalAccess", "permit");要点回顾
| 操作 | 正常访问 | 反射访问 |
|---|---|---|
| public | ✅ | ✅ |
| protected | ✅(同包/子类) | ✅(setAccessible) |
| package-private | ✅(同包) | ✅(setAccessible) |
| private | ❌ | ✅(setAccessible) |
- setAccessible 跳过安全检查 — 让 private 成员可以被访问
- 基本类型有快捷方法 — getInt/setInt 比 get(Object)/set(Object, Object) 更快
- final 字段可以修改 — 但可能无效(JIT 内联)
- JDK 9+ 受模块限制 — 可能需要 --add-opens 参数
setAccessible 是 Java 封装机制的一个「漏洞」。它让测试框架、ORM 框架、诊断工具得以正常工作——但也容易被滥用。记住:能力越大,责任越大。
