单元测试、反射、注解、动态代理
1.单元测试
1.1单元测试概述
单元测试就是针对最小的功能单元编写测试代码,Java程序最小的功能单元是方法,因此,单元测试就是针对Java方法的测试,进而检查方法的正确性。
Junit单元测试框架
JUnit是使用Java语言实现的单元测试框架,它是开源的,Java开发者都应当学习并使用JUnit编写单元测试。
此外,几乎所有的IDE工具都集成了JUnit,这样我们就可以直接在IDE中编写并运行JUnit测试,JUnit目前最新版本是5。
JUnit优点
JUnit可以灵活的选择执行哪些测试方法,可以一键执行全部测试方法。
Junit可以生成全部方法的测试报告,如果测试良好则是绿色;如果测试失败,则是红色。
单元测试中的某个方法测试失败了,不会影响其他测试方法的测试。
1.2单元测试快速入门
需求:使用单元测试进行业务方法预期结果、正确性测试的快速入门。
分析:
① 将JUnit的jar包导入到项目中,IDEA通常整合好了Junit框架,一般不需要导入,如果IDEA没有整合好Junit框架,则在类空白处打出“@Test”并使用快速处理快捷键“Alt + Enter”,在网络中自动下载JUnit的jar包并自动“添加为库”,后可直接使用JUnit相关功能。
② 编写测试方法:该测试方法必须是公共的,无参数无返回值的非静态方法。
③ 在测试方法上使用@Test注解,标注该方法是一个测试方法。
④ 在测试方法中完成被测试方法的预期正确性测试(不是必须的,若某个方法没有返回值,则无需断言,直接调用即可)。
⑤ 选中测试方法,选择“JUnit运行”,如果测试良好则是绿色;如果测试失败,则是红色;如果方法没有出现异常但结果与预期不同,则警告,颜色为黄色。
示例代码如下:
业务类
publicclassUserService { publicStringloginName(StringloginName, StringpassWorld) { if ("admin".equals(loginName) &&"123456".equals(passWorld)) retur"登陆成功"; elseretur"用户名或密码错误"; } publicvoidselectNames() { System.out.println(10/0); System.out.println("查询全部用户名成功!"); } }
测试类
publicclassTestUserService { /*测试方法(每个方法都对应一个单独的测试方法)注:1.测试方法必须是公开的,无参数,无返回值的非静态方法2.测试方法必须使用@Test注解标记3.测试时,测试哪个方法就在哪个方法范围内,右键->run,在类中方法外右键->默认运行测试类中的全部测试方法,在module处右键->RuAllTests 可运行该模块中的全部测试方法*/publicvoidtestLoginName() { UserServiceuserService=newUserService(); Stringrs=userService.loginName("admin", "123456"); // 预期结果正确性测试/*public static void assertEquals(String message, Object expected, Object actual)参数一:断言失败的提示信息参数二:断言返回值参数三:实际返回值*/Assert.assertEquals("loginName业务出现问题", "登陆成功", rs); } publicvoidtestSelectNames() { UserServiceuserService=newUserService(); userService.selectNames(); // selectNames()方法没有返回值,无需断言,直接调用即可 } }
注:测试时,测试哪个方法就在哪个方法范围内,右键->run,在类中方法外右键->默认运行测试类中的全部测试方法,在module处右键->RuAllTests 可运行该模块中的全部测试方法。
Junit常用注解(Junit 4.xxxx版本)
注解(Junit 4.xxxx版本) |
注解(Junit 5.xxxx版本) |
说明 |
@Test |
测试方法 |
|
@Before |
@BeforeEach |
用来修饰实例方法,该方法会在每一个测试方法执行之前执行一次 |
@After |
@AfterEach |
用来修饰实例方法,该方法会在每一个测试方法执行之后执行一次 |
@BeforeClass |
@BeforeAll |
用来静态修饰方法,该方法会在所有测试方法之前只执行一次 |
@AfterClass |
@AfterAll |
用来静态修饰方法,该方法会在所有测试方法之后只执行一次 |
开始执行的方法:初始化资源。
执行完之后的方法:释放资源。
2.反射
反射概述
反射是指对于任何一个Class类,在"运行的时候"都可以直接得到这个类全部成分。
在运行时,可以直接得到这个类的构造器对象:Constructor,成员变量对象:Field,成员方法对象:Method,这种运行时动态获取类的字节码文件对象以及动态调用类中全部成分的能力称为Java语言的反射机制。
反射的关键
反射的第一步都是先得到编译后的Class类对象,然后就可以得到Class的全部成分。
获取Class对象的三种方式
方式一:Class类中的一个静态方法:Class.forName(全限名:包名 + 类名(不加.class/.java))
方式二:类名.class
方式三:对象.getClass() 获取对象对应类的Class对象
反射获取构造器对象
Class类中用于获取构造器的方法
方法名 |
说明 |
Constructor<?>[] getConstructors () |
返回所有构造器对象的数组(只能拿public) |
Constructor<?>[] getDeclaredConstructors () |
返回所有构造器对象的数组,存在就能拿到 |
Constructor<T> getConstructor (Class<?>... parameterTypes) |
返回单个构造器对象(只能拿public的) |
Constructor<T> getDeclaredConstructor (Class<?>... parameterTypes) |
返回单个构造器对象,存在就能拿到 |
使用反射技术获取构造器对象并使用
Constructor类中用于创建对象的方法
方法名 |
说明 |
T newInstance(Object... initargs) |
根据指定的构造器创建对象 |
public void setAccessible(booleaflag) |
设置为true,表示取消访问检查,进行暴力反射(用于打开私有构造器的访问权限) |
使用反射技术获取成员变量对象
Class类中用于获取成员变量的方法
方法名 |
说明 |
Field[] getFields() |
返回所有成员变量对象的数组(只能拿public的) |
Field[] getDeclaredFields() |
返回所有成员变量对象的数组,存在就能拿到 |
Field getField(String name) |
返回单个成员变量对象(只能拿public的) |
Field getDeclaredField(String name) |
返回单个成员变量对象,存在就能拿到 |
使用反射技术获取成员变量对象并使用
Field类中用于取值、赋值的方法
方法名 |
说明 |
void set(Object obj, Object value) |
赋值 |
Object get(Object obj) |
获取值 |
使用反射技术获取方法对象
Class类中用于获取成员方法的方法
方法名 |
说明 |
Method[] getMethods () |
返回所有成员方法对象的数组(只能拿public的) |
Method[] getDeclaredMethods () |
返回所有成员方法对象的数组,存在就能拿到 |
Method getMethod (String name, Class<?>... parameterTypes) |
返回单个成员方法对象(只能拿public的) |
Method getDeclaredMethod (String name, Class<?>... parameterTypes) |
返回单个成员方法对象,存在就能拿到 |
使用反射技术获取方法对象并使用
Method类中用于触发执行的方法
方法名 |
说明 |
Object invoke (Object obj, Object... args) |
运行方法 参数一:用obj对象调用该方法 参数二:调用方法的传递的参数(如果没有就不写) 返回值:方法的返回值 (如果返回值没有就不写,硬写的话,返回null) |
反射的作用-绕过编译阶段为集合添加数据
反射是作用在运行时的技术,此时集合的泛型将不能产生约束(泛型只约束编译阶段集合中的数据类型)了,此时是可以为集合存入其他任意类型的元素的。
泛型只是在编译阶段可以约束集合只能操作某种数据类型,在编译成Class文件进入运行阶段的时候,其真实类型都是ArrayList,泛型相当于被擦除了。
示例代码如下:
publicclassReflectDemo { publicstaticvoidmain(String[] args) throwsExceptio{ // 需求:反射实现泛型擦除后,加入其他类型的元素ArrayList<String>list1=newArrayList<>(); ArrayList<Integer>list2=newArrayList<>(); System.out.println(list1.getClass()); // class java.util.ArrayListSystem.out.println(list2.getClass()); // class java.util.ArrayListSystem.out.println(list1.getClass() ==list2.getClass()); // true 地址相同,说明是同一个Class对象ArrayList<Integer>list3=newArrayList<>(); list3.add(10); list3.add(88); // list3.add("苏轼");Classc=list3.getClass(); Methodadd=c.getDeclaredMethod("add", Object.class); add.invoke(list3, "苏轼"); System.out.println(list3); // [10, 88, 苏轼] } }
反射的作用-通用框架的底层原理
需求:给你任意一个对象,在不清楚对象类型的情况可以,可以把对象的字段名称和对应值存储到文件中去。
分析:
① 定义一个方法,可以接收任意类的对象。
② 每次收到一个对象后,需要解析这个对象的全部成员变量名称。
这个对象可能是任意的,那么怎么样才可以知道这个对象的全部成员变量名称呢?
③ 使用反射获取对象的Class类对象,然后获取全部成员变量信息。
④ 遍历成员变量信息,然后提取本成员变量在该对象中的具体值。
⑤ 存入成员变量名称和值到文件中去即可。
示例代码如下:
保存任意类型的对象工具类
publicclassMyBatisUtil { /*** 保存任意类型的对象** @param obj*/publicstaticvoidsave(Objectobj) { try ( PrintStreamps=newPrintStream(newFileOutputStream("day14_junit_reflect_annotation_proxy_app\\src\\com\\itheima\\d7_reflect_framework\\data.txt", true)); ) { // 1.提取对象的全部成员变量(只有反射可以解决)Classc=obj.getClass(); ps.println("==========="+c.getSimpleName() +"=============="); // c.getSimpleName() 获取当前类名 c.getName() 获取全限名:包名 + 类名// 2.提取全部成员变量Field[] fields=c.getDeclaredFields(); // 3.获取成员变量的信息for (Fieldfield : fields) { field.setAccessible(true); // 暴力破解private类型变量StringfieldName=field.getName(); // 取名称// 取值(提取本成员变量在obj对象中的值)StringfieldValue=field.get(obj) +""; // 字符串拼接,使结果变为String类型(不能使用强转,使用强转时该变量真实类型必须是强转后的类型,否则报错)ps.println(fieldName+"="+fieldValue); } } catch (Exceptioe) { e.printStackTrace(); } } }
测试类
publicclassReflectDemo { publicstaticvoidmain(String[] args) { Students=newStudent("王安石", '男', 50, "1班"); MyBatisUtil.save(s); Teachert=newTeacher("孔丘", '男', 200000); MyBatisUtil.save(t); } }
程序运行结果如下:
data.txt文件内容:
===========Student==============
name=王安石
sex=男
age=50
className=1班
===========Teacher==============
name=孔丘
sex=男
salary=200000.0
注:反射的作用?
可以在运行时得到一个类的全部成分然后操作。
可以破坏封装性。(很突出)。
也可以破坏泛型的约束性。(很突出)。
更重要的用途是适合:做Java高级框架,基本上主流框架都会基于反射设计一些通用技术功能。
3.注解
3.1注解概述
注解概述
注解(Annotation)又称 Java 标注,是 JDK5.0 引入的一种注释机制,Java 语言中的类、构造器、方法、成员变量、参数等都可以被注解进行标注。
注解的作用
对Java中类、方法、成员变量做标记,然后进行特殊处理,至于到底做何种处理由业务需求来决定。例如:JUnit框架中,标记了注解@Test的方法就可以被当成测试方法执行,而没有标记的就不能当成测试方法执行。
3.2自定义注解
自定义注解就是自己做一个注解来使用。
自定义注解格式如下图所示。
示例代码如下:
注解类
public@interfaceMyBook { Stringname(); String[] authors(); doubleprice(); } 测试类(注解的使用位置以及赋初值)name="《活着》", authors="余华", price=50) (publicclassAnnotationDemo1 { }
注:特殊属性:value属性
如果只有一个value属性的情况下,使用value属性的时候可以省略value名称不写,但是如果有多个属性,且多个属性没有默认值,那么value名称是不能省略的(若其他多个属性都有default默认值,则value属性同样可以省略名称不写,直接写值)。
3.3元注解
概念:就是注解注解的注解。
元注解有两个:
@Target:约束自定义注解只能在哪些地方使用。
@Retention:申明注解的生命周期。
@Target中可使用的值定义在ElementType枚举类中,常用值如下:
TYPE:类,接口
FIELD:成员变量
METHOD:成员方法
PARAMETER:方法参数
CONSTRUCTOR:构造器
LOCAL_VARIABLE:局部变量
@Retention中可使用的值定义在RetentionPolicy枚举类中,常用值如下:
SOURCE:注解只作用在源码阶段,生成的字节码文件中不存在
CLASS:注解作用在源码阶段,字节码文件阶段,运行阶段不存在,默认值.
RUNTIME:注解作用在源码阶段,字节码文件阶段,运行阶段(开发常用)
示例代码如下:
注解类
ElementType.METHOD, ElementType.FIELD}) // 元注解 ({RetentionPolicy.RUNTIME) // 一直存在,不会消亡 (public@interfaceMyTest { }
3.4注解解析
注解的操作中经常需要进行解析,注解的解析就是判断是否存在注解,存在注解就解析出内容。
与注解解析相关的接口
Annotation: 注解的顶级接口,注解都是Annotation类型的对象。
AnnotatedElement:该接口定义了与注解解析相关的解析方法。
AnnotatedElement定义的与注解解析相关的解析方法、
方法名 |
说明 |
Annotation[] getDeclaredAnnotations() |
获得当前对象上使用的所有注解, 返回注解数组 |
T getDeclaredAnnotation(Class<T> annotationClass) |
根据注解类型获得对应注解对象 |
boolean isAnnotationPresent(Class<Annotation> annotationClass) |
判断当前对象是否使用了指定的注解, 如果使用了则返回true,否则返回false |
注:所有的类成分Class, Method , Field , Constructor,都实现了AnnotatedElement接口,他们都拥有解析注解的能力。
解析注解的技巧
注解在哪个成分上,我们就先拿哪个成分对象。
注解作用在类上,则要获得该类的Class对象,再来拿上面的注解。
注解作用在成员变量上,则要获得该成员变量对应的Field对象,再来拿上面的注解。
注解作用成员方法上,则要获得该成员方法对应的Method对象,再来拿上面的注解。
示例代码如下:
注解类
ElementType.TYPE, ElementType.METHOD}) ({RetentionPolicy.RUNTIME) (public@interfaceProduct { Stringvalue(); // 产品名doubleprice() default100; String[] manufacturer(); // 生产厂家}
商店类
value="钢笔", price=88, manufacturer= {"得力", "回力"}) (publicclassStore { value="蓝球", manufacturer= {"李宁", "安踏"}) (publicvoidtest(){} }
测试类
publicclassAnnotationDemo3 { // 类对象的注解解析publicvoidparseClass() { // 1.得到类对象Classc=Store.class; // 2.判断这个类上面是否存在这个注解if (c.isAnnotationPresent(Product.class)) { // 3.直接获取该注解对象// Annotation product = c.getDeclaredAnnotation(Product.class);// 要获取Product中的属性等,不能使用多态写法Productproduct= (Product) c.getDeclaredAnnotation(Product.class); System.out.println(product.value()); System.out.println(product.price()); System.out.println(Arrays.toString(product.manufacturer())); } } // 方法对象的注解解析publicvoidparseMethod() throwsException { Classc=Store.class; Methodm=c.getDeclaredMethod("test"); if (m.isAnnotationPresent(Product.class)) { // 3.直接获取该注解对象// Annotation product = c.getDeclaredAnnotation(Product.class);// 要获取Product中的属性等,不能使用多态写法Productproduct=m.getDeclaredAnnotation(Product.class); System.out.println(product.value()); System.out.println(product.price()); System.out.println(Arrays.toString(product.manufacturer())); } } }
3.5案例:模拟Junit框架
需求:定义若干个方法,只要加了MyTest注解,就可以在启动时被触发执行。
分析:
① 定义一个自定义注解MyTest,只能注解方法,存活范围是一直都在。
② 定义若干个方法,只要有@MyTest注解的方法就能在启动时被触发执行,没有这个注解的方法不能执行。
示例代码如下:
注解类MyTest与3.3元注解中的示例代码注解类MyTest相同。
测试类
publicclassAnnotationDemo4 { publicvoidtest1() { System.out.println("===test1==="); } publicvoidtest2() { System.out.println("===test2==="); } publicvoidtest3() { System.out.println("===test3==="); } /*** 启动菜单,有注解的才会被调用** @param args*/publicstaticvoidmain(String[] args) throwsException { AnnotationDemo4a=newAnnotationDemo4(); // 1.获取类对象Classc=AnnotationDemo4.class; // Class c = a.getClass();// 2.提取类对象中的全部成员方法Method[] methods=c.getDeclaredMethods(); // 遍历所有方法,若其有MyTest注解,则执行该方法for (Methodmethod : methods) { if (method.isAnnotationPresent(MyTest.class)) { method.invoke(a); } } } }
程序运行结果如下:
===test1===
===test3===
4.动态代理
代理是什么?
代理是一个对象,用来对被代理对象的行为额外做一些辅助工作。
在java中实现动态代理的要求是什么样的?
必须存在接口。
被代理对象需要实现接口。
使用Proxy提供的方法,获取代理对象。
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
参数一:要生成代理对象的对象的类的类加载器
参数二:要生成代理对象的对象的类实现的接口列表
参数三:代理的核心处理程序(将方法调用分派到的处理程序)
通过代理对象调用方法,执行流程是什么样的?
①先走向代理。
②代理可以为方法额外做一些辅助工作(真正方法执行前后)。
③开始真正触发对象的方法的执行。
④回到代理中,由代理负责返回结果给方法的调用者。
具体执行流程见示例代码。
示例代码如下:
接口类
publicinterfaceSkill { voidsing(); voiddance(); voidrap(); voidplayBasketball(); }
明星类(必须实现接口)
publicclassStarimplementsSkill{ privateStringname; publicStar(Stringname) { this.name=name; } publicvoidsing() { System.out.println(this.name+"唱歌"); } publicvoiddance() { System.out.println(this.name+"跳舞"); } publicvoidrap() { System.out.println(this.name+"rap"); } publicvoidplayBasketball() { System.out.println(this.name+"打篮球"); } }
生成代理对象类
publicclassStarAgentProxy { publicstaticSkillgetProxy(Starobj) { // 为Cxk对象生成一个代理对象/*** public static Object newProxyInstance(ClassLoader loader,* Class<?>[] interfaces,* InvocationHandler h)* 参数一:要生成代理对象的对象的类的类加载器* 参数二:要生成代理对象的对象的类实现的接口列表* 参数三:代理的核心处理程序(将方法调用分派到的处理程序)*/return (Skill) Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), newInvocationHandler() { publicObjectinvoke(Objectproxy, Methodmethod, Object[] args) throwsThrowable { System.out.println("执行方法前的辅助工作"); // Method method:正在调用的method方法对象// Object[] args:method方法对象的参数Objectrs=method.invoke(obj, args); System.out.println("执行方法后的首尾工作"); returnrs; } }); } }
测试类
publicclassTest { publicstaticvoidmain(String[] args) { Stars=newStar("Cxk"); s.sing(); s.dance(); s.rap(); s.playBasketball(); System.out.println("=======代理对象======="); // 为Cxk对象生成一个代理对象(经纪人)Skills2=StarAgentProxy.getProxy(s); s2.sing(); s2.dance(); s2.rap(); s2.playBasketball(); } }
程序运行结果如下:
Cxk唱歌
Cxk跳舞
Cxkrap
Cxk打篮球
=======代理对象=======
执行方法前的辅助工作
Cxk唱歌
执行方法后的收尾工作
执行方法前的辅助工作
Cxk跳舞
执行方法后的收尾工作
执行方法前的辅助工作
Cxkrap
执行方法后的收尾工作
执行方法前的辅助工作
Cxk打篮球
执行方法后的收尾工作
案例-模拟企业业务功能开发,并完成每个功能的性能统计
需求:
①模拟某企业用户管理业务,需包含用户登录,用户删除,用户查询功能,并要统计每个功能的耗时。
②使用动态代理的方式解决。
分析:
① 定义一个UserService表示用户业务接口,规定必须完成用户登录,用户删除,用户查询功能。
② 定义一个实现类UserServiceImpl实现UserService,并完成相关功能,且统计每个功能的耗时。
③ 定义测试类,创建实现类对象,调用方法。
关键步骤
①必须有接口,实现类要实现接口(代理通常是基于接口实现的)。
②创建一个实现类的对象,该对象为业务对象,紧接着为业务对象做一个代理对象。
本案例业务执行流程图如下图所示。
示例代码如下:
接口类
publicinterfaceUserService { Stringlogin(StringloginName, Stringpassword); voiddeleteUsers(); StringselectUsers(); }
接口实现类
publicclassUserServiceImplimplementsUserService { publicStringlogin(StringloginName, Stringpassword) { try { Thread.sleep(1000); } catch (InterruptedExceptione) { e.printStackTrace(); } if ("admin".equals(loginName) &&"123456".equals(password)) return"登陆成功!"; return"登陆失败!"; } publicvoiddeleteUsers() { try { Thread.sleep(2500); } catch (InterruptedExceptione) { e.printStackTrace(); } System.out.println("删除了1000条用户数据"); } publicStringselectUsers() { try { Thread.sleep(1300); } catch (InterruptedExceptione) { e.printStackTrace(); } Stringrs="查询了500条数据"; returnrs; } }
生成代理对象工具类
publicclassProxyUtil { // 使用泛型,<T>代表泛型,可以传入任意类型的对象publicstatic<T>TgetProxy(Tobj) { return (T) Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), newInvocationHandler() { publicObjectinvoke(Objectproxy, Methodmethod, Object[] args) throwsThrowable { longstartTime=System.currentTimeMillis(); Objectrs=method.invoke(obj, args); // 真正触发被代理对象的行为longendTime=System.currentTimeMillis(); System.out.println(method.getName() +"方法耗时:"+ (endTime-startTime) /1000.0+"s"); returnrs; } }); } }
测试类
publicclassTest { publicstaticvoidmain(String[] args) { // 掌握使用动态代理解决问题,理解使用动态代理的优势UserServiceuserService=ProxyUtil.getProxy(newUserServiceImpl()); // 声明对象时,实现类对象可以向上转型为接口(多态)System.out.println(userService.login("admin", "123456")); System.out.println(userService.selectUsers()); userService.deleteUsers(); } }
程序运行结果如下:
login方法耗时:1.005s
登陆成功!
selectUsers方法耗时:1.305s
查询了500条数据
删除了1000条用户数据
deleteUsers方法耗时:2.508s
动态代理的优点
非常的灵活,支持任意接口类型(使用泛型<T>)的实现类对象做代理,也可以直接为接本身做代理。
可以为被代理对象的所有方法做代理。
可以在不改变方法源码的情况下,实现对方法功能的增强。
不仅简化了编程工作、提高了软件系统的可扩展性,同时也提高了开发效率。