反射综合案例与框架思想 | JavaSE
反射综合案例与框架思想
一、学习目标
学完本章,你应该能够:
- 将
Class、Field、Constructor、Method组织成完整的反射知识模型。 - 解释为什么反射可以处理编译阶段未知的业务类型。
- 理解“通用代码”和“业务代码”之间的区别。
- 使用反射实现一个能够保存任意对象字段信息的简易框架。
- 理解反射在框架、对象映射、序列化等技术中的核心价值。
- 判断什么时候适合使用反射,什么时候应该继续使用普通 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 知识问答
- 为什么反射适合编写通用框架?
Object参数为什么不能单独解决对象结构未知的问题?Class、Constructor、Field、Method分别承担什么职责?- 什么叫运行时动态性?
- 为什么框架开发者通常无法提前知道所有业务类?
SaveObjectFrameWork为什么能够保存不同类型对象?- 反射的灵活性带来了哪些代价?
- 为什么普通业务代码不应该为了“高级”而滥用反射?
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)
);
}
}
回答:
object.getClass()解决了什么问题?- 为什么不能写死
Student.class? getDeclaredFields()的作用是什么?field.get(object)中为什么必须传object?- 这个方法为什么可以处理多种不同业务类型?
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()
);
}
}
回答:
- 代码能否工作?
- 它是不是通用框架?
- 新增
Product后是否需要修改? - 它违反了本章哪一个核心设计目标?
- 如何用反射重新设计?
7.5 综合训练
实现:
public static void printClassStructure(
Object object
)
要求输出:
类名
构造器:
...
字段:
字段类型 字段名
方法:
返回值类型 方法名(参数...)
要求综合使用:
Class
Constructor
Field
Method
不得针对具体业务类硬编码。
7.6 本章验收
能够闭卷画出:
Class
│
┌───────────┼───────────┐
↓ ↓ ↓
Constructor Field Method
│ │ │
newInstance get/set invoke
│ │ │
创建对象 操作数据 执行行为
并解释:
为什么反射让框架能够
“在不知道具体业务类型的情况下工作”
就算真正完成了反射阶段的核心学习。