文章目录
Java中的注解
· 现在使用框架的趋势是使用注解式开发,注解式开发简单高效。但是知己知彼方能百战不殆,了解和熟悉注解能够让我们更加深入地看懂框架以及记住框架的使用方法。本文章对注解进行了一些简单的总结并实现了一个利用注解进行方法自动化测试的小案例。
本节内容涉及一点反射相关的知识,可移步:反射讲解及案例 进行查看。
1. 一句话概括
· 注解和注释有异曲同工之妙。注释告诉程序员:一个类、一个方法或者一个变量有何作用;而注解则告诉计算机:一个类、一个方法或者一个变量需要进行何种操作。举一个最简单不过的例子:大家耳熟能详的@Override注解则是告诉计算机,某个方法重写了该类父类的方法,若计算机无法找到其父类对应的方法,那么IDE在运行时就会报错。
2. JDK内置注解
- @Override:检测被注解标注的方法是否继承至父类
- @Deprecated:表明被该注解标注的内容已过时
- @SuppressWarnings(“all”):压制被该注解标注内容的所有警告(常见的警告有:使用的方法已过时,声明的方法未使用等)
3. 自定义注解
3.1 注解的声明
· Java中是支持自定义注解的,声明方式如下:
public @interface xxx{}
` 这种声明方式的本质是:
public interface xxx extends java.lang.annotation.Annotation{}
· 说明注解的本质其实是一个接口。有接口就会有方法:规定在接口中定义的方法叫做属性。后面可以看到,这些接口的方法本质上是要传值的。我们用@SuppressWarnins(“all”)为例,可以看到,这个注解被人为传入了值,其源码如下:
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
String[] value();
}
· 该注解上面的两个注解我们会在下文提到,现在只需要关注在SuppressWarnings注解里定义的属性,该方法返回一个String数组,如果我们在使用SuppressWarings时给该数组传入特定的值,则IDE在运行时就知道该压制何种类型的警告。不过针对@SuppressWarings,我们只需记住传入“all”来压制所有警告就行。注意,其实在@SuppressWarings(“all”)中,我们的书写方式有所省略,完整的书写格式应该如下:
@SuppressWarings(value = {"all"})
· 原则上,属性名(即方法名)需要在注解声明时写出,如果返回值是数组,需要将值用{}包裹。但是这里有特殊情况,即:属性有且仅有一个,且名为“value”,则属性名可忽略写;如果需要传入数组的值只有一个,则{}也可以省略。
3.2 注解属性的返回值
· 注解属性的返回值类型只可以是以下几种:
- 基本数据类型
- String
- 枚举
- 注解
- 以上类型的数组
· 只有以上的几种类型可以作为注解属性存在,我们自定义注解一般只会使用String,或者干脆不写属性。
3.3 元注解
· 元注解是描述注解的注解,用于注解之上,刚才我们查看的@SuppressWarings注解的源码就可以看到:
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
String[] value();
}
· 此时可以看到在SuppressWarnings上有两个注解,这就是元注解。
· 元注解一般使用四个,分别是:
- @Target:用于描述注解可以作用的位置,其属性值是一个枚举类型Element的数组。一般使用该枚举类型的值有三个:
- ElementType.TYPE:该注解只能作用在类上
- ElementType.METHOD:该注解只能作用在方法上
- ElementType.FIELD:该注解只能作用在成员变量上 - @Retention:用于描述注解被保留的阶段(Source阶段,Class阶段,Runtime阶段,这三个阶段在我之前讲反射的文章中有提及,可以去反射讲解中学习),这个注解的属性值是一个枚举类型RetentionPolicy的数组。其枚举类型的值只有三个,分别是:
- RetentionPolicy.SOURCE
- RetentionPolicy.CLASS
- RetentionPolicy.RUNTIME (我们自定义注解,一般只是用RUNTIME) - @Document:用于描述该注解是否会会被文档给记录。(使用文档注释后,在命令行使用javadoc命令就可以生成代码文档,但是注解默认是不保留在文档里的,如果想要被文档记录,则应加上该注解)
- @Inherited:描述该注解是否会自动被子类继承
3.4 为何要用方法传递值?
· 在接口中,可以声明值,也可以声明方法,声明代码如下:
public interface xxx{
public static final String = "常量";
public abstract String getChangeableValue(){}
}
· 可见,在接口中只能声明常量,如果要传入不同的属性值来降低代码的重复率,只能采用调用方法的形式。因此注解的声明中不允许声明常量。
4. 注解的解析
· 之前我们提到,注解是给计算机“看”的,那么计算机是如何识别注解的呢?该节运用到了一些反射的知识,可以去反射讲解学习反射。
· 一般情况下,我们书写的注解都在RUNTIMME的阶段被解析,而在这个阶段,系统已经得到所有类的字节码Class对象,Class对象能直接从字节码中获得作用在它上面的注解对象,而在Class对象中,有Field,Method和Construct三个封装对象,这个三个对象也都可以在字节码文件中找到作用在它们上方的注解对象。而获取注解的属性值则是通过自动构建该注解的子类对象,并重写注解方法得到的。文字比较晦涩,还是代码看的清楚,下面将给出一个关于注解解析的小案例(通过注解方式构建某个类的对象):
- 首先构建需要操作的类对象
package com.memoforward.domain;
public class Student {
public String name;
public String gender;
private Integer age;
public Student(){
this.name = "江锦平";
this.gender = "男";
this.age = 999;
}
public Student(String name, String gender, Integer age) {
this.name = name;
this.gender = gender;
this.age = age;
}
public void live(){
System.out.println("长生不老+1+1+1....");
}
public void live(String num){
System.out.println("寿命延长:"+num+"s");
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", gender='" + gender + '\'' +
", age=" + age +
'}';
}
}
- 构造注解
package com.memoforward.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD) //表明该注解只能作用在方法上
@Retention(RetentionPolicy.RUNTIME) //表明该注解的作用时间是在RUNTIME阶段
public @interface Prop {
String beanClass();
}
- 写测试代码
@Test
@Prop( beanClass = "com.memoforward.domain.Student")
public void testAnnotationProp() throws Exception {
//目的:获取到注解的值,并利用该值得到实例对象
//第一步:获取该测试类的字节码对象
Class<AnnotationTest> clazz = AnnotationTest.class; //测试类的名称为Annotation
//第二步:从字节码对象中获取到该方法
Method md = clazz.getMethod("testAnnotationProp");
//第三步,从此方法中获取到压在方法上的注解
//此步骤会自动生成该注解的实现类,并重写注解的方法
Prop annotation = md.getAnnotation(Prop.class);
//得到类名
String beanClass = annotation.beanClass();
//创建实例
Student stu = (Student) Class.forName(beanClass).newInstance();
System.out.println(stu);
}
- 测试代码输出,可以看到Student对象已被成功创建,实际上,代码只需稍作修改后,该注解便可以创建任何类的对象,读者有兴趣可以自行操作。
[TestNG] Running:
C:\Users\handsomestar\.IntelliJIdea2019.1\system\temp-testng-customsuite.xml
Student{name='江锦平', gender='男', age=999}
===============================================
Default Suite
Total tests run: 1, Failures: 0, Skips: 0
===============================================
4. 注解的小案例
· 此案例旨在利用注解实现一个自动化测试的程序,目标是如果某个类的方法加上了@Check注解,那么这个程序就可以自动化实现对这个类的测试,并将测试的异常记录在log日志中。本案例使用的日志框架是slf4j+log4j2。
- @Check注解如下
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Check {
}
- 需要测试的类如下:
package com.memoforward.operate;
import com.memoforward.annotation.Check;
public class NeedToCheck {
@Check
public void add(){
int num = 100 + 100;
System.out.println("100 + 100 = " + num);
}
@Check
public void print(){
String str = null;
System.out.println(str.toString());
}
@Check
public void div(){
int num = 100 / 0;
System.out.println("100 - 100 = " + num);
}
@Check
public void useArray(){
int[] a = new int[]{0,1};
System.out.println(a[2]);
}
public void success(){
System.out.println("没有错误...");
}
}
· 上述的类共有5个方法,需要被Check的有4个方法,很显然这4个方法中共会出现三个异常:1. print()方法的空指针异常; 2. div()方法的除0异常;3. useArray()方法的数组越界异常
- 自动化测试代码如下
@Test
public void testAnnoatationCheck(){
//创建需要被测试的类的实例
NeedToCheck ntc = new NeedToCheck();
Class clazz = ntc.getClass();
//得到该类的所有方法
Method[] mds = clazz.getMethods();
int count = 0;
for(Method md : mds){
//检查该方法是否有@Check注解
if(md.isAnnotationPresent(Check.class)){
try{
//如果标注了@Check注解,则检查该方法
md.invoke(ntc);
}catch (Exception e){
//若出现异常则打印异常
logger.error(md.getName() + "出现了异常");
logger.debug("异常名称为:"+ e.getCause().getClass().getSimpleName());
logger.debug("异常原因为:" + e.getCause().getMessage());
logger.debug("-------------------------");
count++;
}
}
}
logger.debug("本次测试出现了"+count+"次异常");
}
· 如果各位不想用日志框架就直接用System.out.println()打印异常也可以,证明这个测试时可用的就行。
- 控制台输出结果如下,可以看到没有被@Check标注的success没有执行。
[TestNG] Running:
C:\Users\handsomestar\.IntelliJIdea2019.1\system\temp-testng-customsuite.xml
100 + 100 = 200
===============================================
Default Suite
Total tests run: 1, Failures: 0, Skips: 0
===============================================
- 日志文件输出如下,所有异常均被捕获。
14:22:39.696 ERROR AnnotationTest 46 testAnnoatationCheck - print出现了异常
14:22:39.703 DEBUG AnnotationTest 47 testAnnoatationCheck - 异常名称为:NullPointerException
14:22:39.703 DEBUG AnnotationTest 48 testAnnoatationCheck - 异常原因为:null
14:22:39.704 DEBUG AnnotationTest 49 testAnnoatationCheck - -------------------------
14:22:39.704 ERROR AnnotationTest 46 testAnnoatationCheck - useArray出现了异常
14:22:39.705 DEBUG AnnotationTest 47 testAnnoatationCheck - 异常名称为:ArrayIndexOutOfBoundsException
14:22:39.705 DEBUG AnnotationTest 48 testAnnoatationCheck - 异常原因为:2
14:22:39.705 DEBUG AnnotationTest 49 testAnnoatationCheck - -------------------------
14:22:39.705 ERROR AnnotationTest 46 testAnnoatationCheck - div出现了异常
14:22:39.705 DEBUG AnnotationTest 47 testAnnoatationCheck - 异常名称为:ArithmeticException
14:22:39.705 DEBUG AnnotationTest 48 testAnnoatationCheck - 异常原因为:/ by zero
14:22:39.706 DEBUG AnnotationTest 49 testAnnoatationCheck - -------------------------
14:22:39.706 DEBUG AnnotationTest 54 testAnnoatationCheck - 本次测试出现了3次异常
总结
· 注解算是Java基础的一个增强,非常简单,但是如果没有理解的话,看源码还是有些许的头疼,不过一旦学会,应该不容易遗忘,希望今后在运用框架的时候能快速熟练掌握注解式开发这一技能。