Java 反射:从“黑魔法”到手写 Mini-JUnit
前言:作为一名计算机专业的学生,我们在写业务代码时习惯了
new Object()。但在接触 Spring、MyBatis 等框架时,总觉得它们像是有“读心术”——明明我只写了注解,它们怎么知道我要做什么?答案就是 Java 反射 (Reflection)。在这篇文章中,我将记录下我对反射的学习过程,从基础概念到手写一个迷你测试框架。
一、 什么是反射?
反射是 Java 语言赋予开发者的一把“万能钥匙”。通常我们在编译期就知道要使用哪个类,但在运行时 (Runtime),反射允许我们:
-
根据字符串动态加载一个类。
-
获取这个类的所有信息(属性、方法、构造器),哪怕是
private的。 -
动态地创建对象并执行方法。
它是所有现代 Java 框架的基石。
二、 反射的三大核心操作
1. 获取“图纸”:Class 对象
想要解剖一个类,首先要拿到它的 Class 对象。我们有三种方式:
-
已知类名(编译期硬编码):
User.class -
已知对象(运行时动态):
obj.getClass()—— 当我们手里只有一个Object实例时用这个。 -
已知全类名字符串(框架常用):
Class.forName("com.example.User")—— 最灵活,常用于读取配置文件。
2. 动态创建对象:Constructor
在 Java 9 之后,clazz.newInstance() 已过时,推荐使用更严谨的 Constructor 来创建对象。
Class<?> clazz = User.class;
// 获取参数为 (String, int) 的构造器
Constructor<?> constructor = clazz.getConstructor(String.class, int.class);
// 实例化对象 ("Jerry", 25)
Object user = constructor.newInstance("Jerry", 25);
3. 暴力访问:Field 与 Method
这是反射最像“黑魔法”的地方。即便是 private 的属性或方法,反射也能强行访问。
核心方法:
-
getDeclaredFields()/getDeclaredMethods(): 获取本类声明的所有成员(包含私有)。 -
setAccessible(true): 关键一步,关闭安全检查,打破封装。
代码示例:修改私有属性
// 假设 user 对象目前的 age 是 20
Field field = clazz.getDeclaredField("age");
// 1. 拿到通行证
field.setAccessible(true);
// 2. 读取值 (注意:是 field 去读 user,像体温计去测病人)
Object value = field.get(user);
// 3. 修改值
field.set(user, 88);
三、 实战:手写一个 Mini-JUnit 框架
为了真正理解“反射 + 注解”的威力,我尝试手写了一个极简版的单元测试框架。
1. 定义注解 @MyTest
注解本身只是一个元数据(标签),它需要保留到运行时才能被反射读取。
@Retention(RetentionPolicy.RUNTIME) // 关键:让它活到运行时
@Target(ElementType.METHOD) // 限制:只能贴在方法上
public @interface MyTest {
}
2. 编写业务测试类(小白鼠)
这是我们用来测试的目标类,包含正常的逻辑和故意失败的断言。
public class CalculatorTest {
@MyTest
public void testAdd() {
if (1 + 1 != 2) throw new RuntimeException("加法错了!");
}
@MyTest
public void testWrong() {
if (1 + 1 != 5) throw new RuntimeException("故意让它失败");
}
public void cleanUp() {
// 没贴标签,不应该运行
}
}
3. 编写测试运行器(框架核心)
这是框架的灵魂。它的逻辑是:扫描类 -> 找注解 -> 实例化 -> 运行 -> 捕获异常。
public class TestRunner {
public static void runTests(Class<?> testClass) throws Exception {
// 1. 实例化测试类
Object testInstance = testClass.getConstructor().newInstance();
// 2. 拿到所有方法
Method[] methods = testClass.getDeclaredMethods();
for (Method method : methods) {
// 3. 只要贴了 @MyTest 的方法
if (method.isAnnotationPresent(MyTest.class)) {
try {
// 4. 反射执行
method.invoke(testInstance);
System.out.println("✅ " + method.getName() + " Passed");
} catch (Exception e) {
// 5. 捕获异常(反射异常包裹了一层,要取 Cause)
System.err.println("🔴 " + method.getName() + " Failed: " + e.getCause().getMessage());
}
}
}
}
}
四、 深度思考:反射是把双刃剑
既然反射这么强大,为什么我们在写业务代码(如登录、计算)时不推荐使用?
-
编译器“瞎”了:
getMethod("nmae")拼写错误在编译期不会报错,只有运行时才会崩溃,极其危险。 -
性能开销:虽然 JVM 优化了很多,但反射依然比直接调用要慢,且难以被 JIT 深度优化。
-
代码可读性差:满屏的
invoke和Object让人难以理解业务逻辑,IDE 的“查找引用”也会失效。 -
破坏封装:随意修改
private字段会导致对象状态不可控。
总结黄金法则:
反射是用来写框架(Frameworks)和工具(Tools)的,因为框架不知道用户会写什么类。 业务逻辑(Business Logic)应该尽量避免使用反射。
更多推荐
所有评论(0)