Skip to content

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)
  1. setAccessible 跳过安全检查 — 让 private 成员可以被访问
  2. 基本类型有快捷方法 — getInt/setInt 比 get(Object)/set(Object, Object) 更快
  3. final 字段可以修改 — 但可能无效(JIT 内联)
  4. JDK 9+ 受模块限制 — 可能需要 --add-opens 参数

setAccessible 是 Java 封装机制的一个「漏洞」。它让测试框架、ORM 框架、诊断工具得以正常工作——但也容易被滥用。记住:能力越大,责任越大。

基于 VitePress 构建