第八章. common-core 工具类(六):ReflectUtils 与多级属性访问

第八章. common-core 工具类(六):ReflectUtils 反射实战

摘要:本章我们将深入 ReflectUtils。反射是框架的基石,允许我们动态操作对象。我们将首先掌握 Hutool ReflectUtil 在实例化、读写私有字段、调用方法方面的基础,然后重点实战 RVP 独有的 多级嵌套属性读写 增强(invokeGetter / invokeSetter)。

在上一章中,我们从“二次开发”的视角,实战了 TreeBuildUtils,掌握了如何利用 NodeParser 将“扁平列表”高效转换为前端 UI 所需的、带 labelextra 字段的“树形 JSON”。

现在,我们来看 utils 包下一个更“底层”、更强大的工具类——ReflectUtils反射(Reflection) 是 Java 提供的在“运行时”动态检查、访问和操作类(包括私有属性和方法)的能力。它是所有框架(包括 Spring)实现“自动装配”和“AOP”的基石。

RVP 的 ReflectUtils 同样继承了 Hutool 的 ReflectUtil,并在其上提供了“多级”属性访问的超级增强。本章,我们将重点实战这一核心功能。

本章学习路径

ReflectUtils 反射工具


8.1. RVP 的反射哲学:Hutool 基础 + RVP 增强

在开始实战前,我们必须理解为什么需要反射,以及 RVP 对 Hutool 的反射工具做了哪些取舍。

8.1.1. 为什么要用反射?(解耦、泛型操作、动态调用)

反射的核心价值在于“动态”和“解耦”。它允许我们在“运行时”通过字符串(方法名、属性名)来操作对象,而不是在“编译时”硬编码。

在 RVP 中,最典型的真实场景有两个:

  1. 处理泛型:在上一章 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() 方法。这就是反射的威力。
  2. 框架通用封装: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
2
3
4
5
public class ReflectUtils extends ReflectUtil {
// RVP 独有增强
public static <E> E invokeGetter(Object obj, String propertyName) { ... }
public static <E> void invokeSetter(Object obj, String propertyName, E value) { ... }
}

RVP 的 ReflectUtils 继承了 cn.hutool.core.util.ReflectUtil

  • Hutool ReflectUtil:提供了“瑞士刀”般全面的基础反射能力,包括操作 构造方法newInstance)、字段getField, setFieldValue)和 方法getMethod, invoke)。
  • RVP ReflectUtils:RVP 认为 Hutool 的基础功能已经足够好,于是 只增强了两个核心方法invokeGetterinvokeSetter,专门用于 多级(嵌套)的属性访问。

8.1.3. 缓存机制:Hutool 如何通过缓存提升反射性能

反射有一个众所周知的“缺点”:性能较差。因为每次查找字段(getField)或方法(getMethod)都需要 JVM 进行动态查找,开销很大。

Hutool ReflectUtil 巧妙地解决了这个问题。我们查看 ReflectUtil 源码(可以 Ctrl + 点击 ReflectUtil 下载),会发现它内部维护了静态缓存:

1
2
3
4
5
6
7
// 位于 Hutool ReflectUtil 源码中
// 构造器缓存
private static final Map<Class<?>, Constructor<?>[]> CONSTRUCTOR_CACHE = new ConcurrentHashMap<>();
// 字段缓存
private static final Map<Class<?>, Field[]> FIELD_CACHE = new ConcurrentHashMap<>();
// 方法缓存
private static final Map<Class<?>, Method[]> METHOD_CACHE = new ConcurrentHashMap<>();

工作原理:当你第一次调用 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
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
package org.dromara.demo.utils.test;

import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;
// 导入 RVP 的 ReflectUtils (它已包含 Hutool 的功能)
import org.dromara.common.core.utils.reflect.ReflectUtils;
// 导入 Hutool 的 BeanUtil,方便演示
import cn.hutool.core.bean.BeanUtil;

/**
* ReflectUtils 反射工具实战
* (纯 Java 测试,无需 Spring)
*/
public class ReflectUtilsTest {

private static final Log console = LogFactory.get();

// psvm
public static void main(String[] args) {
console.info("ReflectUtils 测试开始...");

// ... 我们将在这里调用测试方法 ...
}

// ... 模拟实体 ...
}

8.2.2. 二开模拟:创建 Student 内部类(含 private 字段)

为了完整测试反射,我们的模拟实体必须包含 私有字段多种构造方法

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// ...
public class ReflectUtilsTest {
// ... main ...

/**
* 模拟实体:学生
* 必须是 static 静态内部类,才能在 static main 中被访问
*/
@Data // 我们不过多的在代码里面写Getter Setter
@ToString
public static class Student {
// 私有字段,外部无法 .name 访问
private String name;
// 公开字段
public Integer age;
// 静态常量
public static final long SCORE = 100L;

// 子对象,用于 RVP 多级访问测试
private Tool tool;


// 1. 无参构造
public Student() {
console.info("Student [无参] 构造方法被调用");
}

// 2. 单参构造
public Student(String name) {
console.info("Student [String] 构造方法被调用");
this.name = name;
}

// 3. 多参构造
public Student(String name, Integer age) {
console.info("Student [String, Integer] 构造方法被调用");
this.name = name;
this.age = age;
}

// 静态方法
public static void clog() {
console.info("静态方法 clog() 被调用! SCORE = " + SCORE);
}
}

// ...
}

8.2.3. 二开模拟:创建 Tool 内部类(Student 的子属性)

为了测试 RVP 的 多级属性访问(例如 student.tool.type),我们必须创建 Tool 类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ...
public class ReflectUtilsTest {
// ... main, Student class ...

/**
* 模拟实体:学生携带的工具
* 必须是 static
*/
@Data
public static class Tool {
private String type; // e.g., "铅笔盒"
}

// ... 测试方法 ...
}

8.3. Hutool 基础(一):动态实例化 (newInstance)

newInstance 是反射最常见的用途:根据 Class 对象动态创建实例

8.3.1. 实战:newInstance(无参、多参构造)

Hutool 的 newInstance 极大地简化了 Java 原生 Constructor.newInstance() 的繁琐操作,它能 自动匹配 参数类型来寻找对应的构造方法。

我们在 ReflectUtilsTest 中创建 testInstantiation 方法:

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
// ...
public class ReflectUtilsTest {

// ... main, Student, Tool ...

public static void main(String[] args) {
console.info("ReflectUtils 测试开始...");
testInstantiation();
}

/**
* 测试 1:动态实例化 (newInstance)
*/
public static void testInstantiation() {
console.info("--- 1. 测试 newInstance (无参) ---");
// Hutool 会自动查找 Student() 构造
Student s1 = ReflectUtils.newInstance(Student.class);
console.info("【s1 结果】: {}", s1);

console.info("--- 2. 测试 newInstance (多参) ---");
// Hutool 会自动匹配 Student(String, Integer) 构造
Student s2 = ReflectUtils.newInstance(Student.class, "张三", 20);
console.info("【s2 结果】: {}", s2);
}

// ...
}

运行 main 方法,控制台输出

1
2
3
4
5
6
7
... INFO ... ReflectUtils 测试开始...
... INFO ... --- 1. 测试 newInstance (无参) ---
... INFO ... Student [无参] 构造方法被调用
... INFO ... 【s1 结果】: Student[name=null, age=null, toolType=null]
... INFO ... --- 2. 测试 newInstance (多参) ---
... INFO ... Student [String, Integer] 构造方法被调用
... INFO ... 【s2 结果】: Student[name=张三, age=20, toolType=null]

分析newInstance 成功地根据我们传入的参数,自动调用了对应的构造方法。

8.3.2. 特殊场景:newInstanceIfPossibleint.class

【二开思考】:如果我拿到的 Class 是一个 原始类型(如 int.class)或者一个 接口,它们没有构造方法,newInstance 会怎么办?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 在 testInstantiation() 方法中继续添加

console.info("--- 3. 测试 newInstance (失败场景) ---");
try {
// int.class (原始类型) 没有构造方法
ReflectUtils.newInstance(int.class);
} catch (Exception e) {
console.error("【newInstance(int.class) 失败】: {}", e.getMessage());
}

console.info("--- 4. 测试 newInstanceIfPossible (健壮) ---");
// Hutool 提供的“尽力而为”方法
// 如果是原始类型,返回其默认值
Integer intVal = ReflectUtils.newInstanceIfPossible(int.class);
console.info("【newInstanceIfPossible(int.class) 结果】: {}", intVal);

运行 main 方法(接上文输出)

1
2
3
4
... INFO ... --- 3. 测试 newInstance (失败场景) ---
... ERROR ... 【newInstance(int.class) 失败】: No constructor found for [int]
... INFO ... --- 4. 测试 newInstanceIfPossible (健壮) ---
... INFO ... 【newInstanceIfPossible(int.class) 结果】: 0

结论newInstanceIfPossible 是一个“容错”方法。在不确定 Class 类型是否能被实例化时(例如在非常通用的泛型工具中),使用它比 newInstance 更安全,它会返回 null 或原始类型的默认值,而不是抛出异常。


8.4. Hutool 基础(二):无视 private 的字段访问

8.4.1. 为何要读写 private 字段?

假设我们要为 Student 类增加一个“数据脱敏”的功能。我们希望在查询 Student 对象并返回给前端时,自动将 name 字段的值(如 “张三”)替换为 “张*”。

我们可能会在 SysUserServiceImpl 这样的服务层中获取到 List<Student>,然后尝试去脱敏:

1
2
3
4
5
6
7
8
// 痛点代码 (Bad Practice)
List<Student> list = studentMapper.selectList();
for (Student student : list) {
// 编译失败!'name' is private in 'Student'
String name = student.name;
// 编译失败!'name' is private in 'Student'
student.name = "张*";
}

我们失败了,因为 name 字段是 private 的。我们 无法在外部(如 StudentServiceImpl)直接读写它。

【解决方案】

  • 方案 A(侵入式):回去修改 Student 类,为 name 增加 public getName()setName()
  • 方案 B(反射)在不修改 Student 类源码 的前提下,使用反射强行读写 private 字段。

在框架开发中(例如 RVP 的 common-sensitive 脱敏组件、common-translation 翻译组件),方案 B 是唯一的选择,因为它必须在“不侵入”业务实体类的前提下,实现对任意 private 字段的读写。

8.4.2. 实战:getFieldsModifier 修饰符

Hutool 的 ReflectUtil.getFields() 可以获取一个类(及其父类)的 所有 字段,无论 public 还是 private

我们在 ReflectUtilsTest.java 中创建 testFields 方法:

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
29
30
31
32
33
34
// ...
import java.lang.reflect.Field; // 导入 Field
import java.lang.reflect.Modifier; // 导入 Modifier
// ...
public class ReflectUtilsTest {
// ... main, Student, Tool ...
public static void main(String[] args) {
// ...
// testInstantiation();
testFields();
}

/**
* 测试 2:访问字段 (Fields)
*/
public static void testFields() {
console.info("--- 5. 测试 getFields (获取所有字段) ---");

// 1. 获取所有字段 (Hutool 会处理缓存)
Field[] fields = ReflectUtils.getFields(Student.class);

for (Field field : fields) {
// getModifiers() 返回的是一个 int 值
int mod = field.getModifiers();

console.info("字段: [{}], 修饰符: {}, 类型: {}",
field.getName(),
Modifier.toString(mod), // 使用 Modifier.toString() 转义
field.getType().getSimpleName()
);
}
}
// ...
}

运行 main 方法,控制台输出

1
2
3
4
5
... INFO ... --- 5. 测试 getFields (获取所有字段) ---
... INFO ... 字段: [name], 修饰符: private, 类型: String
... INFO ... 字段: [age], 修饰符: public, 类型: Integer
... INFO ... 字段: [SCORE], 修饰符: public static final, 类型: long
... INFO ... 字段: [tool], 修饰符: private, 类型: Tool

分析:我们成功获取了所有字段,包括 privatenametoolModifier.toString() 可以帮我们直观地看到修饰符。

8.4.3. 实战:setFieldValue (写私有字段)

现在我们来解决 8.4.1 中的“脱敏”痛点。setFieldValue 会自动处理 setAccessible(true),强行写入私有字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ... 在 testFields() 方法中继续添加 ...

console.info("--- 6. 测试 setFieldValue (写私有字段) ---");
Student s = ReflectUtils.newInstance(Student.class, "张三", 20);
console.info("【修改前】: {}", s);

// 解决“脱敏”痛点:强行写入 private "name" 字段
try {
ReflectUtils.setFieldValue(s, "name", "张*");
ReflectUtils.setFieldValue(s, "age", 99); // public 字段当然也可以
} catch (Exception e) {
console.error("设置失败: {}", e.getMessage());
}

console.info("【修改后】: {}", s);
// ...

运行 main 方法(接上文输出)

1
2
3
4
5
... INFO ... --- 6. 测试 setFieldValue (写私有字段) ---
... INFO ... Student [无参] 构造方法被调用
... INFO ... Student [String, Integer] 构造方法被调用
... INFO ... 【修改前】: Student[name=张三, age=20, toolType=null]
... INFO ... 【修改后】: Student[name=张*, age=99, toolType=null]

结论setFieldValue 成功无视了 private 限制,修改了 name 字段。

8.4.4. 实战:getFieldValue (读私有字段)

同理,getFieldValue 可以强行读取 private 字段的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ... 在 testFields() 方法中继续添加 ...

console.info("--- 7. 测试 getFieldValue (读私有字段) ---");
// s 对象现在是 {name="张*", age=99}

// 强行读取 private "name" 字段
Object nameValue = ReflectUtils.getFieldValue(s, "name");
console.info("【读 name】: {}", nameValue);

// 强行读取 public "age" 字段
Object ageValue = ReflectUtils.getFieldValue(s, "age");
console.info("【读 age】: {}", ageValue);

// 强行读取 public static final "SCORE" 字段
// 读静态字段时,第一个参数传 Class
Object scoreValue = ReflectUtils.getFieldValue(Student.class, "SCORE");
console.info("【读 SCORE】: {}", scoreValue);
// ...

运行 main 方法(接上文输出)

1
2
3
4
... INFO ... --- 7. 测试 getFieldValue (读私有字段) ---
... INFO ... 【读 name】: 张*
... INFO ... 【读 age】: 99
... INFO ... 【读 SCORE】: 100

场景总结getFieldValuesetFieldValue框架开发者(而非业务开发者)的利器,专门用于实现 数据脱敏、数据翻译、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())。
  • AOPsetName 方法上可能加了 @Log@Transactional 等注解。
  • 原则优先调用 public 方法(invoke),走投无路时(没有 setter 或必须绕过 AOP)才操作 private 字段(setFieldValue

8.5.1. 实战:invoke(student, "setName", "...")

invoke 允许我们通过“方法名字符串”来执行方法。

我们在 ReflectUtilsTest.java 中创建 testInvoke 方法:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// ...
import java.lang.reflect.Method; // 导入 Method
// ...
public class ReflectUtilsTest {
// ... main, testFields ...
public static void main(String[] args) {
// ...
// testFields();
testInvoke();
}

/**
* 测试 3:动态方法调用 (invoke)
*/
public static void testInvoke() {
console.info("--- 8. 测试 getMethod (获取方法) ---");
// 1. 准备一个 student 实例
Student s = ReflectUtils.newInstance(Student.class);

// 2. 获取方法
// 必须精确指定参数类型 (String.class),Hutool 才能找到
Method setNameMethod = ReflectUtils.getMethod(
Student.class, "setName", String.class
);
console.info("【getMethod 结果】: {}", setNameMethod.getName());

console.info("--- 9. 测试 invoke (调用方法) ---");
// 3. 方式一:通过 Method 对象调用
ReflectUtils.invoke(s, setNameMethod, "李四");
console.info("【invoke(Method) 后】: {}", s);

// 4. 方式二:【推荐】直接通过方法名调用
// Hutool 会自动查找 setName(String.class) 并执行
ReflectUtils.invoke(s, "setName", "王五");
console.info("【invoke(String) 后】: {}", s);

// 5. 调用有返回值的方法
Object name = ReflectUtils.invoke(s, "getName");
console.info("【invoke(getName) 结果】: {}", name);
}
// ...
}

运行 main 方法,控制台输出

1
2
3
4
5
6
7
... INFO ... --- 8. 测试 getMethod (获取方法) ---
... INFO ... Student [无参] 构造方法被调用
... INFO ... 【getMethod 结果】: setName
... INFO ... --- 9. 测试 invoke (调用方法) ---
... INFO ... 【invoke(Method) 后】: Student[name=李四, age=null, toolType=null]
... INFO ... 【invoke(String) 后】: Student[name=王五, age=null, toolType=null]
... INFO ... 【invoke(getName) 结果】: 王五

8.5.2. invoke vs invokeStatic

invoke 用于调用 实例方法(需要 student 对象),invokeStatic 用于调用 静态方法

1
2
3
4
5
6
// ... 在 testInvoke() 方法中继续添加 ...

console.info("--- 10. 测试 invokeStatic (调用静态方法) ---");
// 静态方法不需要实例,第一个参数传 Class
ReflectUtils.invokeStatic(Student.class, "clog");
// ...

运行 main 方法(接上文输出)

1
2
... INFO ... --- 10. 测试 invokeStatic (调用静态方法) ---
... INFO ... 静态方法 clog() 被调用! SCORE = 100

场景总结invoke 是框架的基石。RVP 中 TreeBuildUtilsinvokeGetter、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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 痛点代码
Student student = new Student();
String propertyName = "tool.type"; // 属性名是动态的
String value = "铅笔盒";

// 1. 我们必须手动解析
String[] parts = propertyName.split("\\."); // ["tool", "type"]
// 2. 手动 invoke
Object tool = ReflectUtils.invoke(student, "getTool");
if (tool == null) {
// 3. 必须手动处理 NullPointerException
tool = new Tool();
ReflectUtils.invoke(student, "setTool", tool);
}
// 4. 再调用第二层
ReflectUtils.invoke(tool, "setType", value);

这段代码是“灾难性”的:它无法通用,且充满了 null 检查。

8.6.2. RVP 源码解析:invokeSetter 如何 split('.') 循环 get 最后 set

RVP 的 ReflectUtils 完美封装了这个“多级访问”的逻辑。我们来看 invokeSetter 的源码(精简过):

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
// 位于 RVP 的 ReflectUtils.java
public static <E> void invokeSetter(Object obj, String propertyName, E value) {
Object object = obj;
// 1. 核心:按 "." 切割属性
String[] names = StringUtils.split(propertyName, "."); // ["tool", "type"]

// 2. 循环调用 getter
for (int i = 0; i < names.length; i++) {
if (i < names.length - 1) {
// (i=0): name = "tool"
// (i<1) => true:
// 拼接 "getTool"
String getterMethodName = GETTER_PREFIX + StringUtils.capitalize(names[i]);
// object = student.getTool()
object = invoke(object, getterMethodName);
} else {
// (i=1): name = "type"
// (i<1) => false:
// 拼接 "setType"
String setterMethodName = SETTER_PREFIX + StringUtils.capitalize(names[i]);
// 调用 tool.setType("铅笔盒")
Method method = getMethodByName(object.getClass(), setterMethodName);
invoke(object, method, value);
}
}
}

invokeGetter 的逻辑类似,只是它会一直 get 到最后一层并返回。

8.6.3. 【实战】invokeSetter 与“三大陷阱”

RVP 的封装虽然强大,但也带来了“约定”,如果约定不满足,就会崩溃。我们来实战 invokeSetter 并复现所有陷阱。

我们在 ReflectUtilsTest.java 中创建 testRvpInvoke 方法:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// ...
public class ReflectUtilsTest {
// ... main, testInvoke ...
public static void main(String[] args) {
// ...
// testInvoke();
testRvpInvoke();
}

/**
* 测试 4:RVP 核心 - 多级属性访问
*/
public static void testRvpInvoke() {
console.info("--- 11. 测试 RVP invokeSetter (多级) ---");
Student s = ReflectUtils.newInstance(Student.class, "小明", 10);

// 目标:动态设置 s.tool.type = "铅笔盒"
String property = "tool.type";
String value = "铅笔盒";

// 【陷阱 1: getTool() 不存在】
// ReflectUtils.invokeSetter(s, property, value);
// 此时会抛错: No such method: [getTool]

// 【陷阱 2: student.tool 对象为 null】
// 我们在 Student.java 中添加了 getTool() 和 setTool()
// 但 s.tool 默认为 null
// s.getTool() 返回 null, 下一步 invoke(null, "setType", ...) 导致 NullPointerException

// 【修复陷阱 2】: 必须确保中间对象已实例化
s.setTool(new Tool());

// 【陷阱 3: getType()/setType() 不存在】
// 此时 s.tool 不是 null 了,但 Tool 类是空的
// ReflectUtils.invokeSetter(s, property, value);
// 此时会抛错: No such method: [setType]

// 【修复陷阱 3】:
// (我们在 8.2.3 节的 Tool 类中已经添加了 getType/setType)

// --- 最终实战 ---
try {
ReflectUtils.invokeSetter(s, property, value);
console.info("【invokeSetter 成功后】: {}", s);
} catch (Exception e) {
console.error("【invokeSetter 失败】: {}", e.getMessage());
}
}
// ...
}

运行 main 方法,控制台输出

1
2
3
... INFO ... --- 11. 测试 RVP invokeSetter (多级) ---
... INFO ... Student [String, Integer] 构造方法被调用
... INFO ... 【invokeSetter 成功后】: Student[name=小明, age=10, toolType=铅笔盒]

结论invokeSetter 成功实现了 student.tool.type = "铅笔盒" 的嵌套设置。

8.6.4. 【RVP 核心】invokeGetter 实战(解决陷阱后)

invokeGetter 同样依赖这一套 get 链。

1
2
3
4
5
6
7
8
9
10
11
12
13
// ... 在 testRvpInvoke() 方法中继续添加 ...

console.info("--- 12. 测试 RVP invokeGetter (多级) ---");
// s 对象现在 { name="小明", tool={type="铅笔盒"} }

// 1. 获取单级属性
String name = ReflectUtils.invokeGetter(s, "name");
console.info("【invokeGetter('name')】: {}", name);

// 2. 获取多级属性
String toolType = ReflectUtils.invokeGetter(s, "tool.type");
console.info("【invokeGetter('tool.type')】: {}", toolType);
// ...

运行 main 方法(接上文输出)

1
2
3
... INFO ... --- 12. 测试 RVP invokeGetter (多级) ---
... INFO ... 【invokeGetter('name')】: 小明
... INFO ... 【invokeGetter('tool.type')】: 铅笔盒

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") 呢?

答案

  1. 统一性:RVP 开发者希望在框架内部统一使用 ReflectUtils 这一套 API。
  2. 简洁性invokeGetter(obj, "parentId")invoke(obj, "getParentId") 更符合“操作属性”的直觉(Hutool 6 也会推出 getFieldValuefield.get 方式)。
  3. 扩展性:虽然 TreeBuildUtils 目前只用了单级,但 RVP 框架的其他地方(如配置中心、数据绑定)可能需要多级访问,invokeGetter 提供了这种 向下兼容 的能力。

8.7. 本章总结

在本章中,我们聚焦于 ReflectUtils,它继承了 Hutool ReflectUtil 并提供了核心增强。

  1. Hutool 基础:我们掌握了 ReflectUtil 提供的三大基础能力:

    • newInstance:动态实例化(可自动匹配构造方法)。
    • get/setFieldValue:强行读写 private 字段(用于脱敏、ORM 等场景)。
    • invoke / invokeStatic:动态调用方法(AOP 和框架的基石)。
  2. RVP 核心增强:我们重点实战了 RVP 独有的 invokeGetterinvokeSetter

    • 价值:它们封装了 split('.') 逻辑,实现了对 多级嵌套属性(如 student.tool.type)的动态读写。
    • 陷阱:使用 RVP 的多级访问,必须 确保调用链上所有的 get 方法都存在,并且 中间对象不能为 null,否则会抛出异常。