反射综合案例与框架思想 | JavaSE

反射综合案例与框架思想

一、学习目标

学完本章,你应该能够:

  • ClassFieldConstructorMethod 组织成完整的反射知识模型。
  • 解释为什么反射可以处理编译阶段未知的业务类型。
  • 理解“通用代码”和“业务代码”之间的区别。
  • 使用反射实现一个能够保存任意对象字段信息的简易框架。
  • 理解反射在框架、对象映射、序列化等技术中的核心价值。
  • 判断什么时候适合使用反射,什么时候应该继续使用普通 Java 调用。

二、核心知识

2.1 从四个反射 API 到一个完整系统

前面已经分别学习了:

Class
Constructor
Field
Method

单独学习时,它们分别解决:

Class
→ 这个对象到底是什么类型?

Constructor
→ 怎么动态创建对象?

Field
→ 这个对象里面有什么数据?

Method
→ 这个对象可以执行什么行为?

组合起来就是:

                   Class
                     │
        ┌────────────┼────────────┐
        ↓            ↓            ↓
 Constructor       Field        Method
        ↓            ↓            ↓
   newInstance    get/set       invoke
        ↓            ↓            ↓
     创建对象      操作数据      调用方法

这就是 Java 基础反射体系的完整主干。

但是,仅仅会调用 API 还不能说明真正理解了反射。

真正重要的问题是:

为什么 Java 要提供这么一套看起来比普通代码复杂得多的机制?

答案就在:

框架(Framework)与通用程序设计。


2.2 普通代码为什么难以处理“任意对象”

假设我们现在要把 Student 对象保存到文件:

public class Student {

    private String name;
    private int age;
    private String hobby;

    // 省略构造器
}

最直接的方式:

public static void saveStudent(Student student) {
    System.out.println("name=" + student.getName());
    System.out.println("age=" + student.getAge());
    System.out.println("hobby=" + student.getHobby());
}

没有任何问题。

但是现在又出现:

public class Dog {

    private String name;
    private int age;
}

我们可能又写:

public static void saveDog(Dog dog) {
    System.out.println("name=" + dog.getName());
    System.out.println("age=" + dog.getAge());
}

再出现:

Teacher
User
Product
Order
Article

怎么办?

继续:

saveTeacher()
saveUser()
saveProduct()
saveOrder()
saveArticle()
……

这说明代码存在一个明显问题:

每增加一种业务类型,工具代码都需要跟着修改。

这不是真正的通用框架。


2.3 框架真正想要什么

我们真正希望得到:

public static void saveObject(Object object) {
}

不管传入:

saveObject(student);
saveObject(dog);
saveObject(teacher);
saveObject(user);
saveObject(product);

都能够自动完成:

获取真实类型
    ↓
获取全部字段
    ↓
获取字段名称
    ↓
获取字段值
    ↓
写入文件

问题来了:

参数类型只有:

Object object

编译器怎么知道里面到底有哪些字段?

这就是反射登场的地方。


2.4 Object + Reflection = 通用处理能力

首先:

Class<?> clazz = object.getClass();

无论传入什么对象,都可以得到其真正的:

运行时类型

例如:

student → Student.class
dog     → Dog.class
teacher → Teacher.class

然后:

Field[] fields =
        clazz.getDeclaredFields();

框架不需要提前知道:

Student 有几个字段
Dog 有几个字段
Teacher 有几个字段

运行时直接问 Class:

你自己告诉我。

然后:

for (Field field : fields) {
    String name = field.getName();
    Object value = field.get(object);
}

于是:

具体业务类型
        ↓
Class
        ↓
Field[]
        ↓
字段名称 + 字段值

全部变成运行时数据。

这就是反射最核心的价值之一:

把原本写死在源代码中的“类型结构”,转换成程序运行期间可以动态读取和处理的数据。


2.5 什么叫框架

框架可以先建立一个入门级理解:

框架是一套提前设计好的通用程序结构和规则,开发者按照规则提供自己的业务代码,由框架负责组织和调用这些代码。

普通 API 往往是:

你的代码
   ↓
调用 API

例如:

Math.max(a, b);

而框架经常是:

框架
 ↓
发现你的代码
 ↓
分析你的代码
 ↓
调用你的代码

例如以后会遇到:

JUnit 找到 @Test 方法
Spring 找到 Bean
Spring MVC 找到 Controller
Jackson 分析 Java 对象
MyBatis 处理对象映射

这种“框架控制程序整体流程”的思想,是后面学习:

控制反转(Inversion of Control, IoC)

的重要基础。

这里先理解思想,不提前展开 Spring。


三、使用方法

3.1 准备三个完全不同的业务类

Dog

public class Dog {

    private String name;
    private int age;

    public Dog(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

Student

public class Student {

    private String name;
    private int age;
    private String hobby;

    public Student(
            String name,
            int age,
            String hobby
    ) {
        this.name = name;
        this.age = age;
        this.hobby = hobby;
    }
}

Teacher

public class Teacher {

    private String name;
    private int age;
    private double salary;
    private String subject;

    public Teacher(
            String name,
            int age,
            double salary,
            String subject
    ) {
        this.name = name;
        this.age = age;
        this.salary = salary;
        this.subject = subject;
    }
}

三个类:

字段数量不同
字段类型不同
业务含义不同

但我们的框架只写:

saveObject(Object object)

3.2 第一版:获取对象真实类型

public static void saveObject(
        Object object
) {
    Class<?> clazz =
            object.getClass();

    System.out.println(
            clazz.getSimpleName()
    );
}

传入:

saveObject(
        new Dog("小黑", 3)
);

saveObject(
        new Student(
                "小明",
                18,
                "篮球"
        )
);

框架可以动态知道:

Dog
Student

第一步完成:

Object
 ↓
Class

3.3 第二版:扫描所有字段

继续:

public static void saveObject(
        Object object
) {
    Class<?> clazz =
            object.getClass();

    Field[] fields =
            clazz.getDeclaredFields();

    for (Field field : fields) {
        System.out.println(
                field.getName()
        );
    }
}

现在:

Dog
→ name
→ age

Student
→ name
→ age
→ hobby

框架依然没有出现:

if (object instanceof Dog)

也没有:

if (object instanceof Student)

这就是通用性。


3.4 第三版:读取字段值

private 字段默认受到访问控制。

在本课程这种普通应用代码场景下,可以:

field.setAccessible(true);

然后:

Object value =
        field.get(object);

组合:

for (Field field :
        clazz.getDeclaredFields()) {

    field.setAccessible(true);

    String fieldName =
            field.getName();

    Object fieldValue =
            field.get(object);

    System.out.println(
            fieldName
                    + "="
                    + fieldValue
    );
}

例如:

Dog dog =
        new Dog("小黑", 3);

saveObject(dog);

效果类似:

name=小黑
age=3

3.5 第四版:保存到文件

原课程的核心案例就是:

将任意对象的字段名和对应字段值写入文件。

JDK 21 版本可以结合前面已经学习过的:

try-with-resources

进行资源管理。

import java.io.FileOutputStream;
import java.io.PrintStream;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;

public class SaveObjectFrameWork {

    public static void saveObject(
            Object object
    ) throws Exception {

        try (PrintStream out =
                     new PrintStream(
                             new FileOutputStream(
                                     "obj.txt",
                                     true
                             ),
                             true,
                             StandardCharsets.UTF_8
                     )) {

            Class<?> clazz =
                    object.getClass();

            out.println(
                    "=========="
                            + clazz.getSimpleName()
                            + "=========="
            );

            Field[] fields =
                    clazz.getDeclaredFields();

            for (Field field : fields) {
                field.setAccessible(true);

                String fieldName =
                        field.getName();

                Object fieldValue =
                        field.get(object);

                out.println(
                        fieldName
                                + "="
                                + fieldValue
                );
            }
        }
    }
}

这里真正决定程序通用性的不是:

PrintStream

而是:

Class<?> clazz =
        object.getClass();

Field[] fields =
        clazz.getDeclaredFields();

Object value =
        field.get(object);

3.6 使用这个框架

public class ReflectionDemo {

    public static void main(
            String[] args
    ) throws Exception {

        Dog dog =
                new Dog(
                        "小黑",
                        3
                );

        Student student =
                new Student(
                        "小明",
                        18,
                        "爱问问题"
                );

        Teacher teacher =
                new Teacher(
                        "小红",
                        25,
                        8000,
                        "Java"
                );

        SaveObjectFrameWork
                .saveObject(dog);

        SaveObjectFrameWork
                .saveObject(student);

        SaveObjectFrameWork
                .saveObject(teacher);
    }
}

假设连续保存,文件可能得到:

==========Dog==========
name=小黑
age=3

==========Student==========
name=小明
age=18
hobby=爱问问题

==========Teacher==========
name=小红
age=25
salary=8000.0
subject=Java

注意:

SaveObjectFrameWork
        .saveObject(...)

完全没有针对:

Dog
Student
Teacher

分别写保存逻辑。


3.7 框架代码逐层分析

整个过程只有五步:

1. Object 接收任意对象
        ↓
2. object.getClass()
        ↓
3. getDeclaredFields()
        ↓
4. field.getName()
   field.get(object)
        ↓
5. 写入文件

可以压缩成:

任意对象
   ↓
运行时类型
   ↓
运行时结构
   ↓
运行时数据
   ↓
统一处理

这一条链路必须真正理解。


四、原理与进阶

4.1 “通用”到底意味着什么

框架不是因为:

saveObject(Object object)

用了 Object 就自动变成通用程序。

如果里面写:

if (object instanceof Student student) {
    ...
} else if (object instanceof Dog dog) {
    ...
}

本质仍然:

知道 Student
知道 Dog
依赖具体业务类型

真正的通用代码应该尽量:

依赖抽象能力
+
依赖运行时元数据
+
避免依赖具体业务类型

SaveObjectFrameWork 的关键是:

object.getClass()

以及:

clazz.getDeclaredFields()

而不是 Object 参数本身。


4.2 这是不是完整的对象持久化框架

不是。

这是一个:

教学级简易对象保存框架。

真正成熟的对象持久化、序列化框架还要解决大量问题,例如:

null
继承字段
static 字段
transient 字段
集合
数组
嵌套对象
循环引用
日期时间
编码
格式规范
异常处理
安全边界
反序列化
兼容性
性能

因此:

SaveObjectFrameWork

的意义不是:

我们已经实现了 Jackson。

而是:

通过最小案例理解框架为什么需要反射。


4.3 反射的代价

反射带来了极强动态性,但同时存在代价:

编译期类型检查减少
      ↓
错误可能推迟到运行期

访问控制更复杂
      ↓
可能出现 IllegalAccessException

API 更复杂
      ↓
代码可读性降低

运行时动态处理
      ↓
通常存在额外调用成本

所以正确思想不是:

反射很高级
→ 所有代码都应该使用反射

而是:

普通明确业务逻辑
→ 优先普通 Java

真正需要运行时动态性
→ 考虑反射

4.4 反射与框架的本质关系

反射特别适合解决:

框架开发者
不知道未来业务类型

的问题。

例如:

框架开发时:

不知道以后会有 User
不知道以后会有 Order
不知道以后会有 Product
不知道以后会有 Article

但是框架仍然能够:

获得 Class
获得 Field
获得 Method
获得 Constructor
获得 Annotation

于是:

框架开发者与业务类之间实现了解耦。

这才是反射最重要的工程价值。


五、实践应用

5.1 序列化框架

输入:

User user = ...

框架自动输出:

{
  "name": "LingXi",
  "age": 21
}

框架必须知道:

字段叫什么
字段值是什么
字段应该怎样转换

反射可以参与提供这些运行时信息。


5.2 ORM

数据库:

id       → 1
username → LingXi
age      → 21

Java:

User user = ...

框架需要把:

数据库数据
   ↓
Java 对象

进行映射。

这同样离不开对:

类
构造器
字段
方法

等结构的动态分析。


5.3 依赖注入

未来 Spring 看到:

@Service
public class UserService {
}

需要:

找到这个类
↓
分析它
↓
创建对象
↓
管理对象
↓
注入依赖

其中反射就是重要基础技术之一。


5.4 测试框架

JUnit 能够:

找到测试类
↓
找到测试方法
↓
运行方法
↓
收集执行结果

这里就会使用类似:

Class
Method
Annotation

的机制。

下一章开始学习的:

注解

会进一步把:

“程序结构”

升级为:

“程序结构 + 元数据”

六、常见问题

6.1 学反射是不是为了以后天天写 getDeclaredField?

不是。

学习反射真正的目标是理解:

运行时类型信息
+
动态操作
+
框架通用性

这些思想。

以后业务开发中更多时候是在:

使用别人已经封装好的框架。


6.2 Object 已经能接收任何对象,为什么还需要反射?

因为:

Object object

只能让参数:

接得住任意对象。

但普通 Object API 并不知道业务类自己声明了:

哪些字段
哪些方法
哪些构造器
哪些注解

反射解决的是:

动态分析对象真实结构。


6.3 反射是不是可以随便破坏 private?

不能这样理解。

在普通课堂项目中:

setAccessible(true)

经常能够关闭普通语言访问检查。

但 Java 21 中仍受:

模块边界
包开放关系
访问规则

等机制约束。


6.4 反射框架是不是越通用越好?

不是。

通用性通常意味着:

抽象程度提高
复杂度提高
运行期行为增加
Debug 难度提高

应该针对真正重复、稳定且值得抽象的问题构建框架。


6.5 框架和工具类有什么区别?

没有一条绝对的代码行数界线。

但通常:

工具类
→ 你主动调用它完成局部功能

框架
→ 它定义整体规则,并在适当时机调用你的代码

本章 SaveObjectFrameWork 仍然是一个极简教学案例,它主要用于建立:

框架通用技术

的思想,而不是严格模拟成熟大型框架的完整控制模型。


七、练习与验收

7.1 知识问答

  1. 为什么反射适合编写通用框架?
  2. Object 参数为什么不能单独解决对象结构未知的问题?
  3. ClassConstructorFieldMethod 分别承担什么职责?
  4. 什么叫运行时动态性?
  5. 为什么框架开发者通常无法提前知道所有业务类?
  6. SaveObjectFrameWork 为什么能够保存不同类型对象?
  7. 反射的灵活性带来了哪些代价?
  8. 为什么普通业务代码不应该为了“高级”而滥用反射?

7.2 代码阅读

阅读:

public static void printObject(
        Object object
) throws Exception {

    Class<?> clazz =
            object.getClass();

    for (Field field :
            clazz.getDeclaredFields()) {

        field.setAccessible(true);

        System.out.println(
                field.getName()
                        + "="
                        + field.get(object)
        );
    }
}

回答:

  1. object.getClass() 解决了什么问题?
  2. 为什么不能写死 Student.class
  3. getDeclaredFields() 的作用是什么?
  4. field.get(object) 中为什么必须传 object
  5. 这个方法为什么可以处理多种不同业务类型?

7.3 手写代码

闭卷实现:

public static void saveObject(
        Object object
)

要求:

  • 获取对象真实 Class。
  • 获取所有声明字段。
  • 输出字段名称。
  • 输出字段类型。
  • 输出字段值。
  • 不允许出现任何具体业务类判断。

7.4 Debug

下面代码:

public static void saveObject(
        Object object
) {
    if (object instanceof Student student) {
        System.out.println(
                student.getName()
        );
    } else if (object instanceof Teacher teacher) {
        System.out.println(
                teacher.getName()
        );
    }
}

回答:

  1. 代码能否工作?
  2. 它是不是通用框架?
  3. 新增 Product 后是否需要修改?
  4. 它违反了本章哪一个核心设计目标?
  5. 如何用反射重新设计?

7.5 综合训练

实现:

public static void printClassStructure(
        Object object
)

要求输出:

类名

构造器:
...

字段:
字段类型 字段名

方法:
返回值类型 方法名(参数...)

要求综合使用:

Class
Constructor
Field
Method

不得针对具体业务类硬编码。

7.6 本章验收

能够闭卷画出:

                    Class
                      │
          ┌───────────┼───────────┐
          ↓           ↓           ↓
     Constructor     Field       Method
          │           │           │
    newInstance     get/set     invoke
          │           │           │
       创建对象      操作数据     执行行为

并解释:

为什么反射让框架能够
“在不知道具体业务类型的情况下工作”

就算真正完成了反射阶段的核心学习。