什么是反射?
一句话理解:反射就是程序在运行时”照X光”——能看穿任何类的内部结构(有哪些字段、方法、构造器),甚至能强行操作private成员
正常写代码是”编译时就知道要调用什么”,反射是”运行时才决定要调用什么”
比喻:正常调用像”按菜单点菜”,反射像”闯进厨房自己翻冰箱”
反射的核心能力:
运行时获取任意类的信息(字段、方法、构造器、注解)
运行时创建任意类的对象
运行时调用任意对象的方法
运行时修改任意对象的字段(包括private)
⭐ 为什么安全从业者必须懂反射? 因为Java安全漏洞的半壁江山都跟反射有关——反序列化漏洞、表达式注入、沙箱逃逸,底层全是反射在干活
一、Class对象:反射的起点
每个类被JVM加载后,都会生成一个 Class 对象,这个对象里存着这个类的所有元信息(像一份类的”体检报告”)
获取Class对象的三种方式
| 方式 | 代码 | 适用场景 |
|---|---|---|
Class.forName("全限定名") |
Class.forName("java.lang.String") |
只知道类名字符串时(最灵活,⭐攻击链最常用) |
对象.getClass() |
"hello".getClass() |
已经有对象实例时 |
类名.class |
String.class |
编译时就知道类型时(最安全) |
1 |
|
⭐ 安全视角:Class.forName 的危险
这个方法接受字符串参数,意味着类名可以来自用户输入、配置文件、反序列化数据
攻击者可以通过控制类名字符串,让JVM加载任意类
这是很多漏洞利用链的第一步
二、获取构造器并创建对象
正常创建对象用 new,反射可以在运行时动态决定创建哪个类的对象
核心API
| 方法 | 说明 |
|---|---|
getConstructor(参数类型...) |
获取public构造器 |
getDeclaredConstructor(参数类型...) |
获取任意构造器(包括private) |
constructor.newInstance(参数...) |
用构造器创建对象 |
1 |
|
getXxx vs getDeclaredXxx 的区别(非常重要)
| 方法前缀 | 能获取的范围 | 包括private吗 |
|---|---|---|
getXxx |
只能获取 public 成员(包括继承的) | ❌ |
getDeclaredXxx |
能获取 所有 成员(但只限本类声明的) | ✅ |
⭐ 攻击链里几乎全用 getDeclaredXxx,因为要访问的东西往往是private的
三、获取方法并调用
反射调用方法 = 运行时决定调用谁的什么方法,传什么参数
核心API
| 方法 | 说明 |
|---|---|
getMethod(方法名, 参数类型...) |
获取public方法 |
getDeclaredMethod(方法名, 参数类型...) |
获取任意方法(包括private) |
method.invoke(对象, 参数...) |
调用方法 |
1 |
|
invoke的第一个参数
如果调用的是实例方法:传对象实例
如果调用的是静态方法:传 null
这个容易搞混,记住:静态方法不属于任何对象,所以传null
四、获取字段并修改
反射不仅能调用方法,还能直接读写对象的字段,包括private字段
核心API
| 方法 | 说明 |
|---|---|
getField(字段名) |
获取public字段 |
getDeclaredField(字段名) |
获取任意字段(包括private) |
field.get(对象) |
读取字段值 |
field.set(对象, 新值) |
修改字段值 |
1 |
|
五、setAccessible(true):暴力反射
一句话理解:setAccessible(true) 就是反射的”万能钥匙”,能打开任何private锁
不调用这个方法,反射访问private成员会抛 IllegalAccessException
调用之后,private、protected、默认访问权限全部形同虚设
1 |
|
⭐ 安全含义:Java的访问控制(private/protected/public)只是编译期的君子协定,反射可以完全绕过。这意味着:
你不能靠private来保护敏感数据
任何private方法都可以被外部调用
访问控制不是安全机制,只是封装机制
六、⭐⭐ 安全专题:反射为什么是Java漏洞的基石
这一节是安全从业者的重点,要深入理解
反射为什么危险?三个层面
层面1:绕过访问控制
private不再private,任何字段和方法都能被访问
攻击者可以读取对象内部的敏感数据(密码、密钥等)
层面2:动态加载任意类
Class.forName(用户输入) → 攻击者控制加载哪个类
配合 newInstance()、invoke() → 创建任意对象、调用任意方法
层面3:反射 + 反序列化 = RCE(远程代码执行)
这是Java安全领域最经典的漏洞模式
⭐ 反序列化攻击链简述
1 | 攻击流程: |
经典漏洞案例
| 漏洞 | 影响 | 反射的角色 |
|---|---|---|
| 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 |
|
⭐ 更隐蔽的反射调用方式
1 |
|
⭐ 防御手段
| 防御手段 | 原理 | 状态 |
|---|---|---|
| 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 |
|
八、反射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.java 中 opens 包,或者启动时加 --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基础 | 注解 |