c260814homework0813(默认包根目录下的具名包)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 统计通过/失败个数,底层原理:注解识别 + 反射调用)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 组合成"能干活"的工具和框架——反射的价值在于组合应用
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 节)
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 节 ……
}
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)——演示代码的偷懒写法,实际开发应精确处理
注释 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() 本来没有任何公开入口,反射硬生生调用成功了——这正是"反射破坏封装性"的体现,也是测试框架能测私有方法的原理
注释 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
| 对比 | 私有实例方法 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) 也能正常执行——如果静态方法内部访问了实例字段反而会出错(静态上下文不能直接访问实例成员)
第 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,但工具设计上支持有返回值的方法
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 个"。
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(权限问题)——反射常用受检异常的"全家福"
框架要测试一个类,编译期并不知道被测类是谁(可能是用户后来写的)——所以不能 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 已学)
测试框架的核心问题:怎么知道哪些方法是测试方法? 真实 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 简写) |
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 开头的方法进入执行分支——这就是"扫描 + 筛选"的框架思路
识别出测试方法后,执行也是暴力反射三件套: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 节反射创建的对象——测试方法都是实例方法,必须传对象
这是反射调用与普通调用最隐蔽的差异:普通代码 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"的实验——本课把这一现象用在了真实框架逻辑里
所有测试方法执行完后,输出统计结果——这就是简化版"测试报告":
// 来源:homework0813/MiniJUnitTest.java(第 6 步)
System.out.println("失败个数:" + count); // 输出 失败个数:1
System.out.println("成功个数:" + a); // 输出 成功个数:2
测试:加法计算
测试:打招呼 null
失败个数:1
成功个数:2
第 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 只演示了最核心的"扫描 + 执行 + 统计"闭环
| 知识点 | 核心内容 | 来源 |
|---|---|---|
| 项目结构 | 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 |
c260814 项目三个类各司其职——Student 提供被测对象与测试方法、MethodPrivateTest 演示暴力反射调用私有方法并封装通用工具、MiniJUnitTest 模拟 JUnit 测试框架;包名 homework0813 的含义(0813 作业进阶)getDeclaredMethod("read")(拿 private)+ setAccessible(true)(放开权限)+ invoke(对象)(实例方法传对象)——对比静态方法 invoke(null),总结"private 决定暴力反射、static 决定传 null"invokeAnyMethod(student.getClass(), "write") 改成 invokeAnyMethod(student, "write")(传对象不传 Class)——观察工具走到实例方法分支、invoke(student) 调静态方法,思考结果是否仍能执行(静态方法不依赖对象,通常也能跑)——体会"传 Class 才是调用静态方法的正确信号"obj instanceof Class<?> 判断——student.getClass() 和 Student.class 都是 Class 对象;传 Class → 静态方法分支,传普通对象 → 实例方法分支;画一张"调用流程图"说明两个分支的执行路径testAdd(int a, int b)),用 invokeAnyMethod(student, "testAdd", 1, 2) 调用——体会工具自动用 args[i].getClass() 算出参数类型数组(自动装箱后实际是 Integer.class)m.setAccessible(true) 注释掉再运行——观察 3 个 private 测试方法全部抛 IllegalAccessException,统计"失败 3 个、成功 0 个"——直观体会暴力反射对私有方法的作用getDeclaredMethods() 改成 getMethods() 再运行——观察 testXxx(private)遍历不到,成功/失败都是 0——验证"getMethods 只拿 public、拿不到 private 测试方法"e.getCause().printStackTrace() 再运行——观察控制台打印出真正的 RuntimeException("故意失败的方法")——体会 getCause() 获取真实异常、测试报告"失败原因"的来源testSub()(减法测试,2-1==1 通过)和 testDiv()(故意 1/0 抛异常失败)——重新运行观察统计变为"成功 3、失败 2"System.out.println(methodName + " 通过")、失败后加 System.out.println(methodName + " 失败:" + e.getCause())——让模拟测试报告显示每个用例的名字和失败原因(贴近真实 JUnit)public Student() {} 注释掉再运行 MiniJUnit——观察 getConstructor().newInstance() 抛 NoSuchMethodException——理解"反射创建对象为什么要求类有无参构造"(框架不知道传什么参数)@Test 注解 + 反射实现测试执行——测试类被 Class.forName 加载 → 遍历方法找 @Test 注解 → setAccessible(true) + invoke 执行 → 用 InvocationTargetException.getCause() 捕获断言异常并生成测试报告——本课 MiniJUnit 正是这个链路的简化版@Test 注解(注解驱动);JUnit 5 引入扩展模型(Extension)——理解"命名约定 → 注解 → 扩展模型"的演进,是理解测试框架历史的钥匙Assert.assertEquals(expected, actual) 等断言方法——断言内部就是"判断条件,不满足就 throw AssertionError";反射 invoke 执行测试方法时,断言异常被包装成 InvocationTargetException,框架用 getCause() 取出断言信息作为"测试失败原因"@BeforeEach/@AfterEach/@BeforeAll/@AfterAll 注解方法——框架在"每次测试前/后、全部测试前/后"反射调用这些钩子方法——本课"测试:打招呼 null"暴露的对象初始化问题,正是 @BeforeEach 要解决的getCause() 拿到业务异常再处理,避免被"包装层"迷惑getCause() 返回的异常可能还有自己的 cause(异常链)——框架(如 JUnit)打印失败栈时会遍历整条异常链 while ((e = e.getCause()) != null),把每一层原因都展示出来java.lang.invoke.MethodHandle(方法句柄),性能优于反射 invoke,是"更高性能的反射"——Spring、Netty 等框架在热点路径上会用 MethodHandle 替代 Method.invokegetConstructor()(public 无参构造)——如果被测类的构造是 private,需要用 getDeclaredConstructor() + setAccessible(true)(上一课 08-12 已学);框架统一要求无参构造,就是为了避免处理各种构造器参数java.lang.Class(getConstructor/getDeclaredMethods 等反射入口)、java.lang.reflect.Method(invoke/setAccessible/getName/getParameterTypes)、java.lang.reflect.InvocationTargetException(包装被调方法异常的反射专用异常,getCause 获取真实异常)、JUnit 5 User Guide(@Test 注解与扩展模型、断言机制)