反射机制

什么是反射?

一句话理解:反射就是程序在运行时”照X光”——能看穿任何类的内部结构(有哪些字段、方法、构造器),甚至能强行操作private成员

正常写代码是”编译时就知道要调用什么”,反射是”运行时才决定要调用什么”

比喻:正常调用像”按菜单点菜”,反射像”闯进厨房自己翻冰箱”

反射的核心能力:

运行时获取任意类的信息(字段、方法、构造器、注解)

运行时创建任意类的对象

运行时调用任意对象的方法

运行时修改任意对象的字段(包括private)

为什么安全从业者必须懂反射? 因为Java安全漏洞的半壁江山都跟反射有关——反序列化漏洞、表达式注入、沙箱逃逸,底层全是反射在干活

一、Class对象:反射的起点

每个类被JVM加载后,都会生成一个 Class 对象,这个对象里存着这个类的所有元信息(像一份类的”体检报告”)

获取Class对象的三种方式

方式 代码 适用场景
Class.forName("全限定名") Class.forName("java.lang.String") 只知道类名字符串时(最灵活,⭐攻击链最常用)
对象.getClass() "hello".getClass() 已经有对象实例时
类名.class String.class 编译时就知道类型时(最安全)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Test
public void testGetClass() throws Exception {
// 方式1:Class.forName(字符串 → Class对象)
// ⭐ 攻击者最爱用这个,因为类名可以拼接、可以从外部输入获取
Class<?> clazz1 = Class.forName("java.lang.Runtime");

// 方式2:对象.getClass()
String str = "hello";
Class<?> clazz2 = str.getClass();

// 方式3:类名.class
Class<?> clazz3 = String.class;

// 方式2和方式3获取的是同一个Class对象
System.out.println(clazz2 == clazz3); // true

// Class对象能告诉你这个类的一切
System.out.println("类名:" + clazz1.getName()); // java.lang.Runtime
System.out.println("简单类名:" + clazz1.getSimpleName()); // Runtime
System.out.println("是接口吗:" + clazz1.isInterface()); // false
}

⭐ 安全视角:Class.forName 的危险

这个方法接受字符串参数,意味着类名可以来自用户输入、配置文件、反序列化数据

攻击者可以通过控制类名字符串,让JVM加载任意类

这是很多漏洞利用链的第一步

二、获取构造器并创建对象

正常创建对象用 new,反射可以在运行时动态决定创建哪个类的对象

核心API

方法 说明
getConstructor(参数类型...) 获取public构造器
getDeclaredConstructor(参数类型...) 获取任意构造器(包括private)
constructor.newInstance(参数...) 用构造器创建对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
public void testConstructor() throws Exception {
// 获取String的Class对象
Class<?> clazz = Class.forName("java.lang.String");

// 获取接受String参数的构造器
Constructor<?> constructor = clazz.getConstructor(String.class);

// 用这个构造器创建一个String对象
Object str = constructor.newInstance("hello reflection");
System.out.println(str); // hello reflection

// 获取private构造器(比如单例类的private构造器)
// getDeclaredConstructor能拿到private的,getConstructor只能拿public的
Class<?> runtimeClass = Class.forName("java.lang.Runtime");
Constructor<?> privateConstructor = runtimeClass.getDeclaredConstructor();
privateConstructor.setAccessible(true); // 暴力破解private限制
Object runtime = privateConstructor.newInstance();
System.out.println("成功创建Runtime对象:" + runtime);
}

getXxx vs getDeclaredXxx 的区别(非常重要)

方法前缀 能获取的范围 包括private吗
getXxx 只能获取 public 成员(包括继承的)
getDeclaredXxx 能获取 所有 成员(但只限本类声明的)

⭐ 攻击链里几乎全用 getDeclaredXxx,因为要访问的东西往往是private的

三、获取方法并调用

反射调用方法 = 运行时决定调用谁的什么方法,传什么参数

核心API

方法 说明
getMethod(方法名, 参数类型...) 获取public方法
getDeclaredMethod(方法名, 参数类型...) 获取任意方法(包括private)
method.invoke(对象, 参数...) 调用方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
public void testInvokeMethod() throws Exception {
// 普通示例:反射调用String的substring方法
String str = "Hello World";
Method method = String.class.getMethod("substring", int.class, int.class);
Object result = method.invoke(str, 0, 5);
System.out.println(result); // Hello

// ⭐ 安全示例:反射调用Runtime.exec执行系统命令
// 这就是RCE(远程代码执行)的核心原理!
Class<?> runtimeClass = Class.forName("java.lang.Runtime");
// Runtime是单例,通过getRuntime()获取实例
Method getRuntime = runtimeClass.getMethod("getRuntime");
Object runtime = getRuntime.invoke(null); // 静态方法,第一个参数传null
// 调用exec方法执行命令
Method exec = runtimeClass.getMethod("exec", String.class);
Process process = (Process) exec.invoke(runtime, "whoami");
// 读取命令执行结果
java.io.InputStream is = process.getInputStream();
byte[] buf = new byte[1024];
int len = is.read(buf);
System.out.println("命令执行结果:" + new String(buf, 0, len));
}

invoke的第一个参数

如果调用的是实例方法:传对象实例

如果调用的是静态方法:传 null

这个容易搞混,记住:静态方法不属于任何对象,所以传null

四、获取字段并修改

反射不仅能调用方法,还能直接读写对象的字段,包括private字段

核心API

方法 说明
getField(字段名) 获取public字段
getDeclaredField(字段名) 获取任意字段(包括private)
field.get(对象) 读取字段值
field.set(对象, 新值) 修改字段值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Test
public void testField() throws Exception {
class Person {
private String name = "Alice";
private int age = 25;
}

Person person = new Person();
// 正常情况下,外部无法访问private字段
// person.name ← 编译报错

// 用反射强行读取
Field nameField = person.getClass().getDeclaredField("name");
nameField.setAccessible(true); // 关键!暴力反射
System.out.println("原始name:" + nameField.get(person)); // Alice

// 用反射强行修改
nameField.set(person, "Bob");
System.out.println("修改后name:" + nameField.get(person)); // Bob

// 甚至可以修改final字段(Java 8及以前可以,Java 12+限制了)
}

五、setAccessible(true):暴力反射

一句话理解setAccessible(true) 就是反射的”万能钥匙”,能打开任何private锁

不调用这个方法,反射访问private成员会抛 IllegalAccessException

调用之后,private、protected、默认访问权限全部形同虚设

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Test
public void testSetAccessible() throws Exception {
class Secret {
private String password = "super_secret_123";
private void selfDestruct() {
System.out.println("💥 自毁程序已启动!");
}
}

Secret secret = new Secret();

// 读取private字段
Field pwdField = secret.getClass().getDeclaredField("password");
// pwdField.get(secret); // ❌ 不设accessible会报错
pwdField.setAccessible(true); // ✅ 暴力打开
System.out.println("偷到密码:" + pwdField.get(secret)); // super_secret_123

// 调用private方法
Method method = secret.getClass().getDeclaredMethod("selfDestruct");
method.setAccessible(true);
method.invoke(secret); // 💥 自毁程序已启动!
}

安全含义:Java的访问控制(private/protected/public)只是编译期的君子协定,反射可以完全绕过。这意味着:

你不能靠private来保护敏感数据

任何private方法都可以被外部调用

访问控制不是安全机制,只是封装机制

六、⭐⭐ 安全专题:反射为什么是Java漏洞的基石

这一节是安全从业者的重点,要深入理解

反射为什么危险?三个层面

层面1:绕过访问控制

private不再private,任何字段和方法都能被访问

攻击者可以读取对象内部的敏感数据(密码、密钥等)

层面2:动态加载任意类

Class.forName(用户输入) → 攻击者控制加载哪个类

配合 newInstance()invoke() → 创建任意对象、调用任意方法

层面3:反射 + 反序列化 = RCE(远程代码执行)

这是Java安全领域最经典的漏洞模式

⭐ 反序列化攻击链简述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
攻击流程:

1. 攻击者构造恶意的序列化数据(精心构造的字节流)
2. 服务端接收并反序列化这段数据(调用 ObjectInputStream.readObject())
3. 反序列化过程中,自动触发某些类的 readObject() 方法
4. 这些方法内部通过一系列链式调用(利用反射),最终执行:
Runtime.getRuntime().exec("恶意命令")
5. 攻击者获得服务器的命令执行权限 → 完整控制服务器

关键:整条链的核心技术就是反射
- Class.forName() 加载目标类
- getDeclaredMethod() 获取方法
- setAccessible(true) 绕过访问控制
- invoke() 执行方法

经典漏洞案例

漏洞 影响 反射的角色
Apache Commons Collections反序列化 无数Java应用被RCE InvokerTransformer内部用反射调用任意方法
Fastjson反序列化 大量国内Java应用被攻击 通过反射调用setter/getter触发恶意代码
Log4Shell(CVE-2021-44228) 史诗级漏洞,影响极广 JNDI注入后通过反射执行任意代码
Spring4Shell(CVE-2022-22965) Spring框架RCE 通过反射访问class.classLoader修改Tomcat配置

⭐ 用反射模拟攻击链(简化版)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Test
public void testReflectionAttackChain() throws Exception {
// 模拟攻击者通过反射执行系统命令的过程
// 这就是很多反序列化漏洞的最终payload

// Step 1: 通过反射获取Runtime类(攻击者只需要一个字符串)
String className = "java.lang.Runtime";
Class<?> runtimeClass = Class.forName(className);

// Step 2: 获取getRuntime方法(Runtime是单例模式)
Method getRuntime = runtimeClass.getMethod("getRuntime");

// Step 3: 调用getRuntime获取Runtime实例
Object runtime = getRuntime.invoke(null);

// Step 4: 获取exec方法
Method exec = runtimeClass.getMethod("exec", String.class);

// Step 5: 执行命令
// exec.invoke(runtime, "calc.exe"); // Windows弹计算器
// exec.invoke(runtime, "open -a Calculator"); // macOS弹计算器

// 上面5步,全部都是反射API
// 在反序列化漏洞中,这些步骤被嵌入到精心构造的对象链中
// 反序列化时自动触发,攻击者无需直接写代码

System.out.println("⚠️ 如果这段代码通过反序列化被触发,服务器就被攻破了");
}

⭐ 更隐蔽的反射调用方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
public void testStealthyReflection() throws Exception {
// 方式1:直接反射(容易被检测)
Runtime.getRuntime().exec("whoami");

// 方式2:通过反射链(绕过静态分析)
// 攻击者可以把类名、方法名拆成多段拼接,躲避安全检测
String cls = "java.lang." + "Runt" + "ime";
String mtd = "ex" + "ec";
Class<?> clazz = Class.forName(cls);
Method getRT = clazz.getMethod("getRuntime");
Method execMethod = clazz.getMethod(mtd, String.class);
// execMethod.invoke(getRT.invoke(null), "whoami");

// 方式3:通过ProcessBuilder(另一个常见的命令执行类)
Class<?> pbClass = Class.forName("java.lang.ProcessBuilder");
Constructor<?> pbConstructor = pbClass.getConstructor(java.util.List.class);
Object pb = pbConstructor.newInstance(java.util.Arrays.asList("whoami"));
Method start = pbClass.getMethod("start");
// start.invoke(pb);

System.out.println("⚠️ 多种反射调用方式,静态检测很难覆盖全");
}

⭐ 防御手段

防御手段 原理 状态
SecurityManager JVM级别的权限控制,可以禁止反射 Java 17+ 已废弃,不推荐
模块化系统(Java 9+ JPMS) 模块之间的反射访问需要显式声明 有效但很多库还没适配
反序列化白名单 只允许反序列化特定类 ✅ 推荐(ObjectInputFilter)
不使用原生序列化 用JSON代替Java序列化 ✅ 推荐
RASP(运行时应用自保护) 在运行时拦截危险的反射调用 ✅ 企业级方案
及时更新依赖 修复已知漏洞组件 ✅ 基本操作

七、反射的正当用途

反射不是”坏东西”,它是Java生态的基石,几乎所有框架都依赖反射

Spring依赖注入

Spring通过反射读取 @Autowired 注解,然后反射创建对象、反射注入字段

你写 @Autowired private UserService userService; 时,Spring在背后用反射把对象塞进去

MyBatis ORM映射

MyBatis通过反射把数据库查询结果映射到Java对象的字段上

SQL查出 user_name 列 → 反射找到 setUserName 方法 → 反射调用填值

JUnit测试框架

JUnit通过反射找到所有带 @Test 注解 的方法,然后反射调用它们

这就是为什么测试方法不需要你手动调用,框架自动发现并执行

JSON序列化(Jackson/Gson)

把Java对象转成JSON:反射读取所有字段和getter方法

把JSON转回Java对象:反射调用构造器创建对象、反射调用setter填值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Test
public void testReflectionUsage() throws Exception {
// 模拟Spring的简化版依赖注入
class UserService {
public String findUser() { return "找到了用户Alice"; }
}
class UserController {
private UserService userService; // private字段,正常没法从外面赋值
}

// Spring做的事情(简化版):
UserController controller = new UserController();
UserService service = new UserService();

// 1. 反射获取private字段
Field field = UserController.class.getDeclaredField("userService");
field.setAccessible(true);

// 2. 反射注入对象
field.set(controller, service);

// 3. 验证注入成功
UserService injected = (UserService) field.get(controller);
System.out.println(injected.findUser()); // 找到了用户Alice
// 这就是 @Autowired 的底层原理!
}

八、反射API速查表

操作 API 说明
获取Class Class.forName("全限定名") 字符串→Class
获取Class 对象.getClass() 对象→Class
获取Class 类名.class 类→Class
获取构造器 clazz.getDeclaredConstructor(参数类型...) 包括private
创建对象 constructor.newInstance(参数...) 动态创建对象
获取方法 clazz.getDeclaredMethod(方法名, 参数类型...) 包括private
调用方法 method.invoke(对象, 参数...) 静态方法第一个参数传null
获取字段 clazz.getDeclaredField(字段名) 包括private
读取字段 field.get(对象) 获取字段值
修改字段 field.set(对象, 新值) 设置字段值
暴力访问 xxx.setAccessible(true) 绕过private限制
获取注解 xxx.getAnnotation(注解类.class) 配合 注解 使用

九、常见坑

坑1:getMethod vs getDeclaredMethod 搞混

getMethod 只能拿public的(但包括从父类继承的)

getDeclaredMethod 能拿所有的(但只限本类声明的,不包括继承的)

要拿父类的private方法?先 getSuperclass()getDeclaredMethod()

坑2:忘了 setAccessible(true)

访问private成员不设accessible → 直接报 IllegalAccessException

这是最常见的反射错误

坑3:参数类型写错

getMethod("exec", String.class)getMethod("exec", String[].class) 完全不同

基本类型用 int.class 不是 Integer.class

方法重载时参数类型必须精确匹配

坑4:invoke时对象和方法不匹配

用A类的Method去invoke B类的对象 → IllegalArgumentException

坑5:反射性能问题

反射调用比直接调用慢几倍到几十倍

热点代码中不要在循环里大量使用反射

优化方式:缓存Method/Field对象、使用MethodHandle(Java 7+)

坑6:Java 9+模块化限制

Java 9引入模块化后,跨模块的反射访问受限

需要在 module-info.javaopens 包,或者启动时加 --add-opens 参数

很多老框架升级Java版本时会遇到这个问题

十、练习题

练习1(基础):用反射获取 java.util.ArrayList 类中名为 elementData 的private字段,读取一个ArrayList内部的数组容量

练习2(进阶):写一个方法 copyProperties(Object source, Object target),用反射把source的所有字段值复制到target的同名字段上(Spring的BeanUtils.copyProperties就是这么干的)

练习3(安全)⭐:用反射修改一个”不可变”的String对象的内容。提示:String内部有一个 private final char[] value 字段(Java 8),反射甚至可以改final字段

练习4(安全)⭐:研究 java.lang.ProcessBuilder 类,用反射构造一个执行系统命令的调用链,对比和 Runtime.exec() 的区别


上一章 目录 下一章
日期时间API java基础 注解