官方文档是这么描述注解的:
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:
简单翻译一下:注解是一种元数据,提供和程序相关的数据但不是程序本身的一部分,对他们注解的的代码没有直接影响。主要有几种用途:为编译器提供信息、编译时和部署时处理,运行时处理。
说实话,要第一次接触注解,看到这样的解释,肯定是云里雾里的(天才请忽略),这他丫在说啥?元数据是啥?为什么能提供和程序有关的程序,又不是程序本身的一部分?.....
不如换一个思路,直接把注解当做标签,标签都知道吧,就是描述一种事物的东西,例如图书馆的书都贴有小条,该小条就是标签,小条有不同的颜色,形状,内容,这些就是标签的属性。从这个角度出发,我们“重新定义”注解:注解是一种用来描述程序的标签,对程序本身没有直接影响,换句话说,即使给一本书贴上了标签,也不会对书本身的内容有直接影响,书还是那本书。
接下来我讲介绍一些Java注解相关的内容,包括
注解可以作用在类、方法、字段、接口甚至是注解上(还有其他,后面会列出一个完整的列表),具体取决于注解是如何定义的。假设现在有有个@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) { } } 复制代码
关于如何使用就讲到这吧,下面来介绍一下如何定义注解以及什么是元注解。
注解通过@interface语法定义,如下所示:
public @interface Yeonon { } 复制代码
但光这样定义注解,注解是无法正常工作的,他没有指明该注解可作用的元素类型,也没有指明注解的作用时间范围(即该注解在什么时候是生效的,什么时候是无效的),那如何指明呢?答案是使用 元注解 。
元注解即作用在注解上的注解,用来描述注解。如果把注解看做标签,那么元注解就是描述某个标签的标签,本质上仍然是一个标签,只是他描述的对象是标签,而普通标签描述的是除标签以外的事物。还是有点绕,直接来看代码吧:
@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 。
Retention翻译过来就是保留期的意思,@Retention就是用来描述注解的保留时间的,具体的保留时间根据其value属性来确定,其value属性是RetentionPolicy类型的值,该类型有如下几种取值:
Documented翻译过来就是文档的意思。作用是将注解的元素包含到Java doc中。
Target翻译过来是目标的意思,@Target的作用是指定该注解作用的地方,例如方法、字段、接口。可以有如下取值:
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注解。
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属性的集合中加入这个类型,就表示该注解是一个元注解了,但最好不用过度使用该功能,因为可能会导致一些逻辑混乱。
在上面的代码中,其实已经出现过属性了,例如之前定义的@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等。
下面来介绍一下注解和反射的结合,如果这里对属性还是有些不明白也没关系,下面的介绍会加深对属性的理解。
上面讲了那么多,你是否有一个疑问:程序是如何从这些注解中提取信息的?答案就是结合反射(关于反射,我在之前的文章有说过,在此就直接使用了,不再讲原理),通过反射获取到类、字段、方法上的注解,然后对注解进行解析并作出相应的处理。下面我将通过一个简单的测试框架来演示注解如何和反射结合使用。
首先,先编写一个注解,当某个方法上有该注解时,就表示该方法应该被测试执行,如下所示:
@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%。
一个小型的测试框架就算是完成了,虽然简单粗糙,但作为演示反射和注解的结合已经完全足够了,相信有了上面的介绍,对于如何将反射和注解结合在一起,你已经大概明白了。
本文简单介绍了什么是注解、元注解、注解的使用以及反射和注解结合使用。注解是Java5提供的一个强大的特性,很多框架例如JUnit4、Spring家族的产品例如Spring Boot,Spring Cloud系列都大量的使用注解来简化编程,可见注解的功能是多么强大,而且Java8中还新增了很多和注解有关的东西,从这也可以看出,Java官方也在大量发展注解。