第八章. common-core 工具类(六):ReflectUtils 与多级属性访问
第八章. common-core 工具类(六):ReflectUtils 与多级属性访问
Prorise第八章. common-core 工具类(六):ReflectUtils 反射实战
摘要:本章我们将深入 ReflectUtils。反射是框架的基石,允许我们动态操作对象。我们将首先掌握 Hutool ReflectUtil 在实例化、读写私有字段、调用方法方面的基础,然后重点实战 RVP 独有的 多级嵌套属性读写 增强(invokeGetter / invokeSetter)。
在上一章中,我们从“二次开发”的视角,实战了 TreeBuildUtils,掌握了如何利用 NodeParser 将“扁平列表”高效转换为前端 UI 所需的、带 label 和 extra 字段的“树形 JSON”。
现在,我们来看 utils 包下一个更“底层”、更强大的工具类——ReflectUtils。反射(Reflection) 是 Java 提供的在“运行时”动态检查、访问和操作类(包括私有属性和方法)的能力。它是所有框架(包括 Spring)实现“自动装配”和“AOP”的基石。
RVP 的 ReflectUtils 同样继承了 Hutool 的 ReflectUtil,并在其上提供了“多级”属性访问的超级增强。本章,我们将重点实战这一核心功能。
本章学习路径

8.1. RVP 的反射哲学:Hutool 基础 + RVP 增强
在开始实战前,我们必须理解为什么需要反射,以及 RVP 对 Hutool 的反射工具做了哪些取舍。
8.1.1. 为什么要用反射?(解耦、泛型操作、动态调用)
反射的核心价值在于“动态”和“解耦”。它允许我们在“运行时”通过字符串(方法名、属性名)来操作对象,而不是在“编译时”硬编码。
在 RVP 中,最典型的真实场景有两个:
处理泛型:在上一章
TreeBuildUtils.build(List<T> list, ...)方法中,T是一个不确定的泛型(可以是SysDept,也可以是我们“二开”的MyCategory)。TreeBuildUtils无法“硬编码”t.getParentId(),因为它不认识T。- RVP 源码:
K k = ReflectUtils.invokeGetter(list.get(0), "parentId"); - 分析:这行代码是 RVP
TreeBuildUtils中的真实代码。它通过反射,动态地(传入 “parentId” 字符串)去调用第一个元素的getParentId()方法。这就是反射的威力。
- RVP 源码:
框架通用封装:RVP 的很多通用功能,比如数据脱敏、数据权限、Excel 导入导出,都需要在运行时动态读取或修改 任意对象 的 私有字段。没有反射,这些功能都无法实现。
8.1.2. 继承关系:RVP ReflectUtils 与 Hutool ReflectUtil
我们打开 ReflectUtils 的源码:
文件路径:ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/reflect/ReflectUtils.java
1 | public class ReflectUtils extends ReflectUtil { |
RVP 的 ReflectUtils 继承了 cn.hutool.core.util.ReflectUtil。
- Hutool
ReflectUtil:提供了“瑞士刀”般全面的基础反射能力,包括操作 构造方法(newInstance)、字段(getField,setFieldValue)和 方法(getMethod,invoke)。 - RVP
ReflectUtils:RVP 认为 Hutool 的基础功能已经足够好,于是 只增强了两个核心方法:invokeGetter和invokeSetter,专门用于 多级(嵌套)的属性访问。
8.1.3. 缓存机制:Hutool 如何通过缓存提升反射性能
反射有一个众所周知的“缺点”:性能较差。因为每次查找字段(getField)或方法(getMethod)都需要 JVM 进行动态查找,开销很大。
Hutool ReflectUtil 巧妙地解决了这个问题。我们查看 ReflectUtil 源码(可以 Ctrl + 点击 ReflectUtil 下载),会发现它内部维护了静态缓存:
1 | // 位于 Hutool ReflectUtil 源码中 |
工作原理:当你第一次调用 ReflectUtil.getFields(Student.class) 时,Hutool 会“慢速”地通过原生反射找到所有字段,然后 存入 FIELD_CACHE。当你 第二次 调用 getFields(Student.class) 时,Hutool 会 直接从 ConcurrentHashMap 缓存中 返回结果,速度极快。
8.2. 测试准备:创建 ReflectUtilsTest (main 方法) 与模拟实体
ReflectUtils 同样是纯 Java 工具,我们使用 main 方法进行测试。
8.2.1. 创建 utils.test.ReflectUtilsTest.java
我们在 ruoyi-demo 模块中创建测试类:
文件路径:ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/utils/test/ReflectUtilsTest.java
1 | package org.dromara.demo.utils.test; |
8.2.2. 二开模拟:创建 Student 内部类(含 private 字段)
为了完整测试反射,我们的模拟实体必须包含 私有字段 和 多种构造方法。
1 | // ... |
8.2.3. 二开模拟:创建 Tool 内部类(Student 的子属性)
为了测试 RVP 的 多级属性访问(例如 student.tool.type),我们必须创建 Tool 类。
1 | // ... |
8.3. Hutool 基础(一):动态实例化 (newInstance)
newInstance 是反射最常见的用途:根据 Class 对象动态创建实例。
8.3.1. 实战:newInstance(无参、多参构造)
Hutool 的 newInstance 极大地简化了 Java 原生 Constructor.newInstance() 的繁琐操作,它能 自动匹配 参数类型来寻找对应的构造方法。
我们在 ReflectUtilsTest 中创建 testInstantiation 方法:
1 | // ... |
运行 main 方法,控制台输出:
1 | ... INFO ... ReflectUtils 测试开始... |
分析:newInstance 成功地根据我们传入的参数,自动调用了对应的构造方法。
8.3.2. 特殊场景:newInstanceIfPossible 与 int.class
【二开思考】:如果我拿到的 Class 是一个 原始类型(如 int.class)或者一个 接口,它们没有构造方法,newInstance 会怎么办?
1 | // 在 testInstantiation() 方法中继续添加 |
运行 main 方法(接上文输出):
1 | ... INFO ... --- 3. 测试 newInstance (失败场景) --- |
结论:newInstanceIfPossible 是一个“容错”方法。在不确定 Class 类型是否能被实例化时(例如在非常通用的泛型工具中),使用它比 newInstance 更安全,它会返回 null 或原始类型的默认值,而不是抛出异常。
8.4. Hutool 基础(二):无视 private 的字段访问
8.4.1. 为何要读写 private 字段?
假设我们要为 Student 类增加一个“数据脱敏”的功能。我们希望在查询 Student 对象并返回给前端时,自动将 name 字段的值(如 “张三”)替换为 “张*”。
我们可能会在 SysUserServiceImpl 这样的服务层中获取到 List<Student>,然后尝试去脱敏:
1 | // 痛点代码 (Bad Practice) |
我们失败了,因为 name 字段是 private 的。我们 无法在外部(如 StudentServiceImpl)直接读写它。
【解决方案】:
- 方案 A(侵入式):回去修改
Student类,为name增加public getName()和setName()。 - 方案 B(反射):在不修改
Student类源码 的前提下,使用反射强行读写private字段。
在框架开发中(例如 RVP 的 common-sensitive 脱敏组件、common-translation 翻译组件),方案 B 是唯一的选择,因为它必须在“不侵入”业务实体类的前提下,实现对任意 private 字段的读写。
8.4.2. 实战:getFields 与 Modifier 修饰符
Hutool 的 ReflectUtil.getFields() 可以获取一个类(及其父类)的 所有 字段,无论 public 还是 private。
我们在 ReflectUtilsTest.java 中创建 testFields 方法:
1 | // ... |
运行 main 方法,控制台输出:
1 | ... INFO ... --- 5. 测试 getFields (获取所有字段) --- |
分析:我们成功获取了所有字段,包括 private 的 name 和 tool。Modifier.toString() 可以帮我们直观地看到修饰符。
8.4.3. 实战:setFieldValue (写私有字段)
现在我们来解决 8.4.1 中的“脱敏”痛点。setFieldValue 会自动处理 setAccessible(true),强行写入私有字段。
1 | // ... 在 testFields() 方法中继续添加 ... |
运行 main 方法(接上文输出):
1 | ... INFO ... --- 6. 测试 setFieldValue (写私有字段) --- |
结论:setFieldValue 成功无视了 private 限制,修改了 name 字段。
8.4.4. 实战:getFieldValue (读私有字段)
同理,getFieldValue 可以强行读取 private 字段的值。
1 | // ... 在 testFields() 方法中继续添加 ... |
运行 main 方法(接上文输出):
1 | ... INFO ... --- 7. 测试 getFieldValue (读私有字段) --- |
场景总结:getFieldValue 和 setFieldValue 是 框架开发者(而非业务开发者)的利器,专门用于实现 数据脱敏、数据翻译、ORM 映射 等需要“无视 private”的通用功能。
8.5. Hutool 基础(三):动态方法调用 (invoke)
get/setFieldValue 是操作“字段”,invoke 则是操作“方法”。
【二开思考】:在 8.4.3 中,我们使用 ReflectUtils.setFieldValue(s, "name", "张*") 成功修改了 name 字段。但如果 Student 类有 public void setName(String name) 方法,我们为什么不优先调用 setName 呢?
- 封装性:
setName方法内部可能包含了 业务逻辑(例如if (name == null) throw ...或this.name = name.trim())。 - AOP:
setName方法上可能加了@Log或@Transactional等注解。 - 原则:优先调用
public方法(invoke),走投无路时(没有setter或必须绕过 AOP)才操作private字段(setFieldValue)。
8.5.1. 实战:invoke(student, "setName", "...")
invoke 允许我们通过“方法名字符串”来执行方法。
我们在 ReflectUtilsTest.java 中创建 testInvoke 方法:
1 | // ... |
运行 main 方法,控制台输出:
1 | ... INFO ... --- 8. 测试 getMethod (获取方法) --- |
8.5.2. invoke vs invokeStatic
invoke 用于调用 实例方法(需要 student 对象),invokeStatic 用于调用 静态方法。
1 | // ... 在 testInvoke() 方法中继续添加 ... |
运行 main 方法(接上文输出):
1 | ... INFO ... --- 10. 测试 invokeStatic (调用静态方法) --- |
场景总结:invoke 是框架的基石。RVP 中 TreeBuildUtils 的 invokeGetter、Spring AOP 的切面执行、Spring MVC 的 Controller 方法调用,其 底层原理都是 invoke。
8.6. 【RVP 核心】多级属性访问 (invokeGetter / invokeSetter)
我们已经掌握了 Hutool 的 invoke,它很强大,但只能处理“单层”调用。
8.6.1. 【二开痛点】Hutool invoke 的局限:无法处理 student.tool.type
假设我们的“二开”需求是,动态设置 Student 的 嵌套属性 tool.type。
如果我们用 Hutool 的 invoke,会非常痛苦:
1 | // 痛点代码 |
这段代码是“灾难性”的:它无法通用,且充满了 null 检查。
8.6.2. RVP 源码解析:invokeSetter 如何 split('.') 循环 get 最后 set
RVP 的 ReflectUtils 完美封装了这个“多级访问”的逻辑。我们来看 invokeSetter 的源码(精简过):
1 | // 位于 RVP 的 ReflectUtils.java |
invokeGetter 的逻辑类似,只是它会一直 get 到最后一层并返回。
8.6.3. 【实战】invokeSetter 与“三大陷阱”
RVP 的封装虽然强大,但也带来了“约定”,如果约定不满足,就会崩溃。我们来实战 invokeSetter 并复现所有陷阱。
我们在 ReflectUtilsTest.java 中创建 testRvpInvoke 方法:
1 | // ... |
运行 main 方法,控制台输出:
1 | ... INFO ... --- 11. 测试 RVP invokeSetter (多级) --- |
结论:invokeSetter 成功实现了 student.tool.type = "铅笔盒" 的嵌套设置。
8.6.4. 【RVP 核心】invokeGetter 实战(解决陷阱后)
invokeGetter 同样依赖这一套 get 链。
1 | // ... 在 testRvpInvoke() 方法中继续添加 ... |
运行 main 方法(接上文输出):
1 | ... INFO ... --- 12. 测试 RVP invokeGetter (多级) --- |
8.6.5. 【RVP 真实场景】TreeBuildUtils 中为何使用 invokeGetter?
现在我们回头看 8.1.1 提到的 RVP 真实源码:K k = ReflectUtils.invokeGetter(list.get(0), "parentId");
这行代码只用到了 invokeGetter 的“单级”调用能力,为什么不直接用 Hutool 的 ReflectUtil.invoke(list.get(0), "getParentId") 呢?
答案:
- 统一性:RVP 开发者希望在框架内部统一使用
ReflectUtils这一套 API。 - 简洁性:
invokeGetter(obj, "parentId")比invoke(obj, "getParentId")更符合“操作属性”的直觉(Hutool 6 也会推出getFieldValue的field.get方式)。 - 扩展性:虽然
TreeBuildUtils目前只用了单级,但 RVP 框架的其他地方(如配置中心、数据绑定)可能需要多级访问,invokeGetter提供了这种 向下兼容 的能力。
8.7. 本章总结
在本章中,我们聚焦于 ReflectUtils,它继承了 Hutool ReflectUtil 并提供了核心增强。
Hutool 基础:我们掌握了
ReflectUtil提供的三大基础能力:newInstance:动态实例化(可自动匹配构造方法)。get/setFieldValue:强行读写private字段(用于脱敏、ORM 等场景)。invoke/invokeStatic:动态调用方法(AOP 和框架的基石)。
RVP 核心增强:我们重点实战了 RVP 独有的
invokeGetter和invokeSetter。- 价值:它们封装了
split('.')逻辑,实现了对 多级嵌套属性(如student.tool.type)的动态读写。 - 陷阱:使用 RVP 的多级访问,必须 确保调用链上所有的
get方法都存在,并且 中间对象不能为null,否则会抛出异常。
- 价值:它们封装了









