注解 - 图2

概念

  • 注解(Annotation)也可以叫做元数据。是一种代码级别的说明。它是jdk1.5及以后引入的一个特性,与类,接口和枚举是在同一个层次,他可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明,注释。注解不会干扰程序的运行。
  • 上面说注解不会干扰程序的运行,所以如果只是声明了注解程序不会有任何影响,只有解析了注解才可以获取我们想要的信息,解析注解需要使用AnnotationElement 这个接口中的方法
    image.png
  • ClassConstructorField 类都继承了AnnotationElement, 所以可以通过反射来拿到注解上面的信息(反射

    注解的本质

    注解的本质是动态代理,生成代理类

    image.png

    上面的案例中会生成一个该注解的代理类。
    image.png

    注解生效的底层原理

    image.png
    如上图所示,Field, Method,Constructor , Class 这四个对象都实现了 AnnotatedElement 接口。现在我们就来分析他们的使用流程。
  1. 比如像 Spring 这种框架,我们可以将类配置到 XML 或者使用 Spring 提供的注解,但无论哪种方式我们都首先可以获取到一个类的 Class 对象

image.png
通过上图看到 Class 对象可以很轻松的获取到 Field、Method、Constructor 等对象。

  1. 拿到 Class、Field、Method 和 Constructor 等对象之后呢?其实根据上面类图的继承关系可以知道,这几个对象都实现了 AnnotatedElement,看一下这个接口里面都有哪些方法

    1. public interface AnnotatedElement {
    2. default boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {
    3. return getAnnotation(annotationClass) != null;
    4. }
    5. <T extends Annotation> T getAnnotation(Class<T> annotationClass);
    6. Annotation[] getAnnotations();
    7. default <T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass) {
    8. T[] result = getDeclaredAnnotationsByType(annotationClass);
    9. if (result.length == 0 && // Neither directly nor indirectly present
    10. this instanceof Class && // the element is a class
    11. AnnotationType.getInstance(annotationClass).isInherited()) { // Inheritable
    12. Class<?> superClass = ((Class<?>) this).getSuperclass();
    13. if (superClass != null) {
    14. // Determine if the annotation is associated with the
    15. // superclass
    16. result = superClass.getAnnotationsByType(annotationClass);
    17. }
    18. }
    19. return result;
    20. }
    21. default <T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass) {
    22. Objects.requireNonNull(annotationClass);
    23. for (Annotation annotation : getDeclaredAnnotations()) {
    24. if (annotationClass.equals(annotation.annotationType())) {
    25. // More robust to do a dynamic cast at runtime instead
    26. // of compile-time only.
    27. return annotationClass.cast(annotation);
    28. }
    29. }
    30. return null;
    31. }
    32. default <T extends Annotation> T[] getDeclaredAnnotationsByType(Class<T> annotationClass) {
    33. Objects.requireNonNull(annotationClass);
    34. return AnnotationSupport.
    35. getDirectlyAndIndirectlyPresent(Arrays.stream(getDeclaredAnnotations()).
    36. collect(Collectors.toMap(Annotation::annotationType,
    37. Function.identity(),
    38. ((first,second) -> first),
    39. LinkedHashMap::new)),
    40. annotationClass);
    41. }
    42. Annotation[] getDeclaredAnnotations();
    43. }

    可以看到这个接口里面定义的都是一些操作注解的方法,所以那四个对象也是可以直接拿到并操作注解的。

  2. 拿到注解之后呢?很多时候我们在使用注解的使用会有属性值(就是定义的方法),拿到注解不就可以拿到定义的这些值了么,剩下的就是使用这些值去做业务操作了,当然不是所有的注解都有值,有的注解就是一个标记的作用。

    作用分类

编译检查

通过代码里的标识的注解让编译器能够实现基本的编译检查(比如说OVerride注解)

编写文档

通过代码里的标识的注解可以生成文档(doc文档)

案例

比如现在包结构和代码示例如下:
image.png

现在想要生成doc文档到doc目录下,使用如下命令:
image.png

  1. javadoc -sourcepath ./src/main/java -subpackages example -d ./src/doc -encoding UTF-8

上面命令的意思是将源文件位置在./src/main/java(这里使用了相对路径)并且子包example里面的所有内容使用utf-8编码格式生成doc文档并且输出到./src/doc目录中

参数查看

具体参数可以使用 javadoc -help来查看

image-20200225212001443.png

生成中的结果如图所示

image-20200225212401505.png

可以看到和我们平时看到的java api是一样一样的。

代码分析

通过代码里标识的注解对代码进行分析(使用反射)

jdk中预定义的注解

@Override

检测该注解标注的方法是否是继承自父类的方法,作用就是编译检查

@Deprecated

标注的方法代表是已经过时的方法

@SuppressWarning

  • 如果是放在类上就是压制类中所有的警告
  • 如果是放在方法上就是压制该方法上的警告

自定义注解

注解格式

注解格式包含两部分

  • 元注解
  • public @interface 注解名称{}

注解本质上是一个接口,并且该接口继承了Annotation抽象类。

注解的属性

所谓注解的属性也就是接口中的抽象方法

为什么称为属性,请看使用案例:

注解代码:

  1. package example.one;
  2. public @interface AnnoOne {
  3. int age();
  4. String name();
  5. String address() default "中国";
  6. }

使用注解代码

  1. package example.one;
  2. @AnnoOne(age=12, name="张三")
  3. public class TestAnnoOne {
  4. public int addTwoNum (int a, int b) {
  5. return a + b;
  6. }
  7. }

在注解里面定义的方法在使用时方法名就相当于是一个属性一样来使用。所以才称之为是注解的属性。

属性的返回值

  • 基本数据类型
  • String
  • 枚举
  • 注解
  • 以上类型的数组

属性赋值

  • 如果定义属性时,使用default关键字给属性默认初始值,则使用注解时可以不进行属性的赋值
  • 如果只有一个属性需要赋值,并且属性的名称是value,则value可以省略,直接定义值即可
  • 数组赋值时,值使用{}包裹,如果数组中只有一个值,那么{}可以省略不写。

元注解

用于描述注解的注解

  • @Target

    1. @Documented
    2. @Retention(RetentionPolicy.RUNTIME)
    3. @Target(ElementType.ANNOTATION_TYPE)
    4. public @interface Target {
    5. ElementType[] value();
    6. }
  • 描述注解能够作用的位置, ElementType是一个枚举,我们常用的是如下几个值:

    • TYPE: 表示这个注解可以作用到类上面
    • FILED:表示这个注解可以作用到字段上面
    • METHOD:表示这个注解可以作用到方法上
  • @Retention

    1. @Documented
    2. @Retention(RetentionPolicy.RUNTIME)
    3. @Target(ElementType.ANNOTATION_TYPE)
    4. public @interface Retention {
    5. RetentionPolicy value();
    6. }
  • 描述注解被保留的阶段,RetentionPolicy也是一个枚举,里面有三个值:

    • SOURCE:表示在源码阶段就起作用
    • CLASS:表示在字节码阶段起作用
    • RUNTIME:表示在运行时阶段起作用(对于我们自己编写的注解一般都是使用RUNTIME)
  • @Document
    描述注解是否被抽取到api文档中
  • @Inherited
    描述注解是否被子类继承

使用注解

案例一

案例地址

我们使用注解主要是为了获取在注解中定义的属性值,下面有一个案例,给定类名和方法名,通过反射的方式来调用这个方法。

使用properties完成

配置 domethod.properties

  1. className=example.xml.DoReflectXmlMethod
  2. methodName=show

与配置文件对应的类

  1. package example.xml;
  2. public class DoReflectXmlMethod {
  3. public void show () {
  4. System.out.println("这个方法是xml配置参数通过反射来完成的");
  5. }
  6. }

测试类

  1. package example.xml;
  2. import org.junit.Test;
  3. import java.io.IOException;
  4. import java.io.InputStream;
  5. import java.lang.reflect.InvocationTargetException;
  6. import java.lang.reflect.Method;
  7. import java.util.Properties;
  8. public class TestXml {
  9. @Test
  10. public void xmlTest() throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
  11. // 第一步加载properties
  12. //1.1创建Properties对象
  13. Properties pro = new Properties();
  14. //1.2加载配置文件,转换为一个集合
  15. //1.2.1获取class目录下的配置文件
  16. ClassLoader classLoader = TestXml.class.getClassLoader();
  17. InputStream is = classLoader.getResourceAsStream("domethod.properties");
  18. pro.load(is);
  19. String className = pro.getProperty("className");
  20. String methodName = pro.getProperty("methodName");
  21. // 加载该类进内存
  22. Class aClass = Class.forName(className);
  23. // 创建对俩
  24. Object o = aClass.newInstance();
  25. // 获取方法
  26. Method method = aClass.getMethod(methodName);
  27. // 执行方法
  28. method.invoke(o);
  29. }
  30. }

使用注解完成

注解类

  1. package example.anno;
  2. import java.lang.annotation.ElementType;
  3. import java.lang.annotation.Retention;
  4. import java.lang.annotation.RetentionPolicy;
  5. import java.lang.annotation.Target;
  6. @Target(value = {ElementType.TYPE, ElementType.METHOD}) // 可以作用在方法和类上
  7. @Retention(RetentionPolicy.RUNTIME) // 运行时起作用,这个注解不能少
  8. public @interface MyAnno {
  9. String className();
  10. String methodName();
  11. }

与注解对应的类

  1. package example.anno;
  2. @MyAnno(className = "example.anno.DoReflectAnnoMethod", methodName = "show")
  3. public class DoReflectAnnoMethod {
  4. public void show () {
  5. System.out.println("这个方法是anno配置参数通过反射来完成的");
  6. }
  7. }

测试类

  1. package example.anno;
  2. import org.junit.Test;
  3. import java.lang.reflect.InvocationTargetException;
  4. import java.lang.reflect.Method;
  5. public class TestAnno {
  6. @Test
  7. public void annoTest() throws ClassNotFoundException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchMethodException {
  8. // 获取含有注解的那个类的字节码对象
  9. Class<DoReflectAnnoMethod> testAnnoClass = DoReflectAnnoMethod.class;
  10. // 获取注解对象,注解本质上是一个接口,该方法是在内存中有一个该接口的实现类
  11. MyAnno annotation = testAnnoClass.getAnnotation(MyAnno.class);
  12. // 执行接口方法,获取注解属性值
  13. String className = annotation.className();
  14. String methodName = annotation.methodName();
  15. // 加载该类进内存
  16. Class aClass = Class.forName(className);
  17. // 创建对俩
  18. Object o = aClass.newInstance();
  19. // 获取方法
  20. Method method = aClass.getMethod(methodName);
  21. // 执行方法
  22. method.invoke(o);
  23. }
  24. }

案例二

一个简单的测试框架,测试方法的时候可能有很多方法,而且有的方法是不需要测试的,此时可以通过注解来标识那些方法是需要测试的,那些方法是不用测试的。

自定义注解

  1. package example2;
  2. import java.lang.annotation.ElementType;
  3. import java.lang.annotation.Retention;
  4. import java.lang.annotation.RetentionPolicy;
  5. import java.lang.annotation.Target;
  6. @Target(ElementType.METHOD) // 该注解是作用方法上
  7. @Retention(RetentionPolicy.RUNTIME) // 运行时使用
  8. public @interface Check {
  9. }

需要测试的类

  1. package example2;
  2. /**
  3. * 加了check注解的方法表示需要测试的
  4. */
  5. public class Calculator {
  6. @Check
  7. public void add () {
  8. System.out.println("1 + 1 =" + (1 + 1));
  9. }
  10. @Check
  11. public void sub() {
  12. System.out.println("1 - 0 =" + (1 - 0));
  13. }
  14. @Check
  15. public void mul () {
  16. System.out.println("1 * 1 =" + (1 * 1));
  17. }
  18. @Check
  19. public void de () {
  20. System.out.println("1 / 0 =" + (1 / 0));
  21. }
  22. public void noException () {
  23. System.out.println("该方法没有异常不用测试");
  24. }
  25. }

可以看到这个需要类中有很多方法需要测试,区分标准就是方法上如果有@Check注解就表示需要测试

测试类

  1. package example2;
  2. import org.junit.Test;
  3. import java.io.BufferedWriter;
  4. import java.io.FileWriter;
  5. import java.io.IOException;
  6. import java.lang.reflect.Method;
  7. public class TestCalculator {
  8. @Test
  9. public void checkTest() throws IOException {
  10. // 获取对象
  11. Calculator calculator = new Calculator();
  12. // 获取字节码文件
  13. Class<? extends Calculator> aClass = calculator.getClass();
  14. // 获取所有方法
  15. Method[] methods = aClass.getMethods();
  16. int number = 0;//出现异常的次数
  17. // 记录异常的文件
  18. BufferedWriter bw = new BufferedWriter(new FileWriter("bug.txt"));
  19. for (Method method : methods) {
  20. // 判断方法上是否具有check注解
  21. if (method.isAnnotationPresent(Check.class)) {
  22. try {
  23. // 有注解,则要执行该方法进行测试
  24. method.invoke(calculator);
  25. } catch (Exception e) {
  26. // 有异常则记录异常
  27. //记录到文件中
  28. number++;
  29. bw.write(method.getName() + " 方法出异常了");
  30. bw.newLine();
  31. bw.write("异常的名称:" + e.getCause().getClass().getSimpleName());
  32. bw.newLine();
  33. bw.write("异常的原因:" + e.getCause().getMessage());
  34. bw.newLine();
  35. bw.write("--------------------------");
  36. bw.newLine();
  37. }
  38. }
  39. }
  40. bw.write("本次测试一共出现 "+number+" 次异常");
  41. bw.flush();
  42. bw.close();
  43. }
  44. }

上面测试代码其实就是利用反射的只是来获取方法上的注解,如果包含@Check则需要执行测试方法,否则不用执行。