[JavaWeb-01]Junit、反射、注解

1、Junit单元测试

1.1 前言

  JUnit是一个开源的java语言的单元测试框架,专门针对java语言设计, 使用最广泛, JUnit是标准的单元测试架构。
  java单元测试是最小的功能单元测试代码, 单元测试就是针对单个java方法的测试。java程序的最小功能单元是方法。
  使用main()方法来进行测试不能把测试代码和源代码分离出来,效率低下。而使用Junit单元测试可以确保单个方法正常运行,如果修改了方法代码,只需要保证对应的单元(方法)测试通过就OK了,同时可以自动化所有的测试并获得报告。
  Junit的特点是可以使用断言(Assertion)测试期望结果,可以方便地组织和运行测试,可以方便地查看测试结果,更可以方便地继承到maven中。

1.2 测试前提

  测试类的使用目录必须是如下, 测试类规定标准是在test目录中进行测试。

├── src
│   ├── main
│   │   └── **.java
│   └── test
│       └── **Test.java

1.3 断言

  断言的使用, 必须先引入必须的包: IDE自动创建的会自动引入。下面用一个例子说明断言地基本使用:
  首先,在src/main中定义一个待测试的计算机类 Calculator.java。

public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }

    public int sub(int a, int b) {
        return a - b;
    }
}

  然后,然后在src/test中定义测试类 CalculatorTest.java。

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import static org.junit.Assert.*;

public class CalculatorTest {

    public CalculatorTest() {
        System.out.println("This is CalculatorTest's Constructor");
    }


    @Before
    public void shows() {
        System.out.println("initing...");
    }

    @After
    public void showe() {
        System.out.println("closing...");
    }

    @Test
    public void add() {
        System.out.println("testing...");
        assertEquals(3, new Calculator().add(1, 2));
    }

    @Test
    public void sub() {
        System.out.println("testing...");
        assertEquals(3, new Calculator().sub(1, 1));
    }
}

  这里用到了几个注解,解释如下:

  1. @Before:在@Test之前运行,用于方法前初始化测试资源;
  2. @After:在@Test之后运行,用于方法后释放测试资源;
  3. @BeforeClass:初始化非常耗时的资源, 例如创建数据库;
  4. @AfterClass:清理@BeforeClass创建的资源, 例如创建数据库

  单个@Test方法执行前会创建新的XxxTest实例, 实例变量的状态不会传递给下一个@Test方法, 单个@Test方法执行前后会执行@Before@After方法。所以,被@Test注解的就是一个测试单元。
  单独测试add()方法,输出如下:

This is CalculatorTest's Constructor
initing...
testing...
closing...

  单独测试sub()方法,输出如下:

This is CalculatorTest's Constructor
initing...
testing...
closing...

java.lang.AssertionError: 
Expected :3
Actual   :0

  可以看出sub()存在问题,这里先说明一下断言的常用方法:

  1. assertEquals(100, x): 断言相等
  2. assertArrayEquals({1, 2, 3}, x): 断言数组相等
  3. assertEquals(3.1416, x, 0.0001): 浮点数组断言相等
  4. assertNull(x): 断言为null
  5. assertTrue(x > 0): 断言为true
  6. assertFalse(x < 0): 断言为false;
  7. assertNotEquals: 断言不相等
  8. assertNotNull: 断言不为null

  代码中的sub()方法用到了 assertEquals(100, x)方法,因为1-1≠3,所以检测到了错误并进行输出。
  这里介绍常用的,其他更详细的使用方法请阅读API。

2、反射

2.1 前言

  在Java中,万事万物皆对象。也就是说,在开发Java代码中,最常见的就是建立对象,然后通过对象进行成员变量设置、访问以及成员方法的调用等。
  然而,在Java中,还有一个更牛逼哄哄的机制,叫做反射,一听到这个词就开始牛逼了,但是这里的“反射”是什么意思呢?首先先说明我个人的理解,就是和“通过类生成对象,然后通过对象去操作”的思想相反,是一种只关心如何使用类的一种机制。举个例子,我想乘坐一种交通工具从广州到北京,那么,先通过飞机类生成飞机对象,然后调用飞机的飞行方法。而反射就是,我不管你坐飞机、坐车还是坐驴,我只按照一种公式来,就是“通过一个类,调用这个类的方法把你载到北京去”。所以,代码关注的是对类的操作,而不是对类的对象的操作。

2.2 类对象和类的对象

  乍一看这两个词好像一个意思,但是要理解Java的反射机制,这两个词是不一样的。“类的对象”很好理解,就是通过一个类的构造函数创造出来的对象。而“类对象”则是“把类当成对象”,也就是类对象了。下面会多处用到这几个词,很绕口,但是要仔细区分理解清楚。

2.3 类类型和类对象

  既然把类当成对象了,那这个类对象又是谁创造出来的呢?没错,有一个叫做类类型的类,叫做Class类,这里的C是大写的。而且每个类的对象都有一个getClass()方法,这个方法就是问这个对象的老子(类)是谁,下面演示一下:

//定义一个Person类
public class Person{
	//类内容
}

//定义一个测试类
public class Demo{
	public static void main(String[] args) {
        Person p = new Person();
        Class c = p.getClass();
        System.out.println(c);
    }
}	

输出如下:

class cn.test.ReflectDemo.Person

  前面的class表示它是一个类对象,后面接着包名.类名

2.4 什么是Class类对象

  Java弄出这么一个机制,把类当成对象,叫做类对象,这个类对象的类型是类类型(很绕口,但是还是不难理解。)到底是干嘛用的呢?其实反射机制大多用在框架上,理解反射对后面学习各种Java框架很有用处。
  首先,我们复习一下Java代码经历的三个阶段:源代码、类对象、运行时阶段。
在这里插入图片描述
  当我们写好xx.java源代码,通过javac就把源代码编译成字节码文件xx.class,JVM通过类加载器ClassLoaderxx.class文件加载成一个个Class类对象,然后通过类对象创建一个个的类的对象。一个类不论加载多少次也只生成一个Class类对象。当然,除了让JVM来生成Class类对象,我们自己也可以生成类对象,有三种方式生成Class类对象,演示如下:

//定义一个Person类
public class Person{
	//类内容
}

//定义一个测试类
public class Demo{
	public static void main(String[] args) throws Exception {
        Person p = new Person("张三",19);

        //第一种,通过对象生成Class类对象
        Class c1 = p.getClass();
        System.out.println("c1 = " + c1);

        //第二种,通过类本身生成Class类对象
        Class c2 = Person.class;
        System.out.println("c2 = " + c2);

        //第三种,通过forName("类的全名")方法生成类对象
        Class c3 = Class.forName("cn.test.ReflectDemo.Person");
        System.out.println("c2 = " + c2);

        //判断三种方法生成的Class对象是否一致
        System.out.println(c1 == c2);
        System.out.println(c1 == c3);
    }
}	

  输出如下:

c1 = class cn.test.ReflectDemo.Person
c2 = class cn.test.ReflectDemo.Person
c2 = class cn.test.ReflectDemo.Person
true
true

  可见,三种方法生成的Class类对象都是一样的。再次强调,类也是一个对象,叫做Class类对象。如果还是不懂什么叫Class类对象,那我说一个“Student类对象”,你就明白什么意思了,即类也是一种对象,是Class类的对象。

2.5 什么是反射

  只要明白了什么是Class类对象,懂得了类本身也是一个对象。现在就好解释什么是反射了。官方定义是:JAVA反射机制是在运行状态中,对于任意一个实体类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。
  现在来看这个定义就不难理解了,所谓反射,就是事先并不知道类和类的内容,要再运行状态中动态获取类的信息。那怎样才叫动态加载一个类呢,想象一下,当我们使用面向对象思想的时候,都是通过一个个的对象去完成一个个任务,那如果我在运行的时候要加一个类或者换掉一个类呢,难不成要程序重新编译?对于项目很大的代码,这是不现实的,比如QQ、微信的升级,都是在线下载一些更新包,而不是重新编译一个软件叫你重新下载软件。下面先通过一个例子演示,然后进一步说明什么是反射,以及如何使用反射。
  项目结构如下:

├── src
│   ├── cn.test.ReflectDemo
│   │           └── Demo01.java
│   │           └── Person.java
│   ├── pro.properties 

   pro.properties 配置文件内容如下:

className=cn.test.ReflectDemo.Person
methodName=sleep
proName=id

   Person.java 文件内容 如下:

public class Person {
    private String name;
    private int age;
    public int id;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public Person() {
    }
    
    //省略set、get方法
    
    public void sleep() {
        System.out.println("Person Sleeping...");
    }
    
	public void eat() {
        System.out.println("Person Eating...");
    }
}

  Demo01.java 文件内容如下:

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Properties;

public class Demo01 {
    public static void main(String[] args) throws Exception {
        //获得类加载器ClassLoader
        ClassLoader classLoader = Demo01.class.getClassLoader();

        //建立属性对象
        Properties proFile = new Properties();

        //获取 pro.properties 输入流并加载到 pro 对象种
        proFile.load(classLoader.getResourceAsStream("pro.properties"));

        //获取 pro.properties 配置文件信息
        String className = proFile.getProperty("className");
        String methodName = proFile.getProperty("methodName");
        String proName = proFile.getProperty("proName");

        //获取 Class 类对象
        Class cls = Class.forName(className);

        //通过 Class 类对象获得 Constructor 类对象
        Constructor cst = cls.getConstructor(String.class, int.class);

        //通过 Class 类对象获得 Method 类对象
        Method m = cls.getMethod(methodName);

        //通过 Class 类对象获得 Field 类对象
        Field pro = cls.getField(proName);

        /**
         * 通过 Constructor 类对象获得 Object 类对象
         *  即 Class 类对象对应什么类,就创建该类对象的对象
         *  此处 Class 类对象是 Person 类,所以创建的是Person类对象
         */
        Object o = cst.newInstance("学哥斌", 18);

        /**
         *  调用 Method 类对象的 invoke 方法
         *  输入值是通过 Constructor 类对象获得的 Object 类对象
         *  根据配置文件,此处输入的是 Person 类对象
         *  即:调用 Person 类对象的 m 方法
         *  此处的 m 方法是 sleep 方法
         */
        m.invoke(o);

        /**
         * 调用 Field 类对象的 get 方法
         * 输入值是通过 Constructor 类对象获得的 Object 类对象
         * 根据配置文件,此处输入的是 Person 类对象
         * 即:访问 Person 类对象的 pro 成员变量
         * 此处的 pro 成员变量是 id
         */
        System.out.println(pro.get(o));

    }
}

  运行后输出如下:

Person Sleeping...
0

  这时候我把配置文件修改成如下:

className=cn.test.ReflectDemo.Person
methodName=eat
proName=id

  运行后输出如下:

Person Eating...
0

  好了,代码演示完了,你会注意到一个地方,就是我完全没有改变源代码,只是修改了配置文件,就改变了输出。这就是反射的,一种只在乎如何使用类=的机制。我的源代码都是在操作如何加载一个类,访问加载到的类的属性并且调用加载到的类的方法,我事先完全不知道加载到的类有什么属性,方法内容是什么,甚至不知道这个类是谁。
  随便百度一下就能看到一句话:“Java反射是框架设计的灵魂”。现在你能体会到这句话的含义吧。我们使用框架时,一千行代码可能就50行你自己写的,剩下950行都是框架生成,框架生成代码除了固定格式,还有就是通过配置文件来加载了。Java的反射机制就是把类的各个组成部分封装为其他对象,然后可以在程序运行过程中操作这些对象。还有一个好处就是可以解耦,无需一更新就重新编译,提高程序的可拓展性。

2.6 反射的总结

  通过上面的一系列讲解,下面来总结一下反射的内容。

2.6.1 获取Class类对象的方式

  1. Class.forName("全类名"):将字节码文件加载进内存,返回Class对象;
  2. 类名.class:通过类名的属性class获取;
  3. 对象.getClass():getClass()方法在Object类中定义着。

  第一种获取方式多用于配置文件,将类名定义在配置文件中,读取文件、加载类;第二种多用于参数的传递;第三种多用于对象的获取字节码的方式。
  注意:同一个字节码文件(*.class)在一次程序运行过程中,只会被加载一次,不论通过哪一种方式获取的Class对象都是同一个。

2.6.2 Class类对象的功能

获取成员变量:
  Field[] getFields():获取所有public修饰的成员变量
  Field getField(String name):获取指定名称的 public修饰的成员变量
  Field[] getDeclaredFields() :获取所有的成员变量,不考虑修饰符
  Field getDeclaredField(String name):获取指定名称的成员变量,不考虑修饰符

获取构造方法:
  Constructor<?>[] getConstructors()
  Constructor<T> getConstructor(Class<?>... parameterTypes)
  Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes)
  Constructor<?>[] getDeclaredConstructors()
  注意,使用了private,记得加一句:AccessibleObject.setAccessible(pro, true);
获取成员方法:
  Method[] getMethods()
  Method getMethod(String name, Class<?>... parameterTypes)
  Method[] getDeclaredMethods()
  Method getDeclaredMethod(String name, Class<?>... parameterTypes)
获取全类名:
  String getName()
  注意,调用getDeclaredxxx方法后,还要调用返回的对象的setAccessible(true)方法,才能执行set和get。

3、注解

3.1 什么是注解

  注释大家都知道,就是代码中给程序员看的,代码编译的时候不会编译进去,更不会执行。那么注释又是啥呢?其实注释也是一种说明作用的,但是这个说明不是给程序员看的,而是给计算机看的。
  注解(Annotation),也叫元数据。一种代码级别的说明。它是JDK1.5及以后版本引入的一个特性,与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明,注释。

3.2 注解的作用

  看了上面对注解的描述,貌似还是云里雾里,这里说一下注解的作用,就能彻底理解什么是注解了。第一节的Junit单元测试还记得吧,那些@Test@Before@After等就叫做注解。程序员要是不看API,谁也不能第一眼就知道这货是干嘛的,所以说明根本不是给程序员看的,而是给计算机看的。注解主要分为三类:自定义注解、JDK内置注解、还有第三方框架提供的注解。
  自定义注解、JDK内置注解、还有第三方框架提供的注解。JDK内置注解,比如@Override检验方法重载,@Deprecated标识方法过期等。第三方框架定义的注解比如SpringMVC的@Controller等。
  实际项目开发重,注解常常出现在类、方法、成员变量、形参位置上。当然还有上面Junit单元测试的注解。

3.3 注解的本质

  注解的定义格式如下:

public @interface 注解名称{
    //属性列表;
}

  可以通过编译+反编译,得到如下:

import java.lang.annotation.Annotation;

public interface 注解名称 extends Annotation{
	//属性列表
}

  可以看出,@interface变成了interface,而且自动继承了Annotation。看得出注解其实本质上是一个接口。但是既然要这种写法,应该是跟接口有些区别,还是按照一种新的东西对待。

3.4 注解的原理

  既然注解是给计算机看的,那么就得有一个第三方程序专门来解析注解。只要用到注解,必然有一种三角关系:定义注解、使用注解、解析注解。

3.4.1 定义注解

  定义注解前先看Java官方是如何定义一个注解的,@Override的源码如下:

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

  可以看到@Override注解上面还有注解,注解的注解称之为元注解,主要有三个:@Documented@Target@Retention
  @Documented用于制作文档,不是很重要,忽略,有需要再自己查API。
  @Target用于限定注解的位置,比如类、成员方法或成员变量等。
  @Retention用于限定注解的保留策略,保留策略有三种:SOURCE\CLASS\RUNTIME,顾名思义,就是说被这个元注解修饰的注解能保留到哪个阶段。
在这里插入图片描述
  但是还注意到一点,元注解貌似有输入值,为了探索这个是怎么定义的,进入@Retention源码查看一下,得到下面源代码:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
    /**
     * Returns the retention policy.
     * @return the retention policy
     */
    RetentionPolicy value();
}

  上面提到过,注解是一个接口,既然是接口,就可以定义抽象方法,而接口的方法修饰符默认是public abstract。其实注解括号里是一个省略写法,@Retention(value=RetentionPolicy.RUNTIME),你可能会懵逼,给方法赋值是什么操作?可能是低层又有什么机制在操作,这里就不管了。看到这么多与接口不太一样的东西,所以还是不要把注解等同于接口,最多类比来看代。
  继续深入源码,可以看出RetentionPolicy是一个枚举:

public enum RetentionPolicy {
    /**
     * Annotations are to be discarded by the compiler.
     */
    SOURCE,

    /**
     * Annotations are to be recorded in the class file by the compiler
     * but need not be retained by the VM at run time.  This is the default
     * behavior.
     */
    CLASS,

    /**
     * Annotations are to be recorded in the class file by the compiler and
     * retained by the VM at run time, so they may be read reflectively.
     *
     * @see java.lang.reflect.AnnotatedElement
     */
    RUNTIME
}

  分析了注解的源码,了解了元注解,我们来有样学样,自定义一个注解:

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnn {
    String getValue() default "default value";
}

  注解使用了RetentionPolicy.RUNTIME,说明这个注解能保留到运行阶段。
  注解里的成员方法一般不叫方法,叫属性。“给属性赋值”总比“给方法赋值”好理解。属性的数据类型有限制。只能是:八种基本数据类型、String、枚举、Class、注解类型以及这几个对应的一维数组。如果注解的属性只有一个,且叫value,那么使用该注解时,可以不用指定属性名,因为默认就是给value赋值。比如上面的@Target(ElementType.ANNOTATION_TYPE)不用看源码我都能猜到只有一个属性,且叫做value。但是注解的属性如果有多个,无论是否叫value,都必须写明属性的对应关系。

3.4.1 使用注解

  定义一个类,来使用自定义的注解:

@MyAnn(getValue = "annotation on class")
public class Student {
    @MyAnn(getValue = "annotation on field")
    public String name;

    @MyAnn(getValue = "annotation on method")
    public void say() {
        System.out.println("hello");
    }

    @MyAnn()
    public void defaultMethod() {
        System.out.println("DefaultMethod...");
    }
}

3.4.2 解析注解

  既然自定义了注解,肯定要写一个程序专门来解析这个注解,否则这个注解跟注释没两样。

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class ParseAnn {
    public static void main(String[] args) throws Exception {
        // 通过类名获取 Class 类对象
        Class StuClass = Student.class;

        // 获取类上面的注解
        Annotation annotationOnClass = StuClass.getAnnotation(MyAnn.class);
        System.out.println(annotationOnClass);

        // 获取成员变量上面的注解
        Field name = StuClass.getField("name");
        MyAnn annotationOnField = name.getAnnotation(MyAnn.class);
        System.out.println(annotationOnField);

        // 获取成员方法上面的注解
        Method say = StuClass.getMethod("say");
        MyAnn annotationOnMethod = say.getAnnotation(MyAnn.class);
        System.out.println(annotationOnMethod);

        // 获取 defaultMethod 方法上面的注解
        Method defaultMethod = StuClass.getMethod("defaultMethod");
        MyAnn annotationOnDefaultMethod = defaultMethod.getAnnotation(MyAnn.class);
        System.out.println(annotationOnDefaultMethod);
    }
}

  输出如下:

@cn.test.Annotation.MyAnn(getValue="annotation on class")
@cn.test.Annotation.MyAnn(getValue="annotation on field")
@cn.test.Annotation.MyAnn(getValue="annotation on method")
@cn.test.Annotation.MyAnn(getValue="default value")

  这里用到了Java的反射机制来获取自定义的注解,毕竟反射就是获取类、接口等的,而注解又类似于接口。这里解析到了注解内容后仅仅只是打印出来,一般的注解解析程序都是对被注解的地方进行特殊处理,比如Junit的@Before@Test@After

3.5 注解的总结

  注解就像是一个标签,给计算机看的,是程序判断执行的依据。比如,注解解析程序读到@Test就知道这个方法是待测试的方法,而@Before的方法就要在测试方法之前执行;
  注解需要三要素:定义、使用和解析。(一般我们都是使用。)
  注解又分为自定义注解、JDK内置注解和第三方注解。我们只要使用就可以,定义和读取都交给别人。
  大多数情况下,我们只负责使用注解,框架会将注解的解析程序隐藏起来,除非阅读源码,否则根本看不到。平时用注解多了,不见定义和读取过程,就会忘了注解是如何作用了。如果你忘了,就收藏这篇,到时候翻出来继续看。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值