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

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

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:shell

  • 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注解相关的内容,包括ide

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

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) {
        
    }
}
复制代码

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

@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提供的一个强大的特性,不少框架例如JUnit四、Spring家族的产品例如Spring Boot,Spring Cloud系列都大量的使用注解来简化编程,可见注解的功能是多么强大,并且Java8中还新增了不少和注解有关的东西,从这也能够看出,Java官方也在大量发展注解。

相关文章
相关标签/搜索