Java小知识——Junit、反射、注解

前言

有几个知识点,我一直很模糊、很欠缺,现在开始重新学习一下

 

 

 

 

 


Junit

JUnit是一个Java语言的单元测试框架。

测试分类

对于程序的测试,我们一般分成两类:

  • 黑盒测试:不需要写代码,给输入值,看程序是否能够输出期望值
  • 白盒测试:需要写代码的。关注程序具体的执行流程

Junit 的使用

我们的单元测试属于白盒测试。

今天我们来学习Java中的单元测试——JUnit

我们以往对代码的测试是怎么做呢?

一般都是如下吧:

public class Calculator {
    public int add(int a,int b){
        return a+b;
    }
    public int sub(int a,int b) {
        return a - b;
    }
}
public class CalculatTest {
    public static void main(String[] args) {
        Calculator c=new Calculator();
        int result=c.add(3,4);
        System.out.println(result);
    }
}

这样很明显有很多问题,例如我们在main方法对方法进行测试的时候还需要屏蔽其他的方法;main方法只有一个,不方便统一管理,所以我们需要引入单元测试的方法

使用步骤:

  1. 定义一个测试类(测试用例),建议:类型叫做“被测试的类型+Test”;包名:xxx.xxx.test。
  2. 定义测试方法:可以独立运行,建议:方法名叫做“test测试的方法名”;测试方法的返回值一般用void;参数列表建议空参
  3. 给方法加注解@Test
  4. 导入JUnit的依赖环境(java自带就有,也可以网上去找junit.jar包)

 

这里我分享一个JUnit4的jar包:

链接:https://pan.baidu.com/s/1If_y5S16T6P1uXCJwTPb4g
提取码:wun0

 

继续上面的例子,我们创建一个test的包,然后写测试代码:

public class CalculatorTest {
    @Test
    public void testAdd(){
        //1.创建计算器对象
        Calculator c=new Calculator();
        //2.调用add方法
        int result=c.add(3,4);
        System.out.println(result);
    }
}

单机左边的绿色三角,就可以单独执行这个方法了!

注意:junit-4.12的jar包需要同时导入hamcrest-core-1.3.jar,否则会报错

测试中如果程序存在异常,那个绿色的三角形会是红色的,只有是绿色才说明测试成功。

断言

一般来说,我们不会去在单元测试中通过输出来检验,更多使用的是“断言”。

使用断言,可以让单元测试的那个提示三角符号根据情况改变成红色,不是单纯的靠输出结果人脑判断了。

断言Assert非常好用,下面是示例:

public void testAdd(){
    //1.创建计算器对象
    Calculator c=new Calculator();
    //2.调用add方法
    int result=c.add(3,4);
    //断言 断言结果
    Assert.assertEquals(3,result); //参数1:期望;参数2:实际
}

如果不是我们想要的结果,那么就会抛出异常。

 

补充

一般我们的测试类中,是会有个init方法配合junit,init是为junit中用到的资源的初始化申请,需要加上一个@Before的注解。

@Before:修饰的方法会在测试方法之前被自动被执行。

同理还有一个close方法,类似于C++爸爸中的析构函数,是测试类中负责释放资源的方法,在所有测试方法执行完后,都会执行该方法,需要加上一个@After的注释。

@After:修饰的方法会在测试方法执行之后自动被执行。

public class CalculatorTest {
    @Before
    public void init(){

    }
    @After
    public void close(){

    }
    @Test
    public void testAdd(){
        //1.创建计算器对象
        Calculator c=new Calculator();
        //2.调用add方法
        int result=c.add(3,4);
        //断言 断言结果
        Assert.assertEquals(7,result); //参数1:期望;参数2:实际
    }
}

 


反射

什么是反射

反射:框架设计的灵魂

框架,也就是半成品软件,我们开发人员将会在框架(半成品软件)的基础上再次进行软件开发,从而高效率地完成项目,简化编码。

而反射是框架的灵魂,即如果使用框架,我们不需要了解太多反射的知识,但是你要亲自开发一个框架,那么就要好好学习和利用反射。

将类的各个组成部分封装成其他对象,这就是反射机制。

 

Java程序的三个阶段

java代码在计算机中经历的三个阶段:
  1. Source 源代码阶段:.java源文件,包括.class字节码文件存在于硬盘上
  2. Class 类对象阶段:将Source源代码阶段的字节码文件从硬盘上加载到内存中。具体怎么加载呢?通过类加载器( 对应java中的一个对象——ClassLoader)。到了内存中后,会有一个对象来描述这个字节码文件,这个对象是一个Class类的对象,没错,一个叫Class的类,这个类的对象可以读取这个内存中的被加载进来的类的三大属性:成员变量(被封装为Field对象);构造方法(被封装为Constructor对象);成员方法(被封装成Method对象)。最后通过这个类对象的一些行为,我们就可以创建真正的Source源代码中的对象。
  3. 运行时阶段:源代码中的类得到了实例化等操作

是的,Java代码在计算机中的经历就是典型的反射机制,一个类拆开换一种方式表达,最后再拼合还原。

 

反射机制的好处

  1. 可以在程序运行过程中,操作这些对象
  2. 可以解耦,提高程序的可扩展性

我们使用的编译器的自动提示能力一般也是依赖于反射机制,将用户的输入读取到内存中,找到对应的类,整理一下methods然后显示出来(怪不得IDE通常非常占用内存)。

 

获取Class对象的方式

 手动加载

如果我们的程序还在源代码阶段,还没有进入内存,我们可以手动让其进入内存。

Class.forName(“全类名”):将字节码文件加载进内存,返回Class对象

类对象中获取Class对象

我们将源代码阶段的类加载进内存之后,该怎么获取呢?

通过类名来获取:

类名.class:通过类名的属性.class来获取

运行时获取Class对象

在目标对象已经运行起来的情况下,我们可以通过Object类的方法getClass来获取(Object类的方法会被子类继承,而Java中的除了基本数据类型以外的类[包括基本数据类型的封装类,例如int的封装类:Integer]都是继承Object类)

对象.getClass()

示例

一个包:com.atluoluo.domain,内部有一个类Person,是一个JavaBean类,可重用组件,具有两个属性name和age。

一个包:com.atluoluo.reflect,内部有一个类ReflectDemo1,代码如下:

public class ReflectDemo1 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class cls1=Class.forName("com.atluoluo.domain.Person");
        System.out.println(cls1);
        Class cls2= Person.class;
        System.out.println(cls2);
        Person t=new Person();
        Class cls3=t.getClass();
        System.out.println(cls3);

        //比较这几个Class对象的堆内存的地址是否相同
        System.out.println(cls1==cls2); //true
        System.out.println(cls1==cls3); //true
    }
}

如此测试,得出结论:

同一个字节码文件(*.class)在一次程序运行过程中,只会被加载一次,不论通过哪一种方式获取的Class对象,都是同一个

 

三种方式使用场合

  1. Class.forName():这种方式多用于配置文件,读取文件,得到字符串,然后通过字符串加载类。
  2. 类名.class:多用于作为参数的传递
  3. 对象.getClass():多用于对象的获取字节码的方式

 

使用Class对象

获取属性

我们接下来来看一个具体的Class对象使用案例:

首先我们先来明白一下Class对象的功能:

  • 获取成员变量们
    • Field[]   getFields():获取所有public修饰的成员变量
    • Field   getField(String name):获取某个public修饰的成员变量
    • Field[]   getDeclaredFields():获取所有成员变量
    • Field   getDeclaredField(String name):获取某个成员变量
  • 获取构造方法们
    • Constructor<?>[]   getConstructors()
    • Constructor<T>   getConstructor(类<?>… parameterTypes)
    • Constructor<?>[]   getDeclaredConstructors()
    • Constructor<T>    getDeclaredConstructor(类<?>… parameterTypes)
  • 获取成员方法们
    • Method[]  getMethods()
    • Method   getMethod(String name, 类<?>… parameterTypes)
    • Method[]  getDeclaredMethods()
    • Method   getDeclaredMethod(String name, 类<?>… parameterTypes)
  • 获取类名
    • String   getName()

方法使用

  • 获取成员变量——Field:

首先给我们前面那个JavaBean类Person类添加几个成员属性来方便我们做实验:

//修饰符权限由大到小
public String a;
protected String b;
String c;
private String d;

注意ToString方法中要重写一下,要包括上这几个成员。

然后写我们的主要测试代码

public class ReflectDemo1 {
    public static void main(String[] args) throws Exception {
        //获取Person的Class对象
        Class personClass=Person.class;
        //获取成员变量们
        Field[] fields=personClass.getFields();
        for (Field fl:fields ) {
            System.out.println(fl);  //得到public的a
        }
        System.out.println("————————————————————————");
        Field f=personClass.getField("a");
        System.out.println(f);    //同样得到public的a
        //获取成员变量a的值
        Person p=new Person();
        Object value=f.get(p);
        System.out.println(value);
        //设置成员变量a的值
        f.set(p,"张三");
        System.out.println(p);

        System.out.println("————————————————————————");
        //获取所有成员变量(declare:声明)
        Field[] fields2=personClass.getDeclaredFields();
        for (Field fl:fields2 ) {
            System.out.println(fl);  //得到public的a
        }
        System.out.println("————————————————————————");
        Field d=personClass.getDeclaredField("d");
        //设置暴力反射,忽略安全性,允许访问非公有成员
        d.setAccessible(true);
        Object value2=d.get(p);
        System.out.println(value2);
    }
}



输出:
————————————————————————
public java.lang.String com.atluoluo.domain.Person.a
null
Person{name='null', age=0, a='张三', b='null', c='null', d='null'}
————————————————————————
private java.lang.String com.atluoluo.domain.Person.name
private int com.atluoluo.domain.Person.age
public java.lang.String com.atluoluo.domain.Person.a
protected java.lang.String com.atluoluo.domain.Person.b
java.lang.String com.atluoluo.domain.Person.c
private java.lang.String com.atluoluo.domain.Person.d
————————————————————————
null

注意:如果通过getDeclaredField得到一些没有权限操作的成员变量,如果你get或set,IDE是会报错提示你的,你可以设置暴力反射setAccessible来允许对这些成员变量操作。

通过这个例子我们可以深刻感受到,反射机制对于我们的意义,其实就是通过得到类,来对这个类的对象进行处理。

  • 获取构造方法——Constructor
public class ReflectDemo1 {
    public static void main(String[] args) throws Exception {
        //获取Person的Class对象
        Class personClass=Person.class;
        //得到构造方法
        Constructor constructor=personClass.getConstructor(String.class,int.class);
        System.out.println(constructor);
        //利用Constructor的方法创建对象
        Object person=constructor.newInstance("张三",23);
        System.out.println(person);

        System.out.println("——————————————————————————");
        //得到构造方法
        Constructor constructor2=personClass.getConstructor();
        //利用Constructor的方法创建对象
        Object person2=constructor2.newInstance();
        System.out.println(person2);

        System.out.println("——————————————————————————");
        //直接利用Class对象创建空参构造的对象
        Object o=personClass.newInstance();
        System.out.println(o);
    }
}
public com.atluoluo.domain.Person(java.lang.String,int)
Person{name='张三', age=23, a='null', b='null', c='null', d='null'}
——————————————————————————
Person{name='null', age=0, a='null', b='null', c='null', d='null'}
——————————————————————————
Person{name='null', age=0, a='null', b='null', c='null', d='null'}

构造方法主要是要来创建这个类的实例化,得到构造方法,即Contructor的对象,然后调用newInstance方法即可创建对象,如果计划以空参创建对象,可以直接使用Class对象的newInstance方法,而不必再去得到Constructor对象,对于私有构造方法也可以使用getDeclaredConstructor,并且设置暴力反射,这里不做演示了。

  •  获取成员方法——Method

我们给之前那个JavaBean类——Person类添加一个方法:

public void eat(){
    System.out.println("eat...");
}

好拉,回到ReflectDemo.java

public class ReflectDemo1 {
    public static void main(String[] args) throws Exception {
        //获取Person的Class对象
        Class personClass=Person.class;
        //得到方法eat(是一个无参方法)
        Method eat_method=personClass.getMethod("eat");
        System.out.println(eat_method);
        //执行这个方法
        Person p=new Person();
        eat_method.invoke(p);

        System.out.println("-----------------");
        //得到方法eat(有参方法重载)
        Method eat2_method=personClass.getMethod("eat",String.class);
        System.out.println(eat2_method);
        //执行这个方法
        eat2_method.invoke(p,"香蕉");

        System.out.println("-----------------");
        //获取所有public修饰的方法
        // 这里得到的方法除了Person还有Object类的方法
        Method[] methods=personClass.getMethods();
        for (Method m:methods) {
            System.out.println(m);
        }
    }
}
public void com.atluoluo.domain.Person.eat()
eat...
-----------------
public void com.atluoluo.domain.Person.eat(java.lang.String)
eat:香蕉
-----------------
public java.lang.String com.atluoluo.domain.Person.toString()
public java.lang.String com.atluoluo.domain.Person.getName()
public void com.atluoluo.domain.Person.setName(java.lang.String)
public void com.atluoluo.domain.Person.setAge(int)
public int com.atluoluo.domain.Person.getAge()
public void com.atluoluo.domain.Person.eat()
public void com.atluoluo.domain.Person.eat(java.lang.String)
public final void java.lang.Object.wait() throws java.lang.InterruptedException
public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
public boolean java.lang.Object.equals(java.lang.Object)
public native int java.lang.Object.hashCode()
public final native java.lang.Class java.lang.Object.getClass()
public final native void java.lang.Object.notify()
public final native void java.lang.Object.notifyAll()

注意就是得到这个类的方法的时候还有把父类的方法也得到(因为继承关系嘛~),Java中的类都继承Object。

反射的应用案例

学了上面这些,貌似没有体会到反射的牛逼之处,接下来我们来具体做个案例你就会理解很多了。

案例需求:

写一个“框架”,可以帮我们去创建任意类的对象,并且执行其中任意方法

我们在包com.atluoluo.domain中创建一个Studnet类,这个项目还和上面的例子一样,还存在Person类:

public class Student {
    public void sleep(){
        System.out.println("I'm in sleep……");
    }
}

然后在com.atluoluo.reflect包中,写上一个java文件——ReflectDemo.java

如果我们想要创建一个类并且调用其方法,一般都是这样做:

public class ReflectDemo1 {
    public static void main(String[] args) throws Exception {
        Student s=new Student();
        s.sleep();
    }
}

但是这样其实并不能满足框架的需求,所谓框架,是一个软件半成品,我们不允许使用框架的人使用框架的时候还要修改框架的代码,只允许他们去用、他们去增加内容(这里正好对应设计模式中的开放-封闭原则:对增加开放、对修改封闭),如果如上所写,我们创建其他类的对象的时候还要写删删改改,这不能称之为框架

接下来我们来完成框架:

 

我们需要做的是利用两个东西:

  1. 配置文件
  2. 反射

将需要创建的对象的全类名和需要执行的方法定义在配置文件中,在程序中去加载读取配置文件,使用反射技术去加载类文件进内存,并且创建对象,并且执行方法。

接下来我们找个合适的位置(在src目录下)来创建Java的配置文件pro.properties:

后缀properties是一种属性文件。
这种文件以key=value格式存储内容
Java中可以使用Properties类来读取这个文件
String value=p.getProperty(key);
就能得到对应的数据
一般这个文件作为一些参数的存储,代码就可以灵活一点

学英语:properties    所有物; 财产; 财物; 不动产; 

className=com.atluoluo.domain.Person
methodName=eat

ReflectDemo.java的文件的内容如下:

public class ReflectDemo1 {
    public static void main(String[] args) throws Exception {
        //创建properties对象
        Properties pro=new Properties();

        //加载配置文件,转换为一个集合
        /*这里重点解释一下:我们的本类(ReflectDemo1)
        是被ClassLoader类对象加载进入内存的,
        该ClassLoader的对象可以读取指定资源的字节流*/
        ClassLoader classLoader=ReflectDemo1
                .class
                .getClassLoader();
        InputStream is=classLoader.getResourceAsStream("pro.properties");
        //根据字节流读取配置文件
        pro.load(is);

        //获取配置文件中定义的数据
        String className=pro.getProperty("className");
        String methodName=pro.getProperty("methodName");

        //利用反射,加载该类进入内存
        Class cls=Class.forName(className);
        //利用空参构造创建对象
        Object obj=cls.newInstance();
        //获取方法对象
        Method method=cls.getMethod(methodName);
        //执行方法
        method.invoke(obj);
    }
}

这样就可以成功调用配置文件中写的内容了,目前只能实现无参方法的调用,后续再更新吧!

如果我修改pro.properties:

className=com.atluoluo.domain.Student
methodName=sleep

也是可以正常调用的,框架中的代码一行不用改。

 


注解

 概念

什么是注释?用文字描述程序的,给程序员看的说明。

注解和注释非常相似,只不过注释是给程序员看的、注解则是给计算机看的。

 

注解:Annotation,也叫元数据,一种代码级别的说明。他是JDK1.5及以后版本引入的一个特性,与类、接口、枚举是在同一层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明、注释。

使用注解的方式:@+注解名称

 

作用分类

  • 编写文档:通过代码里标识的注解生成文档【生成文档doc文档】

众所周知Java里可以使用工具javadoc来生成文档,我们写一个java文件:

/**
 * @Description TODO
 * @author luoluo
 * @Date 2020/5/19
 * @version 2020/7/3 21:08
 */
public class Annotation {
    /**
     * @author luoluo
     * @Description 计算两个类的和
     * @Date 21:09 2020/7/3
     * @param a 整数
     * @param b 整数
     * @return 两数的和
     **/
    public int add(int a,int b){
        return a+b;
    }
}

然后打开该文件所在目录的命令行窗口,使用javadoc Annotation 命令即可。

这里我遇到了不少问题,主要是编码方面和标签不认识的问题,最后解决后的命令是:

javadoc -encoding UTF-8 -charset UTF-8 
         -tag Description:a:" Description" -tag Date:a:" Date" 
         Annotation.java

最后在index.html中,就是我们最后的类文档。

上面的例子中,我们的@param、@version等都属于注解,告诉计算机这些是编译API文档要读取的信息。

  • 代码分析:通过代码里标识的注解对代码进行分析【使用反射】
  • 编译检查:通过代码里标识的注解让编译器能够实现基本的编译检查(我们常用Override,编译器检查父类方法的重写)

JDK内置注解

Java的软件开发环境包jdk为我们设置好了一些内置注解

  • @Override:检查方法是否完成了对父类/接口同名方法的覆盖,如果没有则会报错
  • @Deprecated: 对不应该再使用的方法添加注解,当编程人员使用这些方法时,它与javadoc里的@deprecated标记相同的功能(不过它不如Javadoc的deprecated,因为它不支持参数),会提示程序员该方法已过时(不过想用还是可以用)
public class Person {
    @Deprecated
    public void eat(){
        System.out.println("eat");
    }
}
public class Annotation {
    public static void main(String[] args) throws Exception {
        Person p=new Person();
        p.eat(); //'eat()' is deprecated,即方法已过时
    }
}
  • @SuppressWarnings:压制警告(学英语——suppress:压抑、封锁),在编译器中,很多时候会有警告说“某类没有被使用”、“某方法没有被使用”,如果我们在上一行写上@SuppressWarning(“all”),那么这个类/方法中所有的警告就会消失。

 

自定义注解

咱们作为 程序员,也可以定义属于自己的注解。

格式

  1. 元注解
  2. public @interface 注解名称{ }

我们可以找到@Override的实现来参观一下

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

本质

我们可以通过反编译的方法来亲自感受一下思考过程:

首先,写一个java文件,写入内容:

public @interface MyAnno {
}

这就是我们的注解@MyAnno,然后我们利用javac命令将其生成对应的字节码文件(.class),然后妙的来了:我们再利用javap命令处理刚才的字节码文件,使其反编译,然后就是这样的内容:

Compiled from "MyAnno.java"
public interface MyAnno extends java.lang.annotation.Annotation {
}

自定义注解的本质:public interface MyAnno extends java.lang.annotation.Annotation{ }。

注释本质上就是一个public的接口(interface),默认继承自Annotation。

属性

注解的属性,也就是接口中可以定义的成员方法(抽象方法)

  • 属性的返回值类型必须是:
    • 基本数据类型(char、boolean、byte、short、int、long、float、double)
    • String
    • 枚举
    • 注解
    • 以上类型的数组

类类型、void都不能作为注解属性的返回值类型

  • 定义了属性,在使用时,需要给属性赋值
public @interface MyAnno {
     String show();
     int show2();
     Person per();
}

例如这样的注解,使用的时候就需要赋值。将接口中定义的方法像是一个字段一样进行了赋值,怪不得我们叫他属性。

@MyAnno(show = "cool",show2 = 3,per = Person.p1)
     public static void main(String[] args) {
}

我们可以使用default关键字来默认赋值

public @interface MyAnno {
    String show();
    int show2() default 3;
    Person per() default Person.p1;
}

在使用的地方只要给没有默认赋值的赋值就好了

    @MyAnno(show = "cool")
    public static void main(String[] args) {
    }
  • 如果只有一个属性需要赋值,并且属性的名称是value,则value可以省略,直接括号里写上值即可(没错,@SuppressWarning(“all”)这玩意底层,那个属性就是叫“value”)
  • 数组赋值会比较特殊,值需要使用大括号包裹,如果数组中只有一个值的话,则大括号可以省略
public @interface MyAnno {
    int[] test();
    int[] test2();
}
    @MyAnno(test ={4,6,3},test2 = 6)
    public static void main(String[] args) {
    }

元注解

元注解是一种用于描述注解的注解,你也可以理解为就是系统提供的用于去创造注解的基本注解。

元注解就是JDK为我们定义的注解,用来让我们去定义属于自己的注解

我们需要掌握的元注解:

  • @Target:描述注解能够作用的位置(类/方法/成员变量)
    • ElementType.TYPE:可以作用于类上
    • ElementType.METHOD:可以作用于方法上
    • ElementType.FIELD:可以作用于成员变量上

示例:@Target(value={ElementType.TYPE,ElementType.METHOD}),该注解只能作用于方法和类

  • @Retention:描述注解被保留的阶段(源代码阶段/Class类对象阶段/RunTime阶段)
    • RetentionPolicy.SOURCE:当前被注释的注解,但不会保留到class字节码文件中
    • RetentionPolicy.CLASS:当前被注释的注解,会保留到class字节码文件中,但不会被JVM(Java虚拟机)读取到。
    • RetentionPolicy.RUNTIME:(常用)当前被注释的注解,会保留到class字节码文件中,并在运行时被JVM(Java虚拟机)读取到。
  • @Decumented:描述注解是否被抽取到API文档中

示例:@Documented,然后生成API文档之后,我们的注解那一行也会被记录在doc文档中。

  • @Inherited:描述注解是否被子类继承

示例:

@Inherited
public @interface MyAnno {
}
@MyAnno
class Parent{
    void method(){

    }
}
class Chirden extends Parent{
    @Override
    void method() {
        super.method();
    }
}

这样我们的Chirden类,也会隐式地含有注解@MyAnno

解析注解(注解的作用)

解析注解主要是要获取注解中定义的属性值

我们来个案例你就明白了,记得前面学习反射的时候,我们使用一个配置文件pro.properties来调整要创建的类的对象等信息,现在我们不需要再用配置文件了,我们使用注解。

创建一个注解叫Pro(名字随意)

然后写上如下内容:

/**
 * 描述需要执行的类名、和方法名
 */
@Target({ElementType.TYPE}) //对类起作用
@Retention(RetentionPolicy.RUNTIME) //保留到运行阶段
public @interface Pro {
    String className();  //属性1
    String methodName();  //属性2
}

然后写一个测试的类Demo:

public class Demo {
    public void show(){
        System.out.println("Demo1 show……");
    }
}

然后我们写主类ReflectDemo:

@Pro(className = "com.atluoluo.domain.Demo",methodName = "show")
public class ReflectDemo{
    public static void main(String[] args) throws Exception {
        //1.解析主类
        //1.1 获取该类的字节码文件对象
        Class<ReflectDemo> reflectDemoClass=ReflectDemo.class;
        //2.获取上边的注解对象
        //其实就是在内存中生成了一个该注解接口的子类实现对象
        Pro an=reflectDemoClass.getAnnotation(Pro.class);
        //3.调用注解对象中定义的抽象方法来获取返回值
        String className= an.className();
        String methodName=an.methodName();

        //4.加载该类进入内存
        Class cls=Class.forName(className);
        //5.创建对象
        Object obj=cls.newInstance();
        //6.获取方法对象
        Method method=cls.getMethod(methodName);
        //7.执行方法
        method.invoke(obj);
    }
}

其中我们将要创建对象的类和要调用方法的方法名写在了注解当中,然后我们首先获取当前类的字节码文件对象Class对象,然后获取Class对象中的注解对象,这里要重点说一下,其底层实际就是在内存中生成了一个该注解接口的子类实现对象,即你的注解,在内存中被自动创建了一个子类,结构大概是这样:

public class ProImpl implements Pro{
    public String className(){
        return "com.atluoluo.domain.Demo";
    }
    public String methodName(){
        return "show";
    }
}

然后创建了一个这个子类的对象,然后调用其内置方法className/methodName获得相关信息,然后就是利用反射去调用即可。

 

我们总结一下:

  1. 获取注解定义的位置的对象(Class、Method、Field)
  2. 获取指定的注解:getAnnotation(Class)
  3. 调用注解中的抽象方法获取配置的属性值

 

简单的测试框架

接下来,咱们来利用自定义注解的能力来写一个简单好用的测试框架,通过个方法添加注解,就可以测试出方法是否存在异常,存在什么异常。

简单的测试框架,当主方法执行后,会自动去执行被检测的所有方法(加了check注解的方法),判断方法是否有异常,然后去记录到文件中。

首先是注解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Check {
}

然后是一个用于测试的类:

public class Calculator {
    @Check
    public void add(){
        System.out.println("1+0="+(1+0));
    }
    @Check
    public void sub(){
        System.out.println("1-0="+(1-0));
    }
    @Check
    public void mul(){
        System.out.println("1*0="+(1*0));
    }
    @Check
    public void div(){
        System.out.println("1/0="+(1/0));
    }
}

然后是主文件:

public class TestCheck {
    public static void main(String[] args) throws IOException {
        //创建计算器对象
        Calculator c=new Calculator();
        //获取字节码文件对象
        Class cls=c.getClass();
        //获取所有的方法
        Method[] methods=cls.getMethods();

        int num=0; //出现异常的次数
        BufferedWriter bw=new BufferedWriter(new FileWriter("bug.txt"));

        //判断方法上是否有check注解
        for(Method method:methods){
            //如果有注解,则执行
            if(method.isAnnotationPresent(Check.class)){
                try{
                    method.invoke(c);
                }catch (Exception e){
                    //捕获异常,记录日志
                    //记录到文件中
                    num++;
                    bw.write(method.getName()+"方法出异常了");
                    bw.newLine(); //换一行
                    bw.write("异常的名称:"+e.getCause().getClass().getSimpleName());
                    bw.newLine(); //换一行
                    bw.write("异常的原因:"+e.getCause().getMessage());
                    bw.newLine(); //换一行
                    bw.write("--------------------");
                    bw.newLine();
                }
            }
        }
        bw.write("本次测试一共出现"+num+"次异常");
        bw.flush();
        bw.close();
    }
}

测试:

总结:

  1. 大多数时候我们会使用注解,而不是自定义注解
  2. 注解给谁用?
    1. 编译器
    2. 给解析程序用
  3. 注解并不是程序的一部分

 


 

 

 

 

 

商业转载 请联系作者获得授权,非商业转载 请标明出处,谢谢

发表评论