文章目录
- 学习日记(单元测试、反射、注解、动态代理)
- 一、单元测试
- 1. 单元测试实践
- 2. JUnit 常用注解
- 二、反射
- 1. 反射获取类对象
- 2. 反射获取构造器对象
- 3. 反射获取成员变量对象
- 4. 反射获取成员方法对象
- 三、反射的作用举例
- 1. 绕过编译阶段为集合添加数据
- 2. 通用框架
- 四、注解
- 1. 自定义注解
- 2. 元注解
- 3. 注解解析
- 4. 注解的应用:模拟 JUnit 框架
- 五、动态代理
- 1. 案例 1
- 2. 案例 2
- 注意:
学习日记(单元测试、反射、注解、动态代理)
一、单元测试
单元测试就是针对最小的功能单元编写测试代码,Java 程序最小的功能单元是方法。
单元测试就是针对 Java 方法的测试,进而检查方法的正确性,需要用到 JUnit 单元测试框架。
JUnit 单元测试框架是使用 Java 语言实现的,是开源的,几乎所有的 IDE 工具都集成了 JUnit。
JUnit 单元测试框架的优点:
- JUnit 可以灵活地选择执行哪些测试方法,也可以一键执行全部测试方法;
- JUnit 可以生成全部方法的测试报告,测试良好为绿色,测试失败为红色;
- 单元测试中的某个方法测试失败了,不会影响其他测试方法的测试。
1. 单元测试实践
使用单元测试进行业务方法预期结果、正确性的测试,步骤:
- 将 JUnit 的 jar 包导入到项目中,IDEA 通常整合好了 JUnit 框架,一般不需要导入;
- 单独创建一个测试类,编写测试方法,每个测试方法必须是公共的、无参数、无返回值的非静态方法;
- 在测试方法上使用
@Test
注解,第一次使用时需要Alt + Enter
导入包org.junit.Test
,标注该方法是一个测试方法; - 在测试方法中完成被测试方法的预期正确性测试,选择测试方法,选择“ JUnit 运行”。
方法名 | 说明 |
---|---|
public static void assertEquals(String message, long expected, long actual) | 断言,类 Assert 直接调用 |
说明:参数一是预期值与实际值不相同时,给出的提示信息;参数二是预期值;参数三是实际值。
当没有问题时
当出现问题时
注意:
测试类和测试方法的命名应该规范。
每个测试方法必须是公共的、无参数、无返回值的非静态方法。
测试方法应该使用
@Test
注解标记。一个业务方法对应一个测试方法。
运行测试方法时,可以单独测试方法,也可以一起测试所有方法。
2. JUnit 常用注解
JUnit 4 版本
注解 | 说明 |
---|---|
@Test | 测试方法 |
@Before | 用来修饰实例方法,该方法会在每一个测试方法执行之前执行一次 |
@After | 用来修饰实例方法,该方法会在每一个测试方法执行之后执行一次 |
@BeforeClass | 用来修饰静态方法,该方法会在所有测试方法执行之前只执行一次 |
@AfterClass | 用来修饰静态方法,该方法会在所有测试方法执行之后只执行一次 |
注意:
- 开始执行的方法:一般用来完成初始化资源;
- 执行完之后的方法:一般用来释放资源。
二、反射
反射是在运行时获取类的字节码文件对象,然后可以解析这个类的全部成分。
在运行时,可以直接得到这个类的构造器对象(Constructor)、成员变量对象(Field)、成员方法对象(Method)。
这种运行时动态获取类信息以及动态调用类中成分的能力称为 Java 语言的反射机制。
反射的核心思想和关键:得到编译后的 Class文件对象。
反射的作用:
- 可以在运行时得到一个类的全部成分然后操作;
- 可以破坏封装性;
- 可以破坏泛型的约束性。
反射适合的用途:做 Java 的高级框架。
1. 反射获取类对象
反射的第一步是先得到编译后的 Class 类对象,有三种方法。
- 方法一:调用 Class 类中的一个静态方法
forName
。 - 方法二:类调用
class
方法。 - 方法三:对象调用
getClass
方法获取对应类的 Class 对象。
方法名 | 说明 | |
---|---|---|
方法一 | public static Class<?> forName(String className) | Class 类调用 |
方法二 | class | 类调用 |
方法三 | public final native Class<?> getClass() | 对象调用 |
注意:方法一中的参数为全限名,也就是包名 + 类名。
2. 反射获取构造器对象
总体步骤:
- 获得 Class 对象;
- (通过 Class 对象)获得构造器 Constructor 对象;
- (通过构造器)创建对象。
第二步:Class 类获得构造器 Constructor 对象的方法:
方法名 | 说明 |
---|---|
public Constructor<?>[] getDeclaredConstructors() | 返回所有构造器对象的数组,存在就能拿到 |
public Constructor getDeclaredConstructor(Class<?>… parameterTypes) | 返回单个构造器对象,存在就能拿到 |
注意:
这两个方法都是 Class 对象调用。
这两个方法获取构造器,无所谓构造器的声明类型,即,无所谓构造器是 public 还是 private。
返回单个构造器对象时,方法的参数为 Class 对象且为可变参数,如:
String.class
、int.class
。返回单个构造器对象时,若不写参数,则返回无参构造器,有参数,返回有参构造器。
第三步:创建对象的方法:
方法名 | 说明 |
---|---|
public T newInstance(Object … initargs) | 根据指定的构造器创建对象 |
public void setAccessible(boolean flag) | 参数设置为 true,表示取消访问检查,进行暴力反射 |
注意:
- 这两个方法都是构造器对象调用。
- 第二个方法是当构造器方法不是用 public 修饰时使用,打开权限,可以有效一次,反射可以破坏封装性,私有的也可以执行。
- 第一个方法中,当构造器为无参时,方法不写参数;当构造器为有参时,方法中的参数为初始化值。
第二步代码示例:获取所有构造器对象的数组、获取单个构造器对象
第三步代码示例:初始化对象、暴力反射
3. 反射获取成员变量对象
总体步骤:
- 获得 Class 对象;
- (通过 Class 对象)获得成员变量 Field 对象;
- (通过成员变量)赋值或者取值。
第二步:Class 类获得成员变量 Field 对象的方法:
方法名 | 说明 |
---|---|
public Field[] getDeclaredFields() | 返回所有成员变量对象的数组,存在就能拿到 |
public Field getDeclaredField(String name) | 返回单个成员变量对象,存在就能拿到 |
注意:
这两个方法都是 Class 对象调用。
这两个方法获取成员变量对象,无所谓成员变量的声明类型,即,无所谓成员变量是 public 还是 private。
第二个方法的参数为成员变量的名称,即属性的名称。
第三步:赋值或者取值的方法:
方法名 | 说明 |
---|---|
public void set(Object obj, Object value) | 赋值 |
public Object get(Object obj) | 取值 |
public void setAccessible(boolean flag) | 参数设置为 true,表示取消访问检查,进行暴力反射 |
注意:
- 这三个方法都是成员变量对象调用。
- 当成员变量对象不是用 public 修饰时使用 setAccessible,打开权限,反射可以破坏封装性,私有的也可以执行。
- 在赋值方法中,参数一是创建的对象,参数二是为该对象指定的属性赋值。
第二步代码示例:获取所有成员变量对象的数组、获取单个成员变量对象
第三步代码示例:赋值、取值、暴力反射
4. 反射获取成员方法对象
总体步骤:
- 获得 Class 对象;
- (通过 Class 对象)获得成员方法 Method 对象;
- (通过成员方法对象)运行方法。
第二步:Class 类获得成员方法对象的方法:
方法名 | 说明 |
---|---|
public Method[] getDeclaredMethods() | 返回所有成员方法对象的数组,存在就能拿到 |
public Method getDeclaredMethod(String name, Class<?>… parameterTypes) | 返回单个成员方法对象,存在就能拿到 |
注意:
这两个方法都是 Class 对象调用。
这两个方法获取成员方法对象,无所谓成员方法的声明类型,即,无所谓成员方法是 public 还是 private。
第二个方法的第一个参数为:方法名;第二个参数为:该方法参数类型的 Class 对象,如:
int.class
,若该方法没有参数,则可以不写第二个参数。
第三步:运行方法:
方法名 | 说明 |
---|---|
public Object invoke(Object obj, Object… args) | 运行方法 |
public void setAccessible(boolean flag) | 参数设置为 true,表示取消访问检查,进行暴力反射 |
注意:
- 这三个方法都是成员方法对象调用。
- 当成员方法对象不是用 public 修饰时使用 setAccessible,打开权限,反射可以破坏封装性,私有的也可以执行。
- 第一个方法的第一个参数:对象;第二个参数:传递的参数(如果没有参数可以不写)。
第二步代码示例:获取所有成员方法对象的数组、获取单个成员方法对象
第三步代码示例:运行方法、暴力反射
三、反射的作用举例
1. 绕过编译阶段为集合添加数据
泛型只是在编译阶段可以约束集合只能操作某种数据类型,在编译成 Class 文件进入运行阶段的时候,其真实类型都是 ArrayList,泛型相当于被擦除,此时,可以为集合存入其他任意类型的元素,这就用到了反射,因为反射是在运行的技术。
通过反射为集合添加其他类型的数据
2. 通用框架
需求:给定任意一个对象,在不清楚对象字段的情况下,可以把对象的字段名称和对应值存储到文件中去。
分析:
- 定义一个类
MybatisUtil
存放save
方法,可以接收任意类型的对象; - 在
save
方法中,应用反射获取该对象的全部成员变量名称以及对应值; - 用打印流
PrintStream
将获取的数据写入到文件中; - 在
main
方法中对象调用save
方法,得到目标文件。
注意:
- 当成员变量是非公有属性时,要用
setAccessible
来打开权限,暴力反射。- Class 对象获取类名的两个方法,如下:
方法名 说明 public String getName() 获取当前 Class 对象的全限名:包名 + 类名 public String getSimpleName() 获取当前 Class 对象的类名
四、注解
注解:Annotation,又称标注。Java 语言中的类、构造器、方法、成员变量、参数等都可以被注解进行标注。
作用:对 Java 中类、构造器、方法、成员变量、参数等做标记,然后进行特殊处理。
例如在 JUnit 框架中,标注了注解 @Test
的方法就可以被当作测试方法执行,而没有标记的就不能当作测试方法执行。
1. 自定义注解
格式
类、构造器、方法、成员变量、参数等都可以被注解进行标注。
特殊属性 value:
- value 属性,如果自定义注解时只有一个 value 属性的情况下,使用 value 属性可以省略 value 名称不写。
- 如果有多个属性,并且多个属性都有默认值,那么 value 名称也可以省略不写。
- 除了上面两种情况外,value 的名称都得写。
2. 元注解
元注解:注解注解的注解。
两个元注解:
@Target
:约束自定义注解只能在哪些地方使用。@Retention
:声明注解的生命周期。
3. 注解解析
注解的操作中经常需要进行解析,注解的解析就是判断是否存在注解,存在注解就解析出内容。
与注解解析相关的接口:
Annotation
:注解的顶级接口,注解都是该类型的对象;AnnotationElement
:该接口定义了与注解解析相关的解析方法。
Class、Method、Field、Constructor 这些类都实现了 AnnotationElement
接口,都拥有解析注解的能力。
解析注解的方法:
方法名 | 说明 |
---|---|
public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) | 判断当前对象是否使用了指定的注释 |
public A getDeclaredAnnotation(Class annotationClass) | 根据注解类型获得对应的注解对象 |
public Annotation[] getDeclaredAnnotations() | 获得当前对象所有注解,返回注解数组 |
注意:方法一和方法二都是由 Class、Method、Field、Constructor 这些类对象调用,参数为注解的类类型,如:
Book.class
。
解析注解的技巧:注解在哪个成分上,就先拿哪个成分对象。
- 注解作用在类上,就先获得该类对应的
Class
对象,然后再拿注解; - 注解作用在成员方法上,就先获得该成员方法对应的
Method
对象,然后再拿注解; - 注解作用在成员变量上,就先获得该成员变量对应的
Field
对象,然后再拿注解。
4. 注解的应用:模拟 JUnit 框架
需求:自定义一个注解
MyTest
,和 JUnit 框架一样,只有方法上有MyTest
注解才可以被执行。
思路:先用反射提取类中的所有方法,用循环依次判断每一个方法是否有 MyTest
注解,有注解然后执行。
五、动态代理
代理也是一个对象,用来对被代理对象的行为额外做一些辅助操作。
Java 中代理的类是:java.lang.reflect.Proxy
,类中有一个静态方法 newProxyInstance
,可以为对象产生一个代理对象返回。
方法名 | 说明 |
---|---|
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) | 为对象返回代理对象 |
注意:
- 参数一:是定义代理类的类加载器,用来产生代理对象,如:
dog.getClass().getClassLoader()
;- 参数二:是代理类要实现的接口列表,用来将被代理的方法交给代理对象,如:
dog.getClass().getInterfaces()
;- 参数三:是代理对象的核心处理程序,用来指定代理对象干什么事,如:
new InvocationHandler()
;
实现动态代理的步骤:
- 必须存在接口,且被代理对象需要实现接口;
- 使用
Proxy
类提供的方法newProxyInstance
,产生一个代理对象。
动态代理的优点:
- 可以在不改变方法源码的情况下,实现对方法功能的增强,提高了代码的复用;
- 简化了编程工作,提高了开发效率,同时提高了软件系统的可扩展性;
- 可以为被代理对象的所有方法做代理;
- 非常灵活,支持为任意接口类型的实现类对象做代理,也可以直接为接口本身做代理。
1. 案例 1
需求:为 dog 对象创建一个代理对象。
注意点:
- 最好将产生代理对象的方法封装到一个类中。
- 必须存在接口,并且被代理对象需要实现接口,代理对象的类型为接口。
- 通过代理对象调用方法的执行流程:先走向代理;代理可以为方法额外做一些辅助工作;用
method
去触发对象方法的执行;回到代理中,由代理负责返回结果给方法的调用者。执行流程图如下。 - 用
method
去触发对象,method
是正在调用的方法,args
是该方法的参数,固定步骤。
2. 案例 2
需求:模拟用户管理业务,包括用户登录、用户删除、用户查询功能,做性能分析,统计每个功能的耗时。
分析:把性能分析交给代理去统一完成,这样可以省去在每个方法中都进行一遍性能分析,提高了代码的复用性。
注意:必须要有接口,并且实现类要实现接口,代理通常是基于接口实现的。
注意:
- 通过这种方式也可以为集合添加其他类型的元素。
对比
- 动态代理非常灵活,支持为任意接口类型的实现类对象做代理,也可以直接为接口本身做代理。