反射机制
1. 反射是什么
反射(Reflection)是指程序在运行时获取自身结构信息(类、方法、字段、构造器等)并对其进行操作的能力。
一句话本质:反射把”编译时确定调用目标”变成了”运行时动态确定调用目标”,代价是牺牲了编译期类型检查和一部分性能,换来的是极高的灵活性
理解这句话你就能明白,为什么上一节讲的”反序列化重建对象不调用构造器”能实现——那正是反射体系里 sun.reflect.ReflectionFactory 提供的特殊能力。反射不只是”调用一个方法”这么简单,它还包括绕过语言层面限制、动态创建对象等一整套运行时操作 JVM 的手段。
2. 为什么需要
-
框架解耦:框架作者在编译时不可能知道使用者会写出什么类,反射让框架能够”发现”并操作使用者提供的任意类型(比如 Spring 的依赖注入,本质是反射扫描并调用构造器/setter)。
-
动态性:插件化架构、SPI 机制、ORM 字段映射,都依赖运行时按名字找到类/方法并调用。
-
访问私有成员的能力:这本不是反射诞生的初衷(反射最初是为了支持 IDE、调试器、序列化框架这类”元编程”工具),但正是这个能力,让反射成了安全领域绕过封装的核心手段。
3. 核心机制拆解
3.1 反射的入口 对象
java
Class<?> clazz = Class.forName("java.lang.Runtime");Class 对象是反射体系的起点——每个被加载的类,在方法区(元空间)里都有唯一对应的一个 Class 实例,你可以通过 .class 字面量、Object#getClass()、Class.forName(String) 三种方式拿到它。
值得注意的是 Class.forName 有个重载:
java
Class.forName(String name, boolean initialize, ClassLoader loader)第二个参数 initialize 决定了拿到 Class 对象的同时是否触发类的初始化(即执行静态代码块、给静态变量赋非零初值)。这个参数在安全利用链里经常被显式指定为 true,因为攻击者往往就是要借助 Class.forName 顺带触发一次类初始化
3.2 Method、Field、Constructor:反射对象也是对象
Method m = clazz.getMethod("exec", String.class);Object result = m.invoke(runtimeInstance, "whoami");Method、Field、Constructor本身也是普通的 Java 对象(分别是 java.lang.reflect.Method 等类的实例),它们持有对目标类、目标成员的元信息引用。这意味着:
-
你可以把一个
Method对象像普通对象一样传递、存储、序列化 -
getMethod和getDeclaredMethod的区别:前者只能拿到public方法(包括继承来的),后者能拿到当前类自己声明的所有方法(不管访问修饰符),但不包括继承的。这个区别在写利用链/绕过某些防御检测时经常用到——很多防御代码只检查了getMethod能拿到的方法名。
3.3 setAccessible(true):绕的是谁的检查
这是这一节的核心,也是安全视角下最值得深挖的一点。
Field f = obj.getClass().getDeclaredField("value");f.setAccessible(true);f.set(obj, "modified");setAccessible绕过的是 Java 语言规范层面的访问控制检查,这个检查原本是由Reflection#getCallerClass结合AccessibleObject内部的override标志位来做的,并不是 JVM 字节码校验器层面的检查。也就是说:
-
private/protected/包私有 这些访问修饰符,本质上是编译器和反射 API 在语言层面自愿遵守的约定,而不是 JVM 执行引擎层面不可逾越的硬限制 -
setAccessible(true)做的事情,就是把AccessibleObject内部一个叫override的 boolean 字段设为true,后续该反射对象在invoke/get/set时,会跳过Reflection.ensureMemberAccess这一层检查
3.4 MethodAccessor:反射调用的两种实现路径
Method.invoke() 的调用不是直接走 native 方法完事的,它内部委托给一个 MethodAccessor 接口的实现,而这个实现有两种,JVM 会动态切换:
-
NativeMethodAccessorImpl:底层直接调用 JVM 的 native 方法(invoke0),每次反射调用都要经过 native 层的一次”翻译”,有比较固定的开销(参数装箱/拆箱、访问检查等)。 -
GeneratedMethodAccessorXXX:这是运行时动态生成的一个类,它不走 native 层,而是用直接的 Java 字节码去调用目标方法,相当于把一次反射调用”编译”成了一次接近原生的方法调用,性能远高于第一种方式。
3.5 JPMS(模块化)之后的变化
JDK 9 引入模块系统后,setAccessible 不再万能了。
-
模块之间默认是强封装的:一个模块的包,除非在
module-info.java里显式exports或opens,否则模块外部即使用反射也无法访问其内部类的非公开成员。 -
对于
exports的包:外部可以正常反射调用public成员,但setAccessible(true)访问其私有成员依然会被拒绝(抛InaccessibleObjectException),除非该包被opens。 -
对于
opens的包(深度开放):setAccessible才能正常工作。