Java SE基础巩固(九):注解

637 阅读11分钟

官方文档是这么描述注解的:

Annotations, a form of metadata, provide data about a program that is not part of the program itself. Annotations have no direct effect on the operation of the code they annotate.

Annotations have a number of uses, among them:

  • Information for the compiler — Annotations can be used by the compiler to detect errors or suppress warnings.
  • Compile-time and deployment-time processing — Software tools can process annotation information to generate code, XML files, and so forth.
  • Runtime processing — Some annotations are available to be examined at runtime.

简单翻译一下:注解是一种元数据,提供和程序相关的数据但不是程序本身的一部分,对他们注解的的代码没有直接影响。主要有几种用途:为编译器提供信息、编译时和部署时处理,运行时处理。

说实话,要第一次接触注解,看到这样的解释,肯定是云里雾里的(天才请忽略),这他丫在说啥?元数据是啥?为什么能提供和程序有关的程序,又不是程序本身的一部分?.....

不如换一个思路,直接把注解当做标签,标签都知道吧,就是描述一种事物的东西,例如图书馆的书都贴有小条,该小条就是标签,小条有不同的颜色,形状,内容,这些就是标签的属性。从这个角度出发,我们“重新定义”注解:注解是一种用来描述程序的标签,对程序本身没有直接影响,换句话说,即使给一本书贴上了标签,也不会对书本身的内容有直接影响,书还是那本书。

接下来我讲介绍一些Java注解相关的内容,包括

  • 如何使用注解
  • 如何自定义注解
  • 元注解
  • 注解和反射结合

1 如何使用注解

注解可以作用在类、方法、字段、接口甚至是注解上(还有其他,后面会列出一个完整的列表),具体取决于注解是如何定义的。假设现在有有个@Yeonon注解,他可以作用类、方法、字段上,那我们可以写出这样的程序:

@Yeonon
public class Main {

    @Yeonon
    private String name;
    
    @Yeonon
    public void testMethod1() {
        System.out.println("test method 1");
    }
    
    public static void main(String[] args) {
        
    }
}

使用起来就是那么简单,如果该注解有属性,还可以对属性进行设置,为编译器或者运行时处理程序提供更多的信息,例如:

@Yeonon(value = "Main")
public class Main {

    @Yeonon(value = "name")
    private String name;

    @Yeonon(value = "testMethod1")
    public void testMethod1() {
        System.out.println("test method 1");
    }


    public static void main(String[] args) {

    }
}

关于如何使用就讲到这吧,下面来介绍一下如何定义注解以及什么是元注解。

2 自定义注解以及元注解

注解通过@interface语法定义,如下所示:

public @interface Yeonon {
}

但光这样定义注解,注解是无法正常工作的,他没有指明该注解可作用的元素类型,也没有指明注解的作用时间范围(即该注解在什么时候是生效的,什么时候是无效的),那如何指明呢?答案是使用元注解

2.1 元注解

元注解即作用在注解上的注解,用来描述注解。如果把注解看做标签,那么元注解就是描述某个标签的标签,本质上仍然是一个标签,只是他描述的对象是标签,而普通标签描述的是除标签以外的事物。还是有点绕,直接来看代码吧:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,ElementType.TYPE, ElementType.METHOD})
public @interface Yeonon {
}

代码中的@Retention和@Target就是所谓的元注解,他们作用在注解上,更准确的说法是作用在注解定义上。之所以这样说,是因为可能会有朋友将下面代码所示的注解当做是元注解:

@Value
@Yeonon
private string name;

这里@Value和@Yeonon都不是元注解,只是两个注解同时作用在一个字段上而已,简单理解就是一本书有两个标签。

Java中内置了5中元注解,分别是: @Retention、@Documented、@Target、@Inherited、@Repeatable 。

2.1.1 @Retention

Retention翻译过来就是保留期的意思,@Retention就是用来描述注解的保留时间的,具体的保留时间根据其value属性来确定,其value属性是RetentionPolicy类型的值,该类型有如下几种取值:

  • RetentionPolicy.SOURCE,保留到源码期,编译的时候会将其忽略。
  • RetentionPolicy.CLASS,保留到编译期,运行时会将其忽略。
  • RetentionPolicy.RUNTIME,保留到运行时,运行时可以获取到。

2.1.2 @Documented

Documented翻译过来就是文档的意思。作用是将注解的元素包含到Java doc中。

2.1.3 @Target

Target翻译过来是目标的意思,@Target的作用是指定该注解作用的地方,例如方法、字段、接口。可以有如下取值:

  • ElementType.TYPE,即作用在类和接口上。
  • ElementType.FIELD,即作用在字段上。
  • ElementType.METHOD,即作用在方法上。
  • ElementType.PARAMETER,即作用在参数上。
  • ElementType.CONSTRUCTOR,即作用在构造器上。
  • ElementType.LOCAL_VARIABLE,即作用在本地变量上。
  • ElementType.ANNOTATION_TYPE,即作用在注解上。
  • ElementType.PACKAGE,即作用在包上,从名字上好像是这样的,但没看到过哪里有这样使用的。
  • ElementType.TYPE_PARAMETER,即作用在类型参数上,Java8新增的。
  • ElementType.TYPE_USE,Java8新增的,没搞懂是什么。

2.1.4 @Inherited

Inherited翻译过来是继承的意思,但并不是指注解可以被继承,而是指的如果一个类被@Inherited注解作用的注解进行注解,那么其子类也会被该注解作用。有点绕,直接来看代码吧:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,ElementType.TYPE, ElementType.METHOD})
@Inherited
public @interface Yeonon {
}

@Yeonon
public class Base {
    //...
}

public class Sub extends Base {
    //...
}

@Yeonon上有@Inherited注解,然后@Yeonon作用到Base类上,而Sub类是Base类的子类,那么Sub类默认就也有@Yeonon注解。

2.1.5 @Repeatable

Repeatable翻译过来是可重复的意思,这是Java8新增的元注解,那这个注解有什么用呢?先来看一个场景,假设一个人既是程序员、又是产品经理(举个例子而已,哈哈)、又是老板,现在有一个@Identity注解,如下所示:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,ElementType.METHOD,ElementType.TYPE})
public @interface Identity {
    String role();
}

可能会这样使用注解来描述一个人:

@Identity(role = "coder")
@Identity(role = "pm")
private User user;

编译一下,发现无法通过编译,错误提示大致是,该注解类型是不可重复的(Java8)。在Java7之前,可能就会定义一个新的可以容纳多个元素的注解来解决这个问题,如下所示:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,ElementType.METHOD,ElementType.TYPE})
public @interface Identites {
    Identity[] value();
}

@Identites(roles = {@Identity(role = "coder"), @Identity(role = "pm")})
private User user;

这样做可以解决问题,但可读性并不好,而且会给注解处理程序带来麻烦。在Java8中,可以使用@Repeatable元注解来表示注解可以重复使用,如下所示:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,ElementType.METHOD,ElementType.TYPE})
@Repeatable(value = Identites.class)
public @interface Identity {
    String role();
}

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,ElementType.METHOD,ElementType.TYPE})
public @interface Identites {
    Identity[] value();
}

//使用
@Identity(role = "coder")
@Identity(role = "pm")
@Identity(role = "boss")
private User user;

可读性好了很多,我们看一眼就知道这个人有三个身份,coder,pm和boss。但仍然需要一个新的注解(例子中的@Identites)来容纳多个元素,这种类型的注解被称作“容器注解”。

除了上述的5个内置的元注解,实际上我们还可以自定义元注解,还记得之前讲@Target注解的时候,ElementType类型有一个ANNOTATION_TYPE属性吗?在@Target的value属性的集合中加入这个类型,就表示该注解是一个元注解了,但最好不用过度使用该功能,因为可能会导致一些逻辑混乱。

2.2 属性

在上面的代码中,其实已经出现过属性了,例如之前定义的@Identity注解,该注解有一个String类型的属性,名字是role,如下所示:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,ElementType.METHOD,ElementType.TYPE})
@Repeatable(value = Identites.class)
public @interface Identity {
    String role();
}

发现这个和声明类的字段有些不一样,比声明字段多了一个括号。这是注解的语法,至于为什么非要这样搞,我也不太明白。属性的类型可以是8中基本类型即其数组类型、引用类型和注解类型,如果声明属性的时候,没有default值,在使用注解的时候就必须给该属性赋值。例如上面的role属性,因为没有默认值,所以在使用的时候必须给出role的值,那默认值该如何定义呢?直接来看代码:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,ElementType.METHOD,ElementType.TYPE})
@Repeatable(value = Identites.class)
public @interface Identity {
    String role();
    
    String name() default "Identity";
}

即在属性声明之后加入default关键字和默认值,非常简单。

那这些属性有什么用呢?可以这样简单的理解:属性提供了额外的信息。举个例子,如果@Identity没有属性(这种注解称作标记注解),当他作用在某个地方的时候,程序包括我们人类都仅仅能知道该注解有一个身份,但不知道具体是什么身份,为了让程序和人类能知道具体是什么身份,就需要用到属性了,例如上述的role属性,此时再使用@Identity的时候,就可以添加role属性的值,用来表示具体的身份,例如coder,pm等。

下面来介绍一下注解和反射的结合,如果这里对属性还是有些不明白也没关系,下面的介绍会加深对属性的理解。

3 注解和反射结合

上面讲了那么多,你是否有一个疑问:程序是如何从这些注解中提取信息的?答案就是结合反射(关于反射,我在之前的文章有说过,在此就直接使用了,不再讲原理),通过反射获取到类、字段、方法上的注解,然后对注解进行解析并作出相应的处理。下面我将通过一个简单的测试框架来演示注解如何和反射结合使用。

首先,先编写一个注解,当某个方法上有该注解时,就表示该方法应该被测试执行,如下所示:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface YTest {
    //暂时不需要属性
}

然后,开始编写测试框架:

public class MyTestTool {

    public static void main(String[] args) throws ClassNotFoundException {
        //获取待测试类的类对象
        Class<?> testClass = Class.forName("top.yeonon.anotation.MyTest");
        int passed = 0; //通过测试的数量
        int tested = 0; //测试的数量

        //获取待测试类所有方法
        Method[] methods = testClass.getDeclaredMethods();

        for (Method method : methods) {
            //如果还方法上有YTest注解,就对其做处理,否则就直接跳过
            if (method.isAnnotationPresent(YTest.class)) {
                tested++;
                try {
                    method.invoke(null);
                    passed++; //能走到这,说明没有发生异常,即测试通过
                } catch (InvocationTargetException e) {
                    System.out.println(method.getName() + " test failed!");
                } catch (Exception e) {
                    System.out.println("Invalid test : " + method.getName());
                }
            }
        }

        System.out.println("tested : " + tested);
        System.out.println("Passed : " + passed);
        System.out.println("Failed : " + (tested - passed));
        System.out.println("Passed Rate : " + ((double)passed / (double)tested));
    }
}

该测试框架非常简单粗暴,最核心的逻辑是通过反射来判断方法上是否有@YTest注解,如果没有,直接跳过该方法,不计入测试总数里,如果有,就调用invoke()方法执行该方法,因为该框架只对静态方法做测试,所以invoke方法的参数是null。如果抛出异常则表示失败(实际上真正的测试框架不会那么简单),如果没有抛出异常则表示成功,最后打印输出一些信息作为结果报告。

最后,编写待测试类,如下所示:

public class MyTest {

    @YTest
    public void m1() {
        //do something
    }

    @YTest
    public void m2() {
        //抛出异常来表示测试失败
        throw new RuntimeException();
    }

    public void m3() {

    }

    @YTest
    public static void m4() {

    }

    @YTest
    public static void m5() {
        throw new RuntimeException();
    }

    public static void m6() {

    }
}

一共有6个方法,3个实例方法,3个静态方法,只有4个方法有@YTest注解,其中有两个方法抛出异常,分别是m2和m5。先来来运行一下之前写的测试框架程序吧,运行结果大致如下所示:

m5 test failed!
Invalid test : m1
Invalid test : m2
tested : 4
Passed : 1
Failed : 3
Passed Rate : 0.25

可以看到,m5测试失败,因为m5抛出了异常,m1和m2则是非法测试,因为m1和m2是实例方法,即使m2内部也抛出了异常,但实际上再执行之前就已经出现了InvocationTargetException,该异常先发生,根本不会调用m2。最后几行表示共有4个测试用例,通过了1个,失败了3个,通过率是25%。

一个小型的测试框架就算是完成了,虽然简单粗糙,但作为演示反射和注解的结合已经完全足够了,相信有了上面的介绍,对于如何将反射和注解结合在一起,你已经大概明白了。

4 小结

本文简单介绍了什么是注解、元注解、注解的使用以及反射和注解结合使用。注解是Java5提供的一个强大的特性,很多框架例如JUnit4、Spring家族的产品例如Spring Boot,Spring Cloud系列都大量的使用注解来简化编程,可见注解的功能是多么强大,而且Java8中还新增了很多和注解有关的东西,从这也可以看出,Java官方也在大量发展注解。