前言:作为一名计算机专业的学生,我们在写业务代码时习惯了 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());
                }
            }
        }
    }
}

四、 深度思考:反射是把双刃剑

既然反射这么强大,为什么我们在写业务代码(如登录、计算)时不推荐使用?

  1. 编译器“瞎”了getMethod("nmae") 拼写错误在编译期不会报错,只有运行时才会崩溃,极其危险。

  2. 性能开销:虽然 JVM 优化了很多,但反射依然比直接调用要慢,且难以被 JIT 深度优化。

  3. 代码可读性差:满屏的 invokeObject 让人难以理解业务逻辑,IDE 的“查找引用”也会失效。

  4. 破坏封装:随意修改 private 字段会导致对象状态不可控。

总结黄金法则

反射是用来写框架(Frameworks)和工具(Tools)的,因为框架不知道用户会写什么类。 业务逻辑(Business Logic)应该尽量避免使用反射

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐