20260814-笔记.md 54 KB

20260814 课堂笔记 — 反射 Method 进阶:暴力反射调用私有方法与模拟 JUnit 单元测试框架

  • 日期:2026-08-14
  • 项目:c260814
  • 包路径:homework0813(默认包根目录下的具名包)
  • 作者:WanJL
  • 内容概览:0813 作业进阶讲解——Student 实体类(回顾 4 种字段权限 + 4 个构造方法 + 私有实例方法 read / 私有静态方法 write + 新增 3 个 test 前缀测试方法 testAdd/testHello/testFail)+ MethodPrivateTest(暴力反射调用私有实例方法 read——invoke 传对象、暴力反射调用私有静态方法 write——invoke 传 null、封装通用调用工具 invokeAnyMethod——用 instanceof Class<?> 判断"传的是 Class 则调静态方法、传的是对象则调实例方法"+ args[i].getClass() 动态计算参数类型数组)+ MiniJUnitTest(模拟 JUnit 单元测试框架——getConstructor().newInstance() 反射创建被测对象 → getDeclaredMethods() 遍历全部方法 → 按 test 方法名前缀识别测试方法 → setAccessible(true) + invoke() 执行 → 捕获 InvocationTargetException 统计通过/失败个数,底层原理:注解识别 + 反射调用)

目录

  1. 项目结构概述:0813 作业进阶——Method 反射的综合应用
  2. Student 实体类解析:反射目标回顾 + 新增测试方法
  3. MethodPrivateTest 演示:暴力反射调用私有方法(实例 + 静态)
  4. 暴力反射调用私有实例方法 read():invoke 传对象
  5. 暴力反射调用私有静态方法 write():invoke 传 null
  6. 通用工具 invokeAnyMethod:instanceof 判断静态/实例 + 动态计算参数类型
  7. MiniJUnitTest 演示:模拟 JUnit 单元测试框架(反射终极应用)
  8. 反射创建被测对象:getConstructor().newInstance()
  9. getDeclaredMethods() 遍历 + test 前缀识别测试方法
  10. setAccessible(true) + invoke 执行测试方法
  11. InvocationTargetException 包装异常:getCause() 获取真实异常
  12. 通过/失败统计:模拟测试报告输出
  13. 知识点全景总结
  14. 随堂练习要点
  15. 拓展阅读

1. 项目结构概述:0813 作业进阶——Method 反射的综合应用

概念 —— 一个包内三个类,把上一课的 Method 反射学以致用

c260814 项目把上一课(08-13 反射 Method)的知识点落到两个实际场景:MethodPrivateTest 演示暴力反射调用私有方法(并把调用过程封装成通用工具),MiniJUnitTest 演示用反射模拟 JUnit 单元测试框架(遍历方法 → 识别测试方法 → 反射执行 → 统计结果)。Student 实体类既是两个演示的反射目标,又新增了 3 个 test 前缀方法专门供 MiniJUnit 扫描执行。

c260814/
└── src/
    └── homework0813/                  ← 包名 homework0813(0813 作业的进阶应用)
        ├── Student.java               ← 反射目标实体类(字段/构造/私有方法 + 测试方法)
        ├── MethodPrivateTest.java     ← 暴力反射调用私有方法 + 通用调用工具 invokeAnyMethod
        └── MiniJUnitTest.java         ← 模拟 JUnit 单元测试框架(测试方法扫描/执行/统计)

要点:

  • 项目类型是"每日默认包 + 具名包":包名为 homework0813——表示这是 08-13 课后作业的进阶练习,三个类在同一个包下,相互可以直接引用(无需 import 同包类)
  • 三个类各司其职:Student 提供"被测对象与测试方法",MethodPrivateTest 演示"私有方法怎么反射调用",MiniJUnitTest 演示"测试框架怎么用反射批量执行测试"
  • 本课是反射 Method 的收尾应用:上一课学了 API(getDeclaredMethod/invoke/setAccessible),本课把 API 组合成"能干活"的工具和框架——反射的价值在于组合应用

2. Student 实体类解析:反射目标回顾 + 新增测试方法

概念 —— 一个同时满足"私有方法反射"与"测试框架反射"的实体类

Student 类是三个演示的共用反射目标,字段/构造方法与上一课(08-13)基本一致(private/public/public static 三种权限字段 + 4 个构造方法 + 私有实例方法 read / 私有静态方法 write + 公开方法 study),新增了 3 个 test 开头的方法——它们是 MiniJUnit 框架扫描并执行的"模拟测试用例"。

// 来源:homework0813/Student.java
public class Student {
    private Integer id;
    private String name;
    private Integer age;
    private String gender;
    public String email;
    public String phone;
    public static String school;

    public Student() { }                                    // public 无参构造(MiniJUnit 反射创建对象用)

    private Student(String name, Integer id) { }            // private 二参构造(上一课暴力反射目标)

    public Student(Integer id, String name, Integer age, String gender) { }  // public 四参构造

    public Student(Integer id, String name, Integer age, String gender, String email, String phone) { }  // public 六参构造

    // …… 7 组 getter/setter(id/name/age/gender/email/phone 实例 + school 静态)……

    private void read(){                                    // private 实例方法:输出 name+"读......."
        System.out.println(this.name+"读.......");
    }

    private static void write(){                            // private 静态方法:输出"静态方法...写..............."
        System.out.println("静态方法...写...............");
    }

    public void study(){                                    // public 实例方法:输出 name+"在学习....."
        System.out.println(name+"在学习.....");
    }

    // 新增 3 个 test 前缀方法:MiniJUnit 扫描识别的"模拟测试用例"
    private void testAdd() {                                // 测试用例①:加法计算
        System.out.println("测试:加法计算");
        int a = 1 + 1;
        if (a != 2) throw new RuntimeException("加法计算错误");
    }
    private void testHello() {                              // 测试用例②:打招呼
        System.out.println("测试:打招呼 " + this.name);
    }
    private void testFail() {                               // 测试用例③:故意失败
        throw new RuntimeException("故意失败的方法");
    }
}

新增测试方法对照表

测试方法 权限 行为 执行结果
testAdd() private 输出"测试:加法计算",计算 1+1,若不等于 2 抛异常 通过(1+1==2,不抛异常)
testHello() private 输出"测试:打招呼 " + name 通过(只是打印,不抛异常)
testFail() private 直接 throw new RuntimeException("故意失败的方法") 失败(抛异常 → invoke 包装成 InvocationTargetException)

字段/方法权限设计对照表(回顾上一课)

成员 权限 作用 / 对应演示
private Integer id private 私有字段——Field 反射 getDeclaredFields() 能拿到
private String name private 私有字段——暴力反射 Field 读取/修改目标;testHello 打印 name
private Integer age private 私有字段
private String gender private 私有字段
public String email public 公有字段——getField + get/set 读取/修改
public String phone public 公有字段
public static String school public + static 静态字段——get(null) / set(null, 值)
read() private 实例方法 暴力反射 Method 调用目标——getDeclaredMethod("read") + setAccessible(true) + invoke(对象)
write() private 静态方法 暴力反射 Method 调用目标——getDeclaredMethod("write") + setAccessible(true) + invoke(null)
study() public 实例方法 公开方法——普通/反射均可调用
testAdd/testHello/testFail private 测试方法 MiniJUnit 扫描执行目标——getDeclaredMethods() 遍历 + test 前缀识别

要点:

  • 私有方法不写 getter/setter:read()/write()/testXxx 都是 private 且无公开入口,普通代码无法调用——只有反射(暴力反射)能调用它们,这正是本课"为什么需要反射"的典型场景
  • 测试方法刻意设计"两成一败":testAdd 通过、testHello 通过、testFail 故意抛异常——这样 MiniJUnit 统计结果时能演示"成功 2 个、失败 1 个"
  • testFail 抛的是 RuntimeException:被调方法内部抛异常时,反射 invoke 不会直接抛 RuntimeException,而是包装成 InvocationTargetException 抛出——这是本课重点(见第 11 节)

3. MethodPrivateTest 演示:暴力反射调用私有方法(实例 + 静态)

概念 —— 一个 main 方法三个任务,私有方法反射的"全家福"

MethodPrivateTest 的 main 方法演示了暴力反射调用私有方法的三种完整路径:调用私有实例方法(invoke 传对象)、调用私有静态方法(invoke 传 null)、以及用封装好的通用工具统一调用(自动判断实例/静态)。

// 来源:homework0813/MethodPrivateTest.java
public class MethodPrivateTest {
    public static void main(String[] args) throws Exception {
        // 1. 创建 Student 对象,反射调用 setName("张三")(复用练习六工具)
        Student student = new Student(101, "张三", 25, "男", "zhangsan@qq.com", "13636363366");
        // 2. 暴力反射调用私有实例方法 read()(invoke 传对象 s)
        // 3. 暴力反射调用私有静态方法 write()(invoke 传 null)
        // 4. 用封装好的 invokeAnyMethod 工具调用 read(obj 传 s)/ write(obj 传 null)
        invokeAnyMethod(student, "read");
        invokeAnyMethod(student.getClass(), "write");
    }
    // …… invokeAnyMethod 通用工具见第 6 节 ……
}

main 方法执行流程

new Student(101, "张三", 25, "男", "zhangsan@qq.com", "13636363366")
      ↓
invokeAnyMethod(student, "read")        ← obj 传 Student 对象 → 调用私有实例方法 read(),输出"张三读......."
      ↓
invokeAnyMethod(student.getClass(), "write")   ← obj 传 Class 对象 → 调用私有静态方法 write(),输出"静态方法...写..............."
代码 作用 说明
new Student(101, "张三", ...) 创建 Student 对象 六参构造(上一课已学),name 赋值为"张三"
invokeAnyMethod(student, "read") 调私有实例方法 read obj 传的是 Student 对象 → 通用工具判定为实例方法,invoke 第一参数传对象
invokeAnyMethod(student.getClass(), "write") 调私有静态方法 write obj 传的是 Class 对象 → 通用工具判定为静态方法,invoke 第一参数传 null

要点:

  • main 方法没有直接写 getDeclaredMethod 三件套——注释 2、3 是把"标准写法"留作对照,实际执行走第 4 步的封装工具:同样能调私有方法,但代码更简洁(一行调一个)
  • instanceof 判断的关键区别:同一个工具方法,传对象 → 实例方法,传 Class → 静态方法——用 student.getClass() 传 Class 对象正是"主动告知工具:我要调静态方法"
  • main throws Exception:throws Exception 一网打尽所有受检异常(NoSuchMethodException / IllegalAccessException / InvocationTargetException)——演示代码的偷懒写法,实际开发应精确处理

4. 暴力反射调用私有实例方法 read():invoke 传对象

概念 —— 标准写法:getDeclaredMethod + setAccessible(true) + invoke(对象)

注释 2 要求用标准三件套暴力反射调用私有实例方法 read()——read() 是 private 实例方法,必须 getDeclaredMethod("read") 拿到方法对象(getMethod 拿不到 private)→ setAccessible(true) 放开权限 → invoke(对象) 传调用该方法的对象:

// 来源:homework0813/MethodPrivateTest.java(注释 2 的标准写法,main 实际用封装工具等价实现)
// 2. 暴力反射调用私有实例方法 read()(invoke 传对象 s)
Class<Student> clazz = Student.class;
Method readMethod = clazz.getDeclaredMethod("read");   // ① getDeclaredMethod 拿到 private 方法
readMethod.setAccessible(true);                        // ② 暴力反射:临时取消访问权限检查
Object result = readMethod.invoke(student);            // ③ invoke 第一参数传"调用该方法的对象" student

三件套拆解

步骤 代码 作用 说明
① clazz.getDeclaredMethod("read") 获取私有方法对象 read() 无参,getDeclaredMethod("read") 即可;getMethod 拿不到 private,必须用 Declared 系列
② readMethod.setAccessible(true) 暴力反射:临时取消访问权限检查 不调用则 invoke 私有方法抛 IllegalAccessException
③ readMethod.invoke(student) 调用私有实例方法 第一参数传对象 student——实例方法属于对象,必须指明"调用哪个对象的方法";read() 内部用 this.name,输出"张三读......."

要点:

  • 私有实例方法三件套:getDeclaredMethod(拿 private)+ setAccessible(true)(放开权限)+ invoke(对象)(传调用对象)——与 Field/Constructor 的暴力反射套路完全一致
  • invoke 第一参数 = 调用方法的对象:read() 是实例方法,内部用了 this.name,所以必须传 student 对象;传错对象或传 null 会抛 IllegalArgumentException 或 NullPointerException
  • 调用私有方法 = 破坏封装:read() 本来没有任何公开入口,反射硬生生调用成功了——这正是"反射破坏封装性"的体现,也是测试框架能测私有方法的原理

5. 暴力反射调用私有静态方法 write():invoke 传 null

概念 —— 静态方法不属于对象,invoke 第一参数传 null

注释 3 要求暴力反射调用私有静态方法 write()——静态方法属于整个类、不属于任何对象,所以 invoke 第一参数不传对象、传 null(与 Field 操作静态属性传 null 规律统一):

// 来源:homework0813/MethodPrivateTest.java(注释 3 的标准写法)
// 3. 暴力反射调用私有静态方法 write()(invoke 传 null)
Method writeMethod = clazz.getDeclaredMethod("write");    // ① getDeclaredMethod 拿到 private 静态方法
writeMethod.setAccessible(true);                          // ② 暴力反射:临时取消访问权限检查
Object result = writeMethod.invoke(null);                 // ③ 静态方法 invoke 第一参数传 null

私有实例方法 vs 私有静态方法对比

对比 私有实例方法 read() 私有静态方法 write()
权限 private private
是否 static 否(实例方法) 是(静态方法)
获取方式 getDeclaredMethod("read") getDeclaredMethod("write")
是否要 setAccessible(true) 要(private 必须暴力反射) 要(private 必须暴力反射)
invoke 第一参数 传对象 invoke(student) 传 null invoke(null)
输出 张三读....... 静态方法...写...............

要点:

  • invoke 第一参数规律(再次强调):实例方法传对象、静态方法传 null——与 Field 操作静态属性传 null 是同一规律:静态成员属于整个类,不属于任何对象
  • private 和 static 是两个独立维度:write() 既 private 又 static——private 决定了必须 getDeclaredMethod + setAccessible(true),static 决定了 invoke 传 null,两件事都要做
  • write() 内部不依赖任何对象状态:它只打印固定字符串,所以 invoke(null) 也能正常执行——如果静态方法内部访问了实例字段反而会出错(静态上下文不能直接访问实例成员)

6. 通用工具 invokeAnyMethod:instanceof 判断静态/实例 + 动态计算参数类型

概念 —— 把暴力反射封装成"一行调用任意方法"的通用工具

第 4、5 节的标准写法每次都要写三件套(getDeclaredMethod + setAccessible + invoke),非常啰嗦。invokeAnyMethod 把整个过程封装成一个通用工具:只要传入"目标对象(或 Class)+ 方法名 + 参数",它自动完成"计算参数类型 → 获取方法(含 private)→ 放开权限 → 调用"四步。核心技巧有两个:instanceof Class<?> 判断是静态还是实例方法、args[i].getClass() 动态计算参数类型数组。

// 来源:homework0813/MethodPrivateTest.java
// 通用工具:反射调用任意权限方法(getDeclaredMethod + setAccessible + invoke)
public static Object invokeAnyMethod(Object obj, String methodName, Object... args) throws Exception {
    // 1. 计算参数类型数组
    Class<?>[] argsClasses = new Class[args.length];
    for (int i = 0; i < args.length; i++) {
        argsClasses[i] = args[i].getClass();
    }
    // 判断,obj 到底是什么类型,如果 obj 属于 Class 类型,无论是哪个类的 Class 对象,都会进入到 if 语句中
    if (obj instanceof Class<?>) {
        // 强转,把 obj 转换成 Class 类型
        Class<?> clazz = (Class<?>) obj;
        // 2. getDeclaredMethod(methodName, 参数类型...) 获取方法(含 private)
        Method method = clazz.getDeclaredMethod(methodName, argsClasses);
        // 3. setAccessible(true) 暴力反射放开权限
        method.setAccessible(true);
        // 4. invoke(null, args) 调用并返回结果 —— 静态方法第一参数传 null
        return method.invoke(null, args);
    }
    // 如果 obj 不是 Class 类型,说明传入的是其他的对象,要调用的也是实例方法,而不是静态方法
    Class<?> clazz = obj.getClass();
    // 2. getDeclaredMethod(methodName, 参数类型...) 获取方法(含 private)
    Method method = clazz.getDeclaredMethod(methodName, argsClasses);
    // 3. setAccessible(true) 暴力反射放开权限
    method.setAccessible(true);
    // 4. invoke(obj, args) 调用并返回结果 —— 实例方法第一参数传对象
    return method.invoke(obj, args);
}

工具执行流程

invokeAnyMethod(student, "read")               invokeAnyMethod(student.getClass(), "write")
      ↓                                                 ↓
args 为空 → argsClasses 为空数组                  args 为空 → argsClasses 为空数组
      ↓                                                 ↓
obj instanceof Class<?> ? 否                    obj instanceof Class<?> ? 是(student.getClass() 是 Class 对象)
      ↓                                                 ↓
clazz = obj.getClass() = Student.class          clazz = (Class<?>) obj = Student.class
      ↓                                                 ↓
getDeclaredMethod("read", 无参)                  getDeclaredMethod("write", 无参)
      ↓                                                 ↓
setAccessible(true)                              setAccessible(true)
      ↓                                                 ↓
invoke(student, )   ← 实例方法传对象              invoke(null, )      ← 静态方法传 null

两个核心技巧拆解

技巧 代码 作用 说明
① 动态计算参数类型 argsClasses[i] = args[i].getClass() 根据实际传入的参数动态推导参数类型数组 不写死 String.class 等——传什么参数就自动得到什么类型,方法有重载也能精确定位
② instanceof 判断实例/静态 obj instanceof Class<?> 判断 obj 是 Class 对象还是普通对象 传 student.getClass() 说明要调静态方法;传普通对象说明要调实例方法
静态方法分支 invoke(null, args) 静态方法调用 obj 是 Class → 方法属于整个类 → invoke 传 null
实例方法分支 invoke(obj, args) 实例方法调用 obj 是普通对象 → 方法属于该对象 → invoke 传 obj

为什么用 instanceof Class<?> 判断?

student.getClass() 返回的是 Class<Student> 类型对象——它既是"用来拿方法元数据的类描述",又被当作"要调静态方法的信号"。工具里用 obj instanceof Class<?> 判断:obj 是 Class 类型 → 传进来的是"类",调静态方法;obj 不是 Class 类型 → 传进来的是"对象",调实例方法。注释里专门解释:"如果 obj 属于 Class 类型,无论是哪个类的 Class 对象,都会进入到 if 语句中"——所以传 student.getClass() 和传 Student.class 效果一样。

要点:

  • args[i].getClass() 动态计算参数类型:上一课练习六的经典技巧——通用工具不能写死参数类型,先根据实际参数用 getClass() 算出参数类型数组,再传给 getDeclaredMethod(methodName, argsClasses);args 是可变参数(Object...),长度为 0 时得到空数组
  • instanceof Class<?> 是"静态方法开关":Class 类型的 obj 走静态分支(invoke(null)),普通对象走实例分支(invoke(obj))——同一个工具两种用法,判断依据就是 obj 是不是 Class
  • 两个分支代码几乎一样:都是"getDeclaredMethod + setAccessible + invoke",只有 invoke 第一参数不同(null vs obj)——这也再次说明实例/静态方法的唯一差别就在"要不要传对象"
  • 返回值 Object:invoke 返回方法执行结果(Object 类型),工具把结果 return 给调用者——本课两个方法都是 void,所以返回 null,但工具设计上支持有返回值的方法

7. MiniJUnitTest 演示:模拟 JUnit 单元测试框架(反射终极应用)

概念 —— 测试框架的底层原理:遍历方法 + 前缀识别 + 反射执行

JUnit 等测试框架的核心原理就是反射:拿到类 → 遍历全部方法 → 找出测试方法 → 反射执行并统计结果。真实 JUnit 用 @Test 注解标记测试方法,MiniJUnit 用方法名前缀 "test" 识别——原理完全一致。整个框架只有 6 步:

// 来源:homework0813/MiniJUnitTest.java
public class MiniJUnitTest {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        // 1. 反射创建被测 Student 对象(getConstructor().newInstance())
        Class<Student> studentClass = Student.class;
        Student student = studentClass.getConstructor().newInstance();
        // 2. getDeclaredMethods() 遍历 Student 全部方法
        Method[] declaredMethods = studentClass.getDeclaredMethods();
        // 3. 方法名以 "test" 开头 → 认定为测试方法
        int count = 0, a = 0;
        for (Method m : declaredMethods) {
            String methodName = m.getName();
            if (methodName.startsWith("test")) {
                // 4. 对每个测试方法:setAccessible(true) + invoke(obj) 执行
                m.setAccessible(true);
                try {
                    m.invoke(student);
                    // 5. 捕获 InvocationTargetException → getCause() 打印真实异常
                    a++;                       // 执行成功 → 通过数 +1
                } catch (IllegalAccessException e) {
                    count++;                   // 权限异常 → 失败数 +1
                } catch (InvocationTargetException e) {
                    count++;                   // 被调方法内部抛异常(包装) → 失败数 +1
                }
            }
        }
        // 6. 统计通过/失败个数
        System.out.println("失败个数:" + count);
        System.out.println("成功个数:" + a);
    }
}

预期输出

测试:加法计算
测试:打招呼 null
失败个数:1
成功个数:2

说明:testAdd 通过(输出"测试:加法计算")、testHello 通过(输出"测试:打招呼 null"——用无参构造创建对象,name 为 null)、testFail 故意抛异常判失败——所以统计"失败 1 个、成功 2 个"。

MiniJUnit 框架六步流程

Student.class                              ← ① 拿 Class 对象(类名.class)
      ↓
getConstructor().newInstance()             ← ② 反射创建被测对象(无参构造)
      ↓
getDeclaredMethods()                       ← ③ 遍历本类全部方法(含 private)
      ↓
methodName.startsWith("test")              ← ④ 按前缀识别测试方法(真实 JUnit 用 @Test 注解)
      ↓
setAccessible(true) + invoke(student)      ← ⑤ 暴力反射执行测试方法
      ↓
成功 a++ / 失败 count++                     ← ⑥ 统计通过/失败个数

要点:

  • MiniJUnit 就是简化版 JUnit:真实 JUnit 的 @Test 注解识别 → 底层就是"遍历方法 + 判断是否测试方法";MiniJUnit 用 startsWith("test") 替代注解,原理一模一样
  • 测试方法都是 private:MiniJUnit 能执行 private 的 testXxx 方法,靠的就是 setAccessible(true) 暴力反射——测试框架破坏封装访问私有方法是反射破坏封装性的典型正面应用
  • main 抛出 4 个受检异常:NoSuchMethodException(getConstructor 找不到无参构造)、InvocationTargetException(被调方法抛异常)、InstantiationException(实例化失败)、IllegalAccessException(权限问题)——反射常用受检异常的"全家福"

8. 反射创建被测对象:getConstructor().newInstance()

概念 —— 不知道类也能创建对象,靠 Class + Constructor

框架要测试一个类,编译期并不知道被测类是谁(可能是用户后来写的)——所以不能 new,必须反射创建对象:getConstructor() 拿到无参构造方法,再 .newInstance() 创建实例:

// 来源:homework0813/MiniJUnitTest.java(第 1 步)
Class<Student> studentClass = Student.class;              // ① 拿 Class 对象
Student student = studentClass.getConstructor().newInstance();   // ② getConstructor() 拿无参构造 → newInstance() 创建对象

代码拆解

代码 作用 说明
Student.class 获取 Student 的 Class 对象 方式一:类名.class(上一课:三种获取 Class 对象的方式)
studentClass.getConstructor() 获取public 无参构造方法对象 不传参数 → 匹配无参构造;Student 有无参构造 public Student() {}
.newInstance() 反射创建对象 等价于 new Student()——但运行时才知道创建的是哪个类

要点:

  • getConstructor() 拿的是 public 无参构造:如果不传参数类型,默认找无参构造;Student 定义了无参构造,所以能成功——如果类没有无参构造,这里会抛 NoSuchMethodException
  • 为什么用无参构造:测试框架不知道被测类有哪些字段、怎么传参,所以统一用无参构造创建对象(对象字段都是默认值 null/0),再通过 setter 或反射赋测试数据
  • 上一条的连锁反应:无参构造创建的对象 name 是 null → 执行 testHello() 输出"测试:打招呼 null"——这正是"字段默认值"的体现
  • Constructor.newInstance() vs Class.newInstance():JDK9+ 推荐用 Constructor.newInstance()(可访问 private 构造、可传参),本课用 getConstructor() 系列(上一课 08-12 已学)

9. getDeclaredMethods() 遍历 + test 前缀识别测试方法

概念 —— 遍历全部方法,用命名约定找出"测试方法"

测试框架的核心问题:怎么知道哪些方法是测试方法? 真实 JUnit 用 @Test 注解,MiniJUnit 用命名约定——方法名以 test 开头的就当作测试方法。遍历用 getDeclaredMethods()(拿本类全部方法,含 private):

// 来源:homework0813/MiniJUnitTest.java(第 2~3 步)
Method[] declaredMethods = studentClass.getDeclaredMethods();   // ① 遍历本类全部方法(含 private)
int count = 0, a = 0;                                           // ② 失败计数器 count、成功计数器 a
for (Method m : declaredMethods) {
    String methodName = m.getName();
    if (methodName.startsWith("test")) {                        // ③ 方法名以 "test" 开头 → 认定为测试方法
        // …… 执行测试(见第 10 节)……
    }
}

代码拆解

代码 作用 说明
studentClass.getDeclaredMethods() 获取本类声明的全部方法数组 含 private(testXxx 都是 private),不含父类 Object 的方法
m.getName() 获取方法名 如 testAdd / testHello / testFail / getName / read 等
methodName.startsWith("test") 判断方法名是否以 test 开头 命名约定识别测试方法——true 才进入执行逻辑
int count = 0, a = 0 失败/成功计数器 count 记失败、a 记成功(a 是 success 简写)

为什么能遍历到 private 的 testXxx?

testAdd/testHello/testFail 都是 private 方法——普通代码在类外部根本看不到它们。但 getDeclaredMethods() 包含本类声明的全部方法(含 private),所以反射能拿到这 3 个测试方法。这正是测试框架能测试"类内部私有逻辑"的基础:私有方法无法从外部调用,但反射遍历 + 暴力反射可以。

要点:

  • 命名约定 vs 注解:JUnit 早期版本(JUnit 3)就是用"方法名以 test 开头"识别测试方法,后来 JUnit 4 才改用 @Test 注解——MiniJUnit 模拟的是最古老的 JUnit 3 设计,更直观地展示"框架如何识别测试方法"
  • getDeclaredMethods 不含父类方法:只遍历 Student 本类声明的方法(含 3 个 testXxx),不会把 Object 的 toString/hashCode 等也遍历出来
  • if 判断是过滤条件:多个方法里只有 3 个 test 开头的方法进入执行分支——这就是"扫描 + 筛选"的框架思路

10. setAccessible(true) + invoke 执行测试方法

概念 —— 暴力反射执行 private 测试方法,异常决定成败

识别出测试方法后,执行也是暴力反射三件套:setAccessible(true) 放开权限 → invoke(student) 执行。成败判定靠异常:invoke 不抛异常 = 测试通过(a++);抛异常 = 测试失败(count++):

// 来源:homework0813/MiniJUnitTest.java(第 4~5 步)
for (Method m : declaredMethods) {
    String methodName = m.getName();
    if (methodName.startsWith("test")) {
        m.setAccessible(true);              // ① 暴力反射:放开 private 测试方法的权限
        try {
            m.invoke(student);              // ② 执行测试方法(invoke 传被测对象)
            a++;                            // ③ 不抛异常 → 成功 +1
        } catch (IllegalAccessException e) {
            count++;                        // ④ 权限异常 → 失败 +1
        } catch (InvocationTargetException e) {
            count++;                        // ⑤ 被调方法内部抛异常(包装) → 失败 +1
        }
    }
}

三种情况对照

测试方法 invoke 结果 进入哪个分支 计数器
testAdd() 正常执行(1+1==2 不抛异常) try 成功 成功 a++
testHello() 正常执行(只是打印) try 成功 成功 a++
testFail() 内部 throw new RuntimeException → invoke 抛 InvocationTargetException catch InvocationTargetException 失败 count++

要点:

  • 测试框架的"断言"哲学:测试方法内部写断言(if 条件不满足就 throw),invoke 执行时一旦抛异常就被捕获并判为失败——"不抛异常 = 通过"是测试框架最基本的判定规则
  • 为什么要捕获异常而不是让程序崩溃:任何一个测试失败都不应该中断整个测试套件——所以 try-catch 包住 invoke,单个测试失败只计一次失败,继续执行下一个测试
  • IllegalAccessException 也被计入失败:理论上 setAccessible(true) 后不会抛权限异常,但防御性编程把两种异常都捕获计入失败——保证任何异常都影响统计而不影响流程
  • invoke 第一参数传被测对象:m.invoke(student) 的 student 是第 8 节反射创建的对象——测试方法都是实例方法,必须传对象

11. InvocationTargetException 包装异常:getCause() 获取真实异常

概念 —— 被调方法内部抛异常时,invoke 会"包装"而不是"直接抛"

这是反射调用与普通调用最隐蔽的差异:普通代码 s.testFail() 会直接抛 RuntimeException;但反射 invoke 调用时,被调方法内部抛的异常会被包装成 InvocationTargetException 抛出。因为 invoke 是"反射机制"调用的方法,它在被调方法外面包了一层——调用者必须通过 e.getCause() 才能拿到真正的异常。

// 来源:homework0813/MiniJUnitTest.java(第 5 步:InvocationTargetException 捕获)
try {
    m.invoke(student);
    a++;                       // 不抛异常 → 成功
} catch (IllegalAccessException e) {
    count++;                   // 权限异常 → 失败
} catch (InvocationTargetException e) {
    count++;                   // 被调方法内部抛异常(InvocationTargetException 包装) → 失败
    // 如果要看真实异常:e.getCause() 返回真正的 RuntimeException("故意失败的方法")
}

异常包装流程

testFail() 内部 throw new RuntimeException("故意失败的方法")
      ↓
m.invoke(student) 执行 testFail()
      ↓
被调方法抛异常 → 反射机制把异常"包了一层"
      ↓
invoke 抛出 InvocationTargetException(不是直接抛 RuntimeException)
      ↓
catch (InvocationTargetException e) 捕获 → count++ 记为失败
      ↓
真实异常:e.getCause()  ← 拿到真正的 RuntimeException("故意失败的方法")
对比 普通调用 反射调用(invoke)
调用方式 student.testFail() m.invoke(student)
被调方法抛异常时 直接抛 RuntimeException 抛 InvocationTargetException(包装)
如何拿到真实异常 直接 catch RuntimeException catch (InvocationTargetException e) 后 e.getCause()
为什么不直接抛 — invoke 是反射机制,被调方法外有"包装层"

要点:

  • InvocationTargetException 是"包装异常":它的名字直译是"调用目标异常"——被调目标(testFail)抛了异常,被 invoke 包了一层再抛出来
  • getCause() 获取真实异常:需要诊断失败原因时,用 e.getCause() 拿到真正的 RuntimeException(及异常消息)——注释 5 特意提醒"getCause() 打印真实异常"
  • MiniJUnit 只统计不计原因:本课框架只 count++ 计失败数,没有打印 getCause() 详情——实际 JUnit 会把 getCause() 的异常栈打印出来,这就是测试报告里"失败原因"的来源
  • 与 08-13 练习的呼应:上一课练习里就有"把 read() 改成会抛异常的语句,观察 invoke 抛 InvocationTargetException"的实验——本课把这一现象用在了真实框架逻辑里

12. 通过/失败统计:模拟测试报告输出

概念 —— 框架的最后一步:汇总成功/失败个数

所有测试方法执行完后,输出统计结果——这就是简化版"测试报告":

// 来源:homework0813/MiniJUnitTest.java(第 6 步)
System.out.println("失败个数:" + count);   // 输出 失败个数:1
System.out.println("成功个数:" + a);       // 输出 成功个数:2

预期输出

测试:加法计算
测试:打招呼 null
失败个数:1
成功个数:2

运行流程全景(MiniJUnit 完整执行)

第 1 步:反射创建被测对象  → Student student(name=null)
第 2 步:遍历全部方法       → getDeclaredMethods()
第 3 步:筛选 test 前缀      → 命中 testAdd / testHello / testFail
第 4 步:逐个暴力反射执行:
    testAdd   →  输出"测试:加法计算",1+1==2 不抛异常  → 成功 a=1
    testHello → 输出"测试:打招呼 null"(name 为 null)→ 成功 a=2
    testFail  →  输出后直接 throw → invoke 抛 InvocationTargetException → 失败 count=1
第 5 步:统计输出 → 失败个数:1,成功个数:2

计数器拆解

变量 含义 最终值 对应测试方法
a 成功个数 2 testAdd(通过)、testHello(通过)
count 失败个数 1 testFail(故意抛异常)

要点:

  • 两个计数器语义:a 记成功(invoke 不抛异常)、count 记失败(IllegalAccessException 或 InvocationTargetException)——一正一负互补,相加等于测试方法总数 3
  • "测试:打招呼 null"是合理的:testHello 用反射创建的无参构造对象执行,name 是 null——这提示"测试对象需要先初始化数据",真实测试框架会提供 @BeforeEach 之类的初始化钩子
  • 模拟测试报告的局限:真实 JUnit 还会输出每个测试的耗时、失败栈、@Test 注解解析等——MiniJUnit 只演示了最核心的"扫描 + 执行 + 统计"闭环

13. 知识点全景总结

知识点 核心内容 来源
项目结构 c260814 项目 = homework0813 包(0813 作业进阶)——三个类:Student(反射目标实体类)/ MethodPrivateTest(暴力反射调用私有方法 + 通用工具)/ MiniJUnitTest(模拟 JUnit 单元测试框架) homework0813/
Student 三种权限字段 private(id/name/age/gender)+ public(email/phone)+ public static(school)——上一课 Field 反射目标,本课复用 homework0813/Student.java
Student 4 个构造方法 public 无参 / private 二参 / public 四参 / public 六参——MiniJUnit 用 public 无参构造反射创建对象 homework0813/Student.java
Student 私有方法 read/write private void read()(实例)+ private static void write()(静态)——MethodPrivateTest 暴力反射调用目标 homework0813/Student.java
Student 测试方法 testAdd/testHello/testFail 3 个 test 前缀 private 方法——MiniJUnit 按命名约定识别的"模拟测试用例";testAdd 通过、testHello 通过、testFail 故意抛异常 homework0813/Student.java
私有方法无公开入口 read()/write()/testXxx 都是 private 且没有 getter/setter,普通代码无法调用——只有暴力反射能调用,这是本课"为什么需要反射"的场景 homework0813/Student.java
暴力反射调用私有实例方法 getDeclaredMethod("read")(拿 private)+ setAccessible(true)(放开权限)+ invoke(对象)(第一参数传调用该方法的对象)——三件套与 Field/Constructor 一致 homework0813/MethodPrivateTest
暴力反射调用私有静态方法 getDeclaredMethod("write") + setAccessible(true) + invoke(null)(静态方法第一参数传 null)——private 决定暴力反射、static 决定传 null,两个维度独立 homework0813/MethodPrivateTest
invoke 第一参数规律 实例方法传对象、静态方法传 null——与 Field 操作静态属性传 null 同一规律:静态成员属于整个类、不属于任何对象 homework0813/MethodPrivateTest
通用工具 invokeAnyMethod 封装"计算参数类型 → 获取方法 → 放开权限 → 调用"四步——invokeAnyMethod(student, "read") 调实例方法、invokeAnyMethod(student.getClass(), "write") 调静态方法 homework0813/MethodPrivateTest
instanceof Class<?> 判断 obj instanceof Class<?> 判断 obj 是 Class 对象还是普通对象——Class → 静态方法分支 invoke(null);普通对象 → 实例方法分支 invoke(obj) homework0813/MethodPrivateTest
args[i].getClass() 动态计算参数类型 通用工具不能写死参数类型,用 args[i].getClass() 根据实际参数动态算出 Class<?>[] 再传给 getDeclaredMethod——可变参数 Object... 长度为 0 时得到空数组 homework0813/MethodPrivateTest
反射创建被测对象 studentClass.getConstructor().newInstance()——getConstructor() 拿 public 无参构造(不传参数类型默认找无参构造),newInstance() 创建对象;类没有无参构造会抛 NoSuchMethodException homework0813/MiniJUnitTest
getDeclaredMethods() 遍历 获取本类声明的全部方法数组(含 private,不含父类 Object 的方法)——测试框架扫描测试方法的入口 homework0813/MiniJUnitTest
test 前缀识别测试方法 methodName.startsWith("test") 按命名约定识别测试方法——模拟 JUnit 3 的设计(JUnit 4+ 用 @Test 注解,原理相同) homework0813/MiniJUnitTest
暴力反射执行测试方法 m.setAccessible(true) + m.invoke(student)——private 测试方法靠暴力反射执行;invoke 不抛异常 = 通过(a++),抛异常 = 失败(count++) homework0813/MiniJUnitTest
"不抛异常 = 通过"的判定哲学 测试方法内部写断言(if 条件不满足就 throw),invoke 执行时抛异常即判失败——测试框架最基本的判定规则 homework0813/MiniJUnitTest
InvocationTargetException 包装异常 被调方法内部抛异常时,反射 invoke 不会直接抛原异常,而是包装成 InvocationTargetException 抛出;调用者需 e.getCause() 获取真实异常 homework0813/MiniJUnitTest
InvocationTargetException 与普通调用对比 普通 student.testFail() 直接抛 RuntimeException;反射 m.invoke(student) 抛 InvocationTargetException(被调方法外有包装层) homework0813/MiniJUnitTest
通过/失败统计 成功计数器 a、失败计数器 count——testAdd/testHello 通过(a=2),testFail 失败(count=1),输出"失败个数:1,成功个数:2" homework0813/MiniJUnitTest
测试报告输出 System.out.println("失败个数:" + count) / System.out.println("成功个数:" + a)——简化版测试报告 homework0813/MiniJUnitTest
受检异常全家福 MiniJUnit main throws NoSuchMethodException(无无参构造)+ InvocationTargetException(被调方法抛异常)+ InstantiationException(实例化失败)+ IllegalAccessException(权限) homework0813/MiniJUnitTest
防御性异常捕获 catch 里同时处理 IllegalAccessException 和 InvocationTargetException 都计入失败——保证单个测试失败不影响后续测试执行 homework0813/MiniJUnitTest
方法反射综合应用闭环 获取方法(getDeclaredMethod(s))→ 暴力反射(setAccessible)→ 调用(invoke)→ 异常处理(InvocationTargetException.getCause())→ 统计——完整走完 Method 反射的全部环节 homework0813/MethodPrivateTest + MiniJUnitTest

14. 随堂练习要点

  • 项目结构梳理(必做):说清 c260814 项目三个类各司其职——Student 提供被测对象与测试方法、MethodPrivateTest 演示暴力反射调用私有方法并封装通用工具、MiniJUnitTest 模拟 JUnit 测试框架;包名 homework0813 的含义(0813 作业进阶)
  • Student 私有方法识别(必做):从 Student 类中找出所有 private 方法(read/write/testAdd/testHello/testFail 共 5 个),区分实例方法(read/testXxx)与静态方法(write)——思考为什么普通代码调用不了它们,只有反射能调
  • 暴力反射三件套背诵(必做):getDeclaredMethod("read")(拿 private)+ setAccessible(true)(放开权限)+ invoke(对象)(实例方法传对象)——对比静态方法 invoke(null),总结"private 决定暴力反射、static 决定传 null"
  • invoke 第一参数规律实验:把 invokeAnyMethod(student.getClass(), "write") 改成 invokeAnyMethod(student, "write")(传对象不传 Class)——观察工具走到实例方法分支、invoke(student) 调静态方法,思考结果是否仍能执行(静态方法不依赖对象,通常也能跑)——体会"传 Class 才是调用静态方法的正确信号"
  • instanceof Class<?> 理解(必做):思考通用工具为什么用 obj instanceof Class<?> 判断——student.getClass() 和 Student.class 都是 Class 对象;传 Class → 静态方法分支,传普通对象 → 实例方法分支;画一张"调用流程图"说明两个分支的执行路径
  • args[i].getClass() 动态计算参数类型:给 Student 新增一个有参方法(如 testAdd(int a, int b)),用 invokeAnyMethod(student, "testAdd", 1, 2) 调用——体会工具自动用 args[i].getClass() 算出参数类型数组(自动装箱后实际是 Integer.class)
  • setAccessible(true) 注释掉实验(必做):把 MiniJUnit 中 m.setAccessible(true) 注释掉再运行——观察 3 个 private 测试方法全部抛 IllegalAccessException,统计"失败 3 个、成功 0 个"——直观体会暴力反射对私有方法的作用
  • getDeclaredMethods vs getMethods 对比:把 MiniJUnit 中 getDeclaredMethods() 改成 getMethods() 再运行——观察 testXxx(private)遍历不到,成功/失败都是 0——验证"getMethods 只拿 public、拿不到 private 测试方法"
  • InvocationTargetException 实验(必做):在 testFail 的 catch 分支里加 e.getCause().printStackTrace() 再运行——观察控制台打印出真正的 RuntimeException("故意失败的方法")——体会 getCause() 获取真实异常、测试报告"失败原因"的来源
  • MiniJUnit 扩展练习①:新增测试方法:给 Student 新增 testSub()(减法测试,2-1==1 通过)和 testDiv()(故意 1/0 抛异常失败)——重新运行观察统计变为"成功 3、失败 2"
  • MiniJUnit 扩展练习②:打印测试结果明细:在 invoke 成功后加 System.out.println(methodName + " 通过")、失败后加 System.out.println(methodName + " 失败:" + e.getCause())——让模拟测试报告显示每个用例的名字和失败原因(贴近真实 JUnit)
  • 无参构造的重要性(必做):把 Student 的 public Student() {} 注释掉再运行 MiniJUnit——观察 getConstructor().newInstance() 抛 NoSuchMethodException——理解"反射创建对象为什么要求类有无参构造"(框架不知道传什么参数)
  • 测试对象初始化问题:观察"测试:打招呼 null"的输出——反思:无参构造创建的对象字段为 null,真实测试框架如何解决初始化?(提示:@BeforeEach / @BeforeAll 钩子方法,或反射 setter 注入测试数据)
  • 防御性异常处理思考:MiniJUnit 把 IllegalAccessException 和 InvocationTargetException 都计入失败——思考还有什么异常可能抛出(如 InvocationTargetException 的 getCause 是 Error)、框架如何处理才算健壮
  • 与真实 JUnit 对比(必做):说出 MiniJUnit 与真实 JUnit 的 3 个差异——①测试方法识别:test 前缀 vs @Test 注解 ②没有 @BeforeEach/@AfterEach 初始化/清理钩子 ③失败时只统计个数、不打印失败原因和堆栈
  • 通用工具复用(必做):把 invokeAnyMethod 抄下来作为自己的反射工具方法——思考它还能调用哪些方法(任意对象的任意方法:getter/setter/私有方法/静态方法),回顾上一课练习六的 invokeMethod 工具与本课工具的异同(本课多了 instanceof 判断静态/实例分支)
  • Method 反射知识串联(本课完结):从 08-12 Constructor(创建对象)→ 08-13 Field(读写字段)+ Method(调用方法)→ 本课 Method 进阶应用(暴力反射私有方法 + 模拟测试框架)——完整回顾反射三大成员从"学 API"到"做框架"的进阶路径

15. 拓展阅读

  • JUnit 的底层原理:JUnit 5(Jupiter)通过 @Test 注解 + 反射实现测试执行——测试类被 Class.forName 加载 → 遍历方法找 @Test 注解 → setAccessible(true) + invoke 执行 → 用 InvocationTargetException.getCause() 捕获断言异常并生成测试报告——本课 MiniJUnit 正是这个链路的简化版
  • JUnit 3 与 JUnit 4/5 的演进:JUnit 3 用"方法名以 test 开头"识别测试(本课模拟的就是 JUnit 3 设计);JUnit 4 引入 @Test 注解(注解驱动);JUnit 5 引入扩展模型(Extension)——理解"命名约定 → 注解 → 扩展模型"的演进,是理解测试框架历史的钥匙
  • 断言(Assertion)机制:真实 JUnit 用 Assert.assertEquals(expected, actual) 等断言方法——断言内部就是"判断条件,不满足就 throw AssertionError";反射 invoke 执行测试方法时,断言异常被包装成 InvocationTargetException,框架用 getCause() 取出断言信息作为"测试失败原因"
  • 生命周期钩子方法:真实 JUnit 的 @BeforeEach/@AfterEach/@BeforeAll/@AfterAll 注解方法——框架在"每次测试前/后、全部测试前/后"反射调用这些钩子方法——本课"测试:打招呼 null"暴露的对象初始化问题,正是 @BeforeEach 要解决的
  • 反射在框架中的"扫描 + 执行"模式:本课 MiniJUnit 的"getDeclaredMethods 遍历 → 条件筛选 → invoke 执行"是反射框架的通用范式——Spring 的组件扫描(找 @Component 注解的类)、MyBatis 的 Mapper 解析(找接口方法)、参数解析器(找匹配参数)都是这个套路
  • InvocationTargetException 在框架中的应用:任何"反射调用业务方法"的框架(Spring AOP 代理方法、RPC 框架调用远程服务方法、定时任务框架)都会遇到 InvocationTargetException——框架统一用 getCause() 拿到业务异常再处理,避免被"包装层"迷惑
  • getCause() 的链式异常:getCause() 返回的异常可能还有自己的 cause(异常链)——框架(如 JUnit)打印失败栈时会遍历整条异常链 while ((e = e.getCause()) != null),把每一层原因都展示出来
  • 反射调用 vs 方法句柄(MethodHandle):JDK7+ 引入 java.lang.invoke.MethodHandle(方法句柄),性能优于反射 invoke,是"更高性能的反射"——Spring、Netty 等框架在热点路径上会用 MethodHandle 替代 Method.invoke
  • 反射调用 setter/getter 的框架应用:Spring BeanUtils.copyProperties、MyBatis 结果映射、Jackson 反序列化都通过反射调用实体类的 setter 赋值——这就是实体类必须写标准 getter/setter 的原因;本课 Student 的 7 组 getter/setter 正是为此准备
  • Class.getConstructor vs getDeclaredConstructor:MiniJUnit 用 getConstructor()(public 无参构造)——如果被测类的构造是 private,需要用 getDeclaredConstructor() + setAccessible(true)(上一课 08-12 已学);框架统一要求无参构造,就是为了避免处理各种构造器参数
  • "无参构造"约定在框架中的地位:Spring Bean、MyBatis 实体、JUnit 测试类都要求无参构造——因为框架无法预知构造参数,统一用无参构造 + setter 注入是最通用的约定;这也是 JavaBean 规范(无参构造 + getter/setter)的由来
  • 测试框架中的"隔离"思想:为什么每个测试方法失败不中断整个套件?——因为测试之间应相互独立(一个测试失败不代表其他测试失效);try-catch 包住每个 invoke 正是"测试隔离"的实现
  • 反射破坏封装性的辩证关系(最终版):从 08-12 的 Constructor(newInstance 创建对象)到 08-13 的 Field(get/set 读写字段)+ Method(invoke 调用方法)再到本课的"暴力反射私有方法 + 测试框架"——反射破坏封装的"负面"(能访问 private)恰恰是框架"正面"能力(测试私有逻辑)的来源:普通业务代码遵守封装,框架用反射突破封装
  • 反射 Method 知识全景(本课收官):Method 反射的全部技能点——获取方法(getMethod(s)/getDeclaredMethod(s))、方法信息(getName/getReturnType/getParameterTypes/getModifiers)、调用方法(invoke)、暴力反射(setAccessible)、异常处理(InvocationTargetException.getCause())、组合应用(通用调用工具 + 测试框架)——至此"反射三件套"(Constructor/Field/Method)全部学完并可组合成框架级工具
  • JDK 文档:java.lang.Class(getConstructor/getDeclaredMethods 等反射入口)、java.lang.reflect.Method(invoke/setAccessible/getName/getParameterTypes)、java.lang.reflect.InvocationTargetException(包装被调方法异常的反射专用异常,getCause 获取真实异常)、JUnit 5 User Guide(@Test 注解与扩展模型、断言机制)