单元测试、反射、注解与动态代理:理解框架的基础机制
用断言确认行为,用元数据发现方法,用代理复用公共逻辑。
继续上次小节 →Java 进阶线 · 线路目录
Java 进阶线
对象与类型设计3 站
文件、配置与并发4 站
网络、框架与复习3 站
本页导读
day14|Junit 单元测试与 java.lang.reflect 包 API
本日学完 JavaSE 的最后四块内容:Junit 单元测试(怎么写测试、测试前后做什么)、反射(Class/Constructor/Field/Method 四件套)、注解(自定义注解、元注解、注解解析)与动态代理(Proxy + InvocationHandler)。这四块在日常业务代码里用得少,但它们是 Spring、JUnit 等框架的底层机制——学完应能读懂”框架凭什么能自动帮我们做事”,并能自己写出一个简易框架。
1. 单元测试与 JUnit 快速入门
1.1 什么是单元测试
单元测试就是:针对最小的功能单元(方法),编写测试代码对其进行正确性测试。
1.2 在 main 方法里测试有什么问题
以前的测试方式只有一种:在 main 方法中写测试代码,去调用其他方法进行测试。这种方式有三个问题:
- 无法实现自动化测试:一个方法测试失败,可能影响其他方法的测试(后面的代码根本走不到)。
- 无法得到测试的报告:需要程序员自己去观察测试是否成功。
- 测试代码和业务代码混在一起,没有统一的组织方式。
1.3 JUnit 单元测试框架
JUnit 是第三方公司开源出来的、用来对方法进行测试的框架(很多开发工具已经集成了 JUnit 框架,比如 IDEA)。
相比在 main 方法中测试,它的优点:
- 可以灵活地编写测试代码:可以针对某个方法单独执行测试,也支持一键完成对全部方法的自动化测试,且各自独立(一个失败不影响其他)。
- 不需要程序员去分析测试结果,会自动生成测试报告。
1.4 快速入门步骤
需求:某系统有多个业务方法,请使用 Junit 单元测试框架,编写测试代码完成对这些方法的正确性测试。
具体步骤:
- 将 JUnit 框架的 jar 包导入到项目中(注意:IDEA 集成了 JUnit 框架,不需要我们手工导入)。
- 为需要测试的业务类,定义对应的测试类;并为每个业务方法编写对应的测试方法(必须:公共、无参、无返回值)。
- 测试方法上必须声明
@Test注解,然后在测试方法中编写代码调用被测试的业务方法进行测试。 - 开始测试:选中测试方法,右键选择 “JUnit 运行”;如果测试通过则是绿色,如果测试失败则是红色。
public class StringUtil {
public static void printNumber(String name) {
System.out.println("名字长度:" + name.length());
}
}
public class StringUtilTest {
@Test
public void testPrintNumber() {
StringUtil.printNumber(null);
StringUtil.printNumber("");
StringUtil.printNumber("admin");
}
}
测试某个方法直接右键该方法启动测试;测试全部方法则选择类或者模块启动。红色失败,绿色通过。
1.5 断言(Assert)
(以下内容来自本日讲义补充,PPT 未展开。)断言的意思是:程序员可以提前预测程序的运行结果,检查运行结果是否与预期一致。
public class StringUtilTest {
@Test
public void testGetMaxIndex() {
int i1 = StringUtil.getMaxIndex(null);
Assert.assertEquals("null测试失败!", -1, i1);
int i2 = StringUtil.getMaxIndex("");
Assert.assertEquals("空字符串测试失败!", -1, i2);
int i3 = StringUtil.getMaxIndex("admin");
Assert.assertEquals("admin测试失败!", 4, i3);
System.out.println("测试通过!");
}
}
Assert.assertEquals(消息, 预期值, 实际值):三者不一致时测试失败,并给出预期值与实际值的对比。
2. JUnit 框架的常用注解
2.1 JUnit 4 版本的常用注解
在测试方法执行前执行的方法,常用于初始化资源;在测试方法执行完后再执行的方法,常用于释放资源。
| 注解 | 说明 |
|---|---|
@Test | 测试类中的方法必须用它修饰才能成为测试方法,才能启动执行 |
@Before | 用来修饰一个实例方法,该方法会在每一个测试方法执行之前执行一次 |
@After | 用来修饰一个实例方法,该方法会在每一个测试方法执行之后执行一次 |
@BeforeClass | 用来修饰一个静态方法,该方法会在所有测试方法之前只执行一次 |
@AfterClass | 用来修饰一个静态方法,该方法会在所有测试方法之后只执行一次 |
2.2 JUnit 5 版本的常用注解
JUnit 5 对 JUnit 4 的注解做了改名,作用完全一样。
| 注解 | 说明 |
|---|---|
@Test | 测试类中的方法必须用它修饰才能成为测试方法,才能启动执行 |
@BeforeEach | 用来修饰一个实例方法,该方法会在每一个测试方法执行之前执行一次 |
@AfterEach | 用来修饰一个实例方法,该方法会在每一个测试方法执行之后执行一次 |
@BeforeAll | 用来修饰一个静态方法,该方法会在所有测试方法之前只执行一次 |
@AfterAll | 用来修饰一个静态方法,该方法会在所有测试方法之后只执行一次 |
(补充说明)JUnit 4 的注解在 org.junit 包下,JUnit 5 的在 org.junit.jupiter.api 包下,两套注解不能混用。
2.3 注解方法的执行顺序
public class StringUtilTest {
@Before
public void before() {
System.out.println("===========before===========");
}
@After
public void after() {
System.out.println("===========after===========");
}
@BeforeClass
public static void beforeClass() {
System.out.println("===========beforeClass===========");
}
@AfterClass
public static void afterClass() {
System.out.println("===========afterClass===========");
}
@Test
public void testPrintNumber() {
StringUtil.printNumber(null);
StringUtil.printNumber("");
StringUtil.printNumber("admin");
}
@Test
public void testGetMaxIndex() {
// 断言测试...
}
}
执行结果的特点:
- 被
@BeforeClass标记的方法,执行在所有方法之前。 - 被
@AfterClass标记的方法,执行在所有方法之后。 - 被
@Before标记的方法,执行在每一个@Test方法之前。 - 被
@After标记的方法,执行在每一个@Test方法之后。
2.4 应用场景:测试前后的资源初始化与释放
假设每个测试方法都要用同一个 Socket 对象,用完还要关闭。就可以把”开一次、关一次”的工作交给 @BeforeClass / @AfterClass:
public class StringUtilTest {
private static Socket socket;
@BeforeClass
public static void beforeClass() {
System.out.println("===========beforeClass===========");
// 初始化 Socket 对象
socket = new Socket();
}
@AfterClass
public static void afterClass() {
System.out.println("===========afterClass===========");
// 关闭 Socket
socket.close();
}
}
3. 反射:认识反射与获取 Class 对象
3.1 反射是什么
Java API 文档中对反射的说明翻译成人话就是:反射(Reflection)就是加载类,并允许以编程的方式解剖类中的各种成分(成员变量、方法、构造器等)。
3.2 反射学什么
先充分认识”获取类的信息、操作它们”,再了解它的作用和应用场景。学习路线共四步:
- 反射第一步:加载类,获取类的字节码
Class对象。 - 获取类的构造器:
Constructor对象。 - 获取类的成员变量:
Field对象。 - 获取类的成员方法:
Method对象。
3.3 获取 Class 对象的三种方式
反射的第一步是将字节码加载到内存,获取字节码对象(Student.java → Student.class → 加载进内存)。有三种写法:
| 方式 | 代码 | 说明 |
|---|---|---|
| 方式一 | Class c1 = 类名.class | 类名点 class |
| 方式二 | Class c2 = Class.forName("全类名") | 调用 Class 提供的 public static Class forName(String package) |
| 方式三 | Class c3 = 对象.getClass() | 调用 Object 提供的 public Class getClass() |
public class Test1Class {
public static void main(String[] args) throws Exception {
// 1、方式一:类名.class
Class c1 = Student.class;
System.out.println(c1);
// 2、方式二:Class.forName(全类名)
Class c2 = Class.forName("com.itheima.d2_reflect.Student");
System.out.println(c2);
// 3、方式三:对象.getClass();
Student s1 = new Student();
Class c3 = s1.getClass();
System.out.println(c3);
System.out.println(c1 == c2); // true
System.out.println(c2 == c3); // true
}
}
三种方式获取到的字节码对象是同一个(== 为 true)。此外 c1.getName() 获取全类名、c1.getSimpleName() 获取简单类名。
课件说法:反射第一步是”加载类,获取类的字节码”。更准确的表述:类的加载由虚拟机按需触发(第一次使用该类、执行
Class.forName()等时),反射拿到的只是同一个字节码对象在内存中的引用。
3.4 反射就在身边:IDEA 的代码提示
用对象调用方法时,IDEA 会把该对象能调用的方法列出来供选择。IDEA 怎么知道对象有这些方法?因为对象能调用的方法全都来自类,IDEA 通过反射技术获取类中有哪些方法,再以提示框形式显示出来。
4. 反射获取类的构造器
4.1 获取构造器的 API
Class 提供了从类中获取构造器的方法:
| 方法 | 说明 |
|---|---|
Constructor<?>[] getConstructors() | 获取全部构造器(只能获取 public 修饰的) |
Constructor<?>[] getDeclaredConstructors() | 获取全部构造器(只要存在就能拿到) |
Constructor<T> getConstructor(Class<?>... parameterTypes) | 获取某个构造器(只能获取 public 修饰的) |
Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes) | 获取某个构造器(只要存在就能拿到) |
记忆规律:
get:获取。Declared:有这个单词表示可以获取任意一个,没有这个单词表示只能获取 public 修饰的。Constructor:构造方法的意思。- 后缀
s:表示可以获取多个,没有后缀s只能获取一个。
4.2 获取全部构造器与单个构造器
public class Test2Constructor {
@Test
public void testGetConstructors() {
// 1、反射第一步:必须先得到这个类的 Class 对象
Class c = Cat.class;
// 2、获取类的全部构造器
// Constructor[] constructors = c.getConstructors(); // 只能拿 public 的
Constructor[] constructors = c.getDeclaredConstructors(); // 存在即可拿
// 3、遍历构造器
for (Constructor con : constructors) {
System.out.println(con.getName() + " ===> " + con.getParameterCount());
}
}
@Test
public void testGetConstructor() throws Exception {
// 1、反射第一步:必须先得到这个类的 Class 对象
Class c = Cat.class;
// 2、获取类的某个构造器:无参数构造器
Constructor con1 = c.getDeclaredConstructor(); // 定位无参数的
Constructor con2 = c.getDeclaredConstructor(String.class, int.class); // 定位有参数的
}
}
4.3 构造器的作用:初始化对象并返回
获取到类的构造器,作用依然是初始化对象返回。使用 Constructor 中的方法:
| 方法 | 说明 |
|---|---|
T newInstance(Object... initargs) | 调用此构造器对象表示的构造器完成对象初始化(返回对象) |
void setAccessible(boolean flag) | 设置禁用访问控制检查(暴力反射),非 public 构造器必须先打开权限 |
// 3、得到构造器的目的依然是初始化对象返回
Cat c1 = (Cat) con1.newInstance();
System.out.println(c1);
con2.setAccessible(true); // 禁止检查访问权限(暴力反射)
Cat c2 = (Cat) con2.newInstance("叮当猫", 30);
System.out.println(c2);
5. 反射获取类的成员变量
5.1 获取成员变量的 API
| 方法 | 说明 |
|---|---|
public Field[] getFields() | 获取类的全部成员变量(只能获取 public 修饰的) |
public Field[] getDeclaredFields() | 获取类的全部成员变量(只要存在就能拿到) |
public Field getField(String name) | 获取类的某个成员变量(只能获取 public 修饰的) |
public Field getDeclaredField(String name) | 获取类的某个成员变量(只要存在就能拿到) |
命名规律同上:有 Declared 表示”任意权限都能拿”,带后缀 s 表示”拿多个”。
5.2 成员变量的作用:取值与赋值
| 方法 | 说明 |
|---|---|
void set(Object obj, Object value) | 给某个对象的该成员变量赋值 |
Object get(Object obj) | 取出某个对象中该成员变量的值 |
void setAccessible(boolean flag) | 非 public 成员变量需要先打开权限(暴力反射),再取值、赋值 |
public class Test3Field {
@Test
public void testGetFields() throws Exception {
// 1、反射第一步:必须先得到类的 Class 对象
Class c = Cat.class;
// 2、获取类的全部成员变量
Field[] fields = c.getDeclaredFields();
// 3、遍历这个成员变量数组
for (Field field : fields) {
System.out.println(field.getType() + "==>" + field.getName());
}
// 4、定位某个成员变量
Field fname = c.getDeclaredField("name");
// 5、成员变量的作用依然是:取值和赋值
Cat cat = new Cat();
fname.setAccessible(true); // 暴力反射
fname.set(cat, "哆啦A梦"); // 赋值
String name = (String) fname.get(cat); // 取值
System.out.println(name);
}
}
6. 反射获取类的成员方法
6.1 获取成员方法的 API
| 方法 | 说明 |
|---|---|
Method[] getMethods() | 获取类的全部成员方法(只能获取 public 修饰的) |
Method[] getDeclaredMethods() | 获取类的全部成员方法(只要存在就能拿到) |
Method getMethod(String name, Class<?>... parameterTypes) | 获取类的某个成员方法(只能获取 public 修饰的) |
Method getDeclaredMethod(String name, Class<?>... parameterTypes) | 获取类的某个成员方法(只要存在就能拿到) |
6.2 成员方法的作用:执行它
| 方法 | 说明 |
|---|---|
Object invoke(Object obj, Object... args) | 在某个对象上触发该方法执行,返回方法返回值 |
void setAccessible(boolean flag) | 非 public 成员方法需要先打开权限(暴力反射),再触发执行 |
public class Test4Method {
@Test
public void testGetMethods() throws Exception {
// 1、反射第一步:先得到 Class 对象
Class c = Cat.class;
// 2、获取类的全部成员方法
Method[] methods = c.getDeclaredMethods();
// 3、遍历这个数组中的每个方法对象
for (Method method : methods) {
System.out.println(method.getName() + " " + method.getParameterCount());
}
// 4、定位单个方法
Method eat1 = c.getDeclaredMethod("eat");
Method eat2 = c.getDeclaredMethod("eat", String.class);
// 5、定位方法的目的是:依然是执行它!
Cat cat = new Cat();
Object result = eat1.invoke(cat);
System.out.println(result);
eat2.setAccessible(true);
Object result2 = eat2.invoke(cat, "鱼儿");
System.out.println(result2);
}
}
无返回值的方法(如 run())执行后 invoke() 返回 null;有返回值的方法返回真实结果。
7. 反射的作用与应用场景
7.1 反射的作用
- 基本作用:可以得到一个类的全部成分然后操作。
- 可以破坏封装性(很突出)。
- 也可以破坏泛型的约束性(很突出)。
- 最重要的用途是:适合做 Java 的框架——基本上主流框架都会基于反射设计出一些通用的功能。
7.2 应用:用反射做一个简易版框架
需求:对于任意一个对象,该框架都可以把对象的字段名和对应的值,保存到文件中去。
实现步骤:
- 定义一个方法,可以接收任意对象。
- 每收到一个对象后,使用反射获取该对象的
Class对象,然后获取全部的成员变量。 - 遍历成员变量,然后提取成员变量在该对象中的具体值。
- 把成员变量名和其值,写出到文件中去。
public class ObjectFrame {
// 目标:保存任意对象的字段和其数据到文件中去
public static void saveObject(Object obj) throws Exception {
// 这个对象中有多少个字段我们不清楚,谁清楚呢?只有反射可以解决。
PrintWriter pw = new PrintWriter(new FileWriter("day14-junit-reflect-annotation-proxy/src/obj.txt", true));
Class c = obj.getClass();
// 0.还得知道是啥对象
// String className = c.getName(); // com.itheima.d2_reflect.Student
String className = c.getSimpleName(); // Student
pw.println("==================" + className + "====================");
// 1、提取这个 Class 对象中的全部字段
Field[] fields = c.getDeclaredFields();
for (Field field : fields) {
String name = field.getName();
field.setAccessible(true);
String value = field.get(obj) + "";
pw.println(name + "=" + value);
}
pw.close();
}
}
public class Test5Frame {
@Test
public void save() throws Exception {
Student s1 = new Student("柳岩", 40, '女', 167.5, "女星");
Teacher s2 = new Teacher("波妞", 6000);
ObjectFrame.saveObject(s1);
ObjectFrame.saveObject(s2);
}
}
运行后生成的文件内容如下(框架对任意对象都能工作,对象的字段名与值一一对应):
8. 注解(Annotation)
8.1 认识注解
注解就是 Java 代码里的特殊标记,比如 @Override、@Test 等,作用是:让其他程序根据注解信息来决定怎么执行该程序。
注意:注解可以用在类上、构造器上、方法上、成员变量上、参数上等位置。
8.2 自定义注解
自定义注解的格式:
public @interface 注解名称 {
public 属性类型 属性名() default 默认值;
}
public @interface MyTest1 {
String aaa();
boolean bbb() default true; // default true 表示默认值为 true,使用时可以不赋值
String[] ccc();
}
使用注解时需要加 @ 符号:
@MyTest1(aaa = "牛魔王", ccc = {"HTML", "Java"})
public class AnnotationTest1 {
@MyTest1(aaa = "铁扇公主", bbb = false, ccc = {"Python", "前端", "Java"})
public void test1() {
}
}
特殊属性名 value:如果注解中只有一个 value 属性,使用注解时 value 名称可以不写。
public @interface MyTest2 {
String value(); // 特殊属性
int age() default 10;
}
@MyTest2("孙悟空") // 等价于 @MyTest2(value = "孙悟空")
public class AnnotationTest1 {
}
8.3 注解的本质
对注解的字节码进行反编译可以看到:
// 源码里定义的
public @interface MyTest1 {
String aaa();
boolean bbb();
String[] ccc();
}
// 反编译后等价于
public interface MyTest1 extends Annotation {
public abstract String aaa();
public abstract boolean bbb();
public abstract String[] ccc();
}
结论:
- 注解本质是一个接口,Java 中所有注解都继承了
Annotation接口。 - 注解中的属性,本质上是抽象方法。
@注解(...)其实就是一个实现类对象,实现了该注解以及Annotation接口。- 注解里的属性值(如
aaa = "孙悟空"),可以通过调用aaa()、bbb()、ccc()方法获取到——解析注解时会用到。
8.4 注解的作用
对 Java 中类、方法、成员变量做标记,然后进行特殊处理。
例如:JUnit 框架中,标记了注解 @Test 的方法就可以被当成测试方法执行,而没有标记的就不能当成测试方法执行。
9. 元注解
9.1 什么是元注解
元注解指的是修饰注解的注解。
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Test {
}
9.2 @Target:声明注解只能在哪些位置使用
| 取值 | 位置 |
|---|---|
ElementType.TYPE | 类、接口 |
ElementType.FIELD | 成员变量 |
ElementType.METHOD | 成员方法 |
ElementType.PARAMETER | 方法参数 |
ElementType.CONSTRUCTOR | 构造器 |
ElementType.LOCAL_VARIABLE | 局部变量 |
@Target(ElementType.TYPE) // 声明 @MyTest3 注解只能用在类上
public @interface MyTest3 {
}
如果把 @MyTest3 用在方法上、变量上,IDEA 会直接报错;写成数组形式就可以声明多个位置:
// 声明 @MyTest3 注解只能用在类上和方法上
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface MyTest3 {
}
9.3 @Retention:声明注解的保留周期
| 取值 | 含义 |
|---|---|
RetentionPolicy.SOURCE | 只作用在源码阶段,字节码文件中不存在 |
RetentionPolicy.CLASS | 保留到字节码文件阶段,运行阶段不存在(默认值) |
RetentionPolicy.RUNTIME | 一直保留到运行阶段(开发常用) |
@Target({ElementType.TYPE, ElementType.METHOD}) // 只能用类上和方法上
@Retention(RetentionPolicy.RUNTIME) // 保留到运行时期
public @interface MyTest3 {
}
@Target约束自定义注解可以标记的范围,@Retention用来约束自定义注解的存活范围。
10. 注解的解析
10.1 什么是注解的解析
注解的解析就是:判断类上、方法上、成员变量上是否存在注解,并把注解里的内容解析出来。
指导思想:要解析谁上面的注解,就应该先拿到谁。
- 要解析类上面的注解,先获取该类的
Class对象,再通过Class对象解析其上面的注解。 - 要解析成员方法上的注解,先获取该成员方法的
Method对象,再通过Method对象解析其上面的注解。
Class、Method、Field、Constructor 都实现了 AnnotatedElement 接口,它们都拥有解析注解的能力。
10.2 AnnotatedElement 接口提供的解析注解方法
| 方法 | 说明 |
|---|---|
public Annotation[] getDeclaredAnnotations() | 获取当前对象上面的注解 |
public T getDeclaredAnnotation(Class<T> annotationClass) | 获取指定的注解对象 |
public boolean isAnnotationPresent(Class<Annotation> annotationClass) | 判断当前对象上是否存在某个注解 |
10.3 解析案例
需求:定义注解 MyTest4(含 String value()、double aaa() 默认值 100、String[] bbb(),只能用在类和成员方法上,保留到运行时);定义类 Demo,在类与其 test1 方法上使用该注解;定义测试类 AnnotationTest3 解析 Demo 类中的全部注解。
① 定义注解
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTest4 {
String value();
double aaa() default 100;
String[] bbb();
}
② 使用注解
@MyTest4(value = "蜘蛛侠", aaa = 99.9, bbb = {"至尊宝", "黑马"})
public class Demo {
@MyTest4(value = "孙悟空", aaa = 199.9, bbb = {"紫霞", "牛夫人"})
public void test1() {
}
}
③ 解析注解
public class AnnotationTest3 {
@Test
public void parseClass() {
// 1.先获取 Class 对象
Class c = Demo.class;
// 2.解析 Demo 类上的注解
if (c.isAnnotationPresent(MyTest4.class)) {
MyTest4 myTest4 = (MyTest4) c.getDeclaredAnnotation(MyTest4.class);
System.out.println(myTest4.value());
System.out.println(myTest4.aaa());
System.out.println(myTest4.bbb());
}
}
@Test
public void parseMethods() {
// 1.先获取 Class 对象
Class c = Demo.class;
// 2.解析 Demo 类中 test1 方法上的注解
Method m = c.getDeclaredMethod("test1");
if (m.isAnnotationPresent(MyTest4.class)) {
MyTest4 myTest4 = (MyTest4) m.getDeclaredAnnotation(MyTest4.class);
System.out.println(myTest4.value());
System.out.println(myTest4.aaa());
System.out.println(myTest4.bbb());
}
}
}
11. 注解的应用场景:模拟 JUnit 框架
需求:定义若干个方法,只要加了 MyTest 注解,就会触发该方法执行。
分析:
- 定义一个自定义注解
MyTest,只能注解方法,存活范围是一直都在。 - 定义若干个方法,部分方法加上
@MyTest注解修饰,部分方法不加。 - 模拟一个 JUnit 程序,可以触发加了
@MyTest注解的方法执行。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTest {
}
public class AnnotationTest4 {
@MyTest
public void test1() {
System.out.println("=====test1====");
}
@MyTest
public void test2() {
System.out.println("=====test2====");
}
public void test3() {
System.out.println("=====test3====");
}
public static void main(String[] args) throws Exception {
AnnotationTest4 a = new AnnotationTest4();
// 1.先获取 Class 对象
Class c = AnnotationTest4.class;
// 2.解析 AnnotationTest4 类中所有的方法对象
Method[] methods = c.getDeclaredMethods();
for (Method m : methods) {
// 3.判断方法上是否有 MyTest 注解,有就执行该方法
if (m.isAnnotationPresent(MyTest.class)) {
m.invoke(a);
}
}
}
}
这段代码就是 JUnit 的核心思路:注解负责标记,反射负责找到标记并执行。代码工程里还有一个扩展版:注解 @TestDlei 带一个 int value() default 1 属性,表示该方法重复执行的次数(片段)。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestDlei {
int value() default 1;
}
// 解析时读出注解属性,决定执行次数
if (method.isAnnotationPresent(TestDlei.class)) {
TestDlei dlei = method.getDeclaredAnnotation(TestDlei.class);
int count = dlei.value();
for (int i = 0; i < count; i++) {
method.invoke(t);
}
}
12. 动态代理
12.1 程序为什么需要代理
以大明星为例:唱歌、跳舞是明星真正擅长的事,但每次唱歌前要准备话筒、收钱,跳舞前要准备场地、收钱——这些繁琐的事明星不想干,于是找一家中介公司(代理)来处理。
唱歌() {
真正开始唱歌
}
跳舞() {
真正开始跳舞
}
对象如果嫌身上干的事太多,可以通过代理来转移部分职责。
12.2 代理长什么样:由接口决定
中介公司怎么知道要派一个有唱歌、跳舞方法的代理呢?对象有什么方法想被代理,代理就一定要有对应的方法——Java 用接口来保证这一点:
public interface Star {
String sing(String name);
void dance(String name);
}
明星类实现这个接口,Proxy 就能根据该接口生成一个同样具有这些方法的代理对象:
public class SuperStar implements Star {
private String name;
public SuperStar(String name) {
this.name = name;
}
@Override
public String sing(String name) {
System.out.println(this.name + "唱歌一首:" + name);
return "谢谢!谢谢!";
}
@Override
public void dance(String name) {
System.out.println(this.name + "跳了一支:" + name);
}
}
12.3 生成代理对象:Proxy.newProxyInstance()
java.lang.reflect.Proxy 类提供了为对象产生代理对象的方法:
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
| 参数 | 含义 |
|---|---|
参数一 loader | 用于指定用哪个类加载器去加载生成的代理类 |
参数二 interfaces | 指定接口,这些接口用于指定生成的代理长什么,也就是有哪些方法 |
参数三 h | 用来指定生成的代理对象要干什么事情 |
12.4 InvocationHandler.invoke():代理到底干什么
public class ProxyUtil {
// 为实现类对象创建代理对象,并返回
public static Star createProxy(SuperStar zrn) {
Star proxy = (Star) Proxy.newProxyInstance(ProxyUtil.class.getClassLoader(),
zrn.getClass().getInterfaces(), new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// proxy:代理对象本身,一般我们不用
// method:正在代理的方法,哪个方法被调用,method 就代表哪个方法
// args:正在被代理的方法的参数
if ("sing".equals(method.getName())) {
System.out.println("准备话筒,收钱20万!");
} else if ("dance".equals(method.getName())) {
System.out.println("准备场地,收钱300万!");
}
// 其他的行为都是真实明星对象自己去做!
Object result = method.invoke(zrn, args);
return result;
}
});
return proxy;
}
}
调用方式:以后找代理唱歌、跳舞即可。
public class Test {
public static void main(String[] args) {
// 1、创建一个明星对象
SuperStar zrn = new SuperStar("章若楠");
// 2、创建章若楠的代理对象
Star proxy = ProxyUtil.createProxy(zrn);
System.out.println(proxy.sing("好日子"));
proxy.dance("科目三");
}
}
12.5 代理的执行流程
每次用代理对象调用接口中的方法时,都会回调 InvocationHandler 中的 invoke() 方法,在其中执行”代理要干的事”,再通过 method.invoke(真实对象, args) 把真正的工作交回给真实对象。
说明:本日讲义中引用的”代理执行流程图”(讲义 assets 中的
1669622605490.png)在讲义 assets 目录里缺失,未能归档,此处以文字描述流程。
12.6 应用:用代理优化用户管理类
场景:某系统有一个用户管理类,包含用户登录、删除用户、查询用户等功能,系统要求统计每个功能的执行耗时情况,以便后期观察程序性能。
原来的实现里每个方法都自己写”记录开始时间 → 业务 → 记录结束时间 → 打印耗时”,重复代码多,而且这些代码不属于业务本身:
/**
* 用户业务接口
*/
public interface UserService {
// 登录功能
void login(String loginName, String passWord) throws Exception;
// 删除用户
void deleteUsers() throws Exception;
// 查询用户,返回数组的形式
String[] selectUsers() throws Exception;
}
改造思路:把计算耗时的操作交给代理对象来做,业务实现类只保留业务代码。
public class ProxyUtil {
public static UserService createProxy(UserServiceImpl obj) {
UserService proxy = (UserService) Proxy.newProxyInstance(ProxyUtil.class.getClassLoader(),
obj.getClass().getInterfaces(), new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
long startTime = System.currentTimeMillis();
// 真正走业务对象的方法
Object result = method.invoke(obj, args);
long end = System.currentTimeMillis();
System.out.println(method.getName() + "方法耗时:" + (end - startTime) / 1000.0 + "s");
return result;
}
});
return proxy;
}
}
public class Test {
public static void main(String[] args) throws Exception {
// 1、创建用户业务对象(实际上是代理对象)
UserService userService = ProxyUtil.createProxy(new UserServiceImpl());
// 2、调用用户业务的功能
userService.login("admin", "123456");
System.out.println("----------------------------------------------------");
userService.deleteUsers();
System.out.println("----------------------------------------------------");
String[] names = userService.selectUsers();
System.out.println("查询到的用户是:" + Arrays.toString(names));
}
}
这样:业务实现类只关心业务,公共的耗时统计被集中到代理的 invoke() 中——这就是代理最典型的好处(Spring 的事务、日志、权限校验也是同样的套路)。
13. 代码工程与作业
- 本日配套讲义(兼作业与校对材料):
day14-Java高级技术/讲义/day14-单元测试、反射、注解、动态代理/day14-单元测试、反射、注解、动态代理.md - 本日代码工程:
day14-Java高级技术/代码/day14-junit-reflect-annotation-proxy/,包结构与今天的四块内容一一对应:com.itheima.d1_junit:StringUtil、StringUtilTest(@Before/@After/@BeforeClass/@AfterClass+ 断言)com.itheima.d2_reflect:Test1Class、Test2Constructor、Test3Field、Test4Method、ObjectFrame、Test5Frame、Cat、Student、Teachercom.itheima.d3_annotation:MyTestMyTest4、AnnotationDemo1AnnotationDemo4、TestDleicom.itheima.d4_proxy:Star、SuperStar、ProxyUtil、Testcom.itheima.d5_proxy2:UserService、UserServiceImpl、ProxyUtil、Test
- 练习要点:把
ObjectFrame.saveObject()改成同时输出字段类型;给MyTest版模拟 JUnit 框架加上”统计通过/失败个数并打印报告”。
14. 复习要点
- 单元测试的定义(针对最小功能单元”方法”),以及在
main中测试的三个问题(无法自动化、失败会互相影响、拿不到测试报告)。 - JUnit 的两个优点与快速入门四步;测试方法的三条硬性要求:公共、无参、无返回值,且必须用
@Test标记;红色失败、绿色通过。 - 断言
Assert.assertEquals(消息, 预期值, 实际值)的作用与失败报告形式。 - JUnit 4 与 JUnit 5 注解的对应关系(
@Before→@BeforeEach、@After→@AfterEach、@BeforeClass→@BeforeAll、@AfterClass→@AfterAll)与执行时机(实例方法每个测试方法执行一次,静态方法整体只执行一次)。 - 反射的定义(加载类并以编程方式解剖类中各种成分)与学习四步:
Class→Constructor→Field→Method。 - 获取
Class对象的三种方式(类名.class、Class.forName(全类名)、对象.getClass()),三者是同一个对象。 - 获取构造器/成员变量/成员方法的 API 与命名规律:有
Declared能拿任意权限、没有只能拿 public;带s拿多个、不带拿一个。 - 三件套的作用:构造器
newInstance()初始化对象、成员变量set()/get()取值赋值、成员方法invoke()触发执行;非 public 成员需要setAccessible(true)(暴力反射)。 - 反射的作用:获取类的全部成分并操作、破坏封装性、破坏泛型约束性,最重要的用途是做 Java 框架(简易框架
ObjectFrame、IDEA 代码提示)。 - 注解的定义与作用;自定义注解的格式、
value属性可省略名称的规则;注解本质是继承Annotation接口的接口,@注解(...)是实现类对象。 - 元注解
@Target(ElementType各取值)与@Retention(SOURCE / CLASS / RUNTIME)的作用。 - 注解解析的指导思想(注解在谁身上就先拿到谁)与
AnnotatedElement的三个方法;模拟 JUnit 框架的完整思路(注解标记 + 反射执行)。 - 动态代理三要素:
Proxy.newProxyInstance(loader, interfaces, handler)三个参数的含义、invoke(proxy, method, args)三个参数的含义与回调时机、接口决定代理有哪些方法;能说出用代理消除耗时统计重复代码的好处。