概念
Java 语言中的类、方法、变量、参数和包等都可以被注解修饰。通过反射获取注解内容。在编译器生成类文件时,注解可以被嵌入到字节码文件中。Java 虚拟机可以保留注解内容,在运行时可以获取到注解内容 。 可以看作是对 一个 类/方法 的一个扩展的模版,每个 类/方法 按照注解类中的规则,来为 类/方法 注解不同的参数,在用到的地方可以得到不同的 类/方法 中注解的各种参数与值 。
从JDK5开始,java增加了对元数据(描述数据属性的信息)的支持。其实说白就是代码里的特殊标志,这些标志可以在编译,类加载,运行时被读取,并执行相应的处理,以便于其他工具补充信息或者进行部署 。《百度百科》
如果说注释是写人看的,那么注解就是写给程序看的,以上概念不理解没关系,接下来结合 Annotation 架构图来一步步分析,到最后再回过头看就会懂的 I promise!
Annotation
Annotation 关键类:
annotation.java
:所有定义的注解默认继承该接口
package java.lang.annotation;
/**
*所有注解类型扩展的公共接口
*/
public interface Annotation {
boolean equals(Object obj);
int hashCode();
String toString();
Class extends Annotation> annotationType();
}
ElementType.java
:定义注解将修饰的对象分类
package java.lang.annotation;
/**
*这个枚举类型的常量提供了一个简单的分类
*元注释,指定在哪里写注释是合法的特定类型
*/
public enum ElementType {
/** 类、接口(包括注释类型)或枚举声明 */
TYPE,
/** 字段声明(包括枚举常量) */
FIELD,
/** 方法声明 */
METHOD,
/** 正式的参数声明 */
PARAMETER,
/** 构造函数声明 */
CONSTRUCTOR,
/** 局部变量声明 */
LOCAL_VARIABLE,
/** 注解类型声明 */
ANNOTATION_TYPE,
/** 包声明 */
PACKAGE,
/** 类型参数声明 @since 1.8版本新增 */
TYPE_PARAMETER,
/** 类型的使用 @since 1.8版本新增 */
TYPE_USE
}
RetentionPolicy.java
:决定注解将会被保留的阶段
package java.lang.annotation;
/**
*注释保留策略。此枚举类型的常量,描述保留注释的各种策略。使用它们
*与{@link Retention}元注释类型一起指定
*注释要保留多长时间。
*/
public enum RetentionPolicy {
/** Annotation 信息保留在源代码阶段. */
SOURCE,
/** 编译器将注解记录保留在类文件中,默认行为 */
CLASS,
/** 编译器将Annotation存储于class文件中,并且在运行时被JVM写入,可以被反射操作读取 */
RUNTIME
}
说明:
- Annotation: 是所有注解类型扩展的公共接口,与ElementType的一个或多个枚举值关联,指定这个注解将作用于Java的那些对象。与RetentionPolicy关联指定唯一的属性来确定该注解将会被保留的阶段。
- ElementType: 是Enum类型,在Annotation上面声明使用时,意味着该注解只能用于指定定义的对象类型。例如:@Target(ElementType.TYPE) 则该 Annotation 只能用来修饰类和接口。
- RetentionPolicy: 也是Enum类型,是用来指定Annotation的策略,可以被保留的阶段,可以理解为作用域。
Annotation 定义
在了解了上面3 个关键类的概念及作用之后,下面来自定义一个 Annotation
package com.angst.student;
import java.lang.annotation.*;
@Documented // 注解内容可以被写入javadoc中
@Inherited // 该注解具有继承性
@Target(ElementType.TYPE) /* 类、接口(包括注释类型)或枚举声明 */
@Retention(RetentionPolicy.RUNTIME) // 作用于程序运行时,方便反射来读取注解内容
public @interface MyAnnotation {
String[] value() default "nothing"; // 给了个默认值
}
说明:
上面定义一个 MyAnnotation 注解 ,发现其定义格式与普通类相似,只是@interface这里的格式会有所区别,那么接下来就来分析一下 @interface 所代表的意思。首先通过javac编译MyAnnotation.java文件生成MyAnnotation.class,然后 javap 反编译来看看它的本来面目
通过反编译可以发现 MyAnnotation 实际就是一个接口,只是继承了 java.lang.annotation.Annotation 接口而已。 Annotation 接口的实现细节都由编译器完成。通过 @interface 定义注解后,该注解不能继承其他的注解或接口 。而定义的 value() 方法,实际就是接口中的一个 abstract 抽象方法。看到这里了是不是觉得Soeasy!
Java内置注解
-
@deprecated
- 所标注内容,不再被建议使用,一般是被新的内容替代
-
@Documented
- 所标注内容,将被写入 javadoc 中
-
@Inherited
- 所标注的Annotation具有继承性,只能被用来标注“Annotation类型”
-
@Override
- 只能标注方法,编译时检测该方法是否覆盖父类中的方法
-
@Retention
- 用来指定Annotation的 RetentionPolicy 属性,只能被用来标注“Annotation类型”
-
@Target
- 用来指定Annotation的 ElementType 属性,只能被用来标注“Annotation类型”
-
@SuppressWarnings
- 所标注内容产生的警告,编译器会对这些警告保持静默,压制警告
Annotation 演示
@SuppressWarnings("all") // 压制 all 警告
public class AnnotationTest {
@Override //检查该方法是否重写父类方法
public String toString() {
return super.toString();
}
@Deprecated // 该方法弃用
public static void show(){
System.out.println("nothing...");
}
public static void main(String[] args) {
AnnotationTest.show(); // 这里调用会有提示
}
}
解析 Annotation
回顾上一遍文章 Java进阶-反射机制 文中的一个案例,是利用反射机制在不改变源代码的情况下,读取配置文件中的任意类并执行指定的任意方法。上次是结合反射机制加载配置文件实现的,这次学习了注解之后,还是这个案例,我们尝试使用反射+注解的方式来实现这个需求。
package com.angst.student;
import java.lang.annotation.*;
/**
* 定义一个模拟配置文件的注解,作用于类和接口,保留在运行时阶段,便于反射读取内容
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface Properties {
String className();
String methodName();
}
package com.angst.student;
import java.lang.reflect.Method;
/**
* 注解解析,获取注解的内容,配合反射加载指定任意类并执行指定任意方法。
* 实现与配置文件同样的功能
*/
@Properties(className = "com.angst.student.Person", methodName = "eat")
public class PropertiesTest {
public static void main(String[] args) throws Exception {
// 1.解析注解就是利用反射在内存中生成了该注解接口的子类实现对象,重写了该接口的抽象方法。
Properties annotation = PropertiesTest.class.getAnnotation(Properties.class);
// 2.调用注解中定义的方法获取返回值
String className = annotation.className();
String methodName = annotation.methodName();
// 3.利用反射来加载指定类并执行指定方法
Class> cls = Class.forName(className);
Object obj = cls.newInstance();
Method method = cls.getMethod(methodName);
method.invoke(obj);
}
}
步骤1
这一行代码需要重点掌握理解,结合上面注解解析的时候可以发现其本身就是个接口,@Properties(className = "com.angst.student.Person", methodName = "eat") 。这里变相就是Properties这个接口实现子类,然后通过类对象中的获取注解对象返回的就是这个注解的实现子类对象,那么步骤2
调用该接口定义的方法来获取返回结果。这么看是不是就能够理解了?
案例: "测试框架"
需求:实现一个类似 “测试框架”的功能,定义一个注解来修饰目标对象方法,检查方法运行是否存在异常,把结果的异常数量、异常内容、异常原因、异常名称等等信息记录到指定文件中。
1.定义 @Check 注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Check {
}
2.定义目标 Calculator 类
import org.testng.annotations.Test;
public class Calculator {
@Check
public void add () {
System.out.println("1+1=" + (1 + 1));
}
@Check
public void sub () {
System.out.println("1-1=" + (1 - 1));
}
@Check
public void mul () {
System.out.println("1*1=" + (1 * 1));
}
@Check
public void div () { System.out.println("1/0=" + (1 / 0)); }
}
3. 定义解析类
import java.io.*;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.Date;
public class CalculatorTest {
private static final Method[] methods;
private static BufferedWriter bufferedWriter;
static {
// 1.获取 Calculator 所有方法
methods = Calculator.class.getMethods();
try {
// 2.创建文件对象记录测试结果
bufferedWriter = new BufferedWriter(new FileWriter("bug.txt"));
} catch (IOException e) {
e.printStackTrace();
}
}
public static void parsingAnnotation() throws IOException {
int fail = 0;int total = 0;
// 3.1 遍历所有方法,判断方法是否使用该注解
for (Method method : methods) {
if (method.isAnnotationPresent(Check.class)) {
total ++;
try {
method.invoke(Calculator.class.newInstance());
} catch (Exception e) {
fail ++;
// 3.1 捕获异常写入文件中
bufferedWriter.write("异常方法: " + method.getName() + "\r\n");
bufferedWriter.write("异常名称:"+ e.getCause().getClass().getSimpleName() + "\r\n");
bufferedWriter.write("异常原因:"+ e.getCause().getMessage() + "\r\n");
bufferedWriter.newLine();
}
}
}
// 3.2 汇总测试异常结果写入文件中
bufferedWriter.write("===============================================" + "\r\n");
bufferedWriter.write("Default Suite:" + "\r\n");
bufferedWriter.write("Total tests run: "+ total +" Failed: "+ fail + " Skips: " + (methods.length - total) + "\r\n");
bufferedWriter.write("Current Date: " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + "\r\n");
bufferedWriter.write("===============================================" + "\r\n");
bufferedWriter.close();
}
public static void main(String[] args) throws IOException {
// 4\. 调用执行验证指定类的异常情况
CalculatorTest.parsingAnnotation();
}
}
4.运行结果记录
异常方法: div
异常名称:ArithmeticException
异常原因:/ by zero
===============================================
Default Suite:
Total tests run: 4 Failed: 1 Skips: 9
Current Date: 2020-12-10 14:18:22
===============================================
在项目根路径下打开bug.txt,会发现结果记录的方式是不是跟 testNg 框架的结果有点类似?
Epilogue
以上详细的讲解了Annotation的整体架构,Java内置的注解及演示,再结合反射+注解实现的一个“测试框架”,我这里打双引号仅仅只是作为演示注解在框架中使用的简单原理,所以不用纠结框架的相关概念。实际上测试框架还是较为复杂,Java目前常用的 JUnit 、testNg等等, 它们在很大程度上借鉴了Java注解来定义测试 。有兴趣了解这些测试框架中是如何结合注解定义测试相关原理,可以去查询相关文档。当然也可以给我留言一起探讨。