死磕,Java注解(Annotation ),一文全懂

疯狂创客圈:如果说Java是一个武林,这里的聚集一群武痴, 打磨着九阳神功和独孤九剑,欢迎砸场子
QQ群链接:
疯狂创客圈QQ群

什么是JAVA注解呢?

顾名思义,注解就是标注。具体的说,是对一些需要特别处理的类名、方法名、参数名称等,加上一些预先定义好的标注。
举个栗子,在JDK1.8的线程类java.lang.Thread中,线程的停止方法,就加上了一个标注:

@Deprecated
public final void stop() {
    SecurityManager security = System.getSecurityManager();
...................

上面的标注 @Deprecated,加在了方法名称的前面,其意思,我们大家都是知道的。表示此方法已经过时,如果需要停止线程,大家尽量不要使用stop方法去停止。

 
特别说明一下,注解的英文名称是Annontation,在Java5引入,中文名称叫注解。

注解的一个显著特征是:标注的前头,多了一个@符号。这个是注解与其他的JAVA 中的类名、方法名、变量名等的显著区别。

顺便说注解与注释。

就一字之差,差之毫厘,失之千里。注释是为了提升代码可读性,在代码中加入的文字说明。而注解,则是作为Java的一种编程元素,作为可以执行的一部分。

 
 使用场景

首先说一下注解的使用位置,注解可以应用于包、类型、构造方法、方法、成员变量、参数及本地变量的声明语句中。从这个角度来说, 注解像一种修饰符,比如说public,只不过,可以在一个位置使用很多个不同的注解。
然后,为大家梳理一下注解的使用场景:


语法检查

举个栗子:比如在重写某个父类的方法时,可以使用一个注解来检查,是否重写正确。这个注解则是:@Override。 如果方法是重写成功,则JVM不报错,否则,如何重写错误,JVM会报错。
很多情况下,大家会重写一个众所周知的JAVA根类的方法——toString,在这种情况下,大家不要忘记,在前面可以加上@Override标记,进行重写的检查,具体如下:

@Override
public String toString() {
    return "宠物{" +
            "名称=" + getName() +
            ", 年龄=" + getAge() +
            '}';
}

上面的方法重写,是成功的。原因是,方法的声明和Object类里边的toString方法是一模一样的。
但如果不小心,加上了一点其他的呢?
请看下面的这个版本:

@Override
public String toString(int type) {
    return "宠物{" +
            "名称=" + name +
            ", 年龄=" + age +
            '}';
}

这个版本的方法重写,是失败的。其方法声明比Object类里边的toString方法,多了一个参数。
托 @Override注解 的洪福,JVM会进行语法的检查,这个版本的重写,会进行报错的提示。在开发时,IDE工具也能检查出来,并且告警。

通过上面一正一反两个例子的对比,可以知道:@Override注解的作用,还是非常巨大的。
语法检查注解,远远不止@Override这一个。后面小节,还要讲到其他的几种。
  

文档元素标注


注解使用的第二个场景是,是对程序进行文档元素的标注。很多的程序,需要给其他的程序进行调用,API文档是提供到调用者的至关重要的参考手册,往往需要和程序进行同步交付,在更新时进行同步更新。
对于Java程序生成文档,一条简单javadoc指令,就可以搞定全部。当然还有一些更先进的方式。
这里不去赘述如何生成文档,而是说明一下,其中需要用到的java 注解。主要有: @param   @return
举一个栗子:
 

/**
 * @param p1 此处为p1参数说明
 * @param p2 此处为p2参数说明
 * @param p3 此处为p3参数说明
 * @return   此处为返回值说明
 */
public String toString(int p1,char p2, boolean p3) {
    return "宠物{" +
            "名称=" + name +
            ", 年龄=" + age +
            '}';
}

使用javadoc指令生成API的文档, 对应的说明如下:

对参数进行说明,需要使用@param注解。在该注解的后面,首先指定指定对应的参数,然后指定说明的内容。
对函数返回值做说明,需要用到@return 注解。直接指定说明的内容。

属性直接赋值

通过注解,可以为对象的属性直接赋值。
举一个在比较有实用价值,帮助大家提升效率的小栗子。
在实际的开发中,有非常多的配置信息,写在.property/.yml配置文件中。比如下面的配置文件,配置了socket服务器的IP、端口等等。
下面是一个在其他栗子中用到的配置文件:

socket.server.ip=127.0.0.1
socket.server.port=18899
socket.send.file=D:/疯狂创客圈 死磕java/testData/nio传输测试视频.mp4
socket.recieve.path=D:/疯狂创客圈 死磕java/testData/socket/
send.buffer.size=1024
server.buffer.size=10240


file.resource.src.path=/system.properties
file.resource.dest.path=/system.copy.properties
file.src.path=D:/疯狂创客圈 死磕java/testData/nio传输测试视频.mp4
file.dest.path=D:/疯狂创客圈 死磕java/testData/file.copy.nio传输测试视频.mp4

如何读取这个配置文件中的配置项的值呢?
有很多的方法,网上都有。
这里演示一种在使用上最为简单和直观的方法——通过注解自动为属性赋值。

属性直接赋值,到底长成啥样子呢?
直接上干货,代码如下:

@ConfigFileAnno(file = "/system.properties")
public class SystemConfig extends ConfigProperties {


    //服务器ip
    @ConfigFieldAnno(proterty= "socket.server.ip")
    public static String SOCKET_SERVER_IP;

    //服务器地址
    @ConfigFieldAnno(proterty= "socket.server.port")
    public static int SOCKET_SERVER_PORT;

.......

}

首先,介绍这段代码的作用,然后慢慢讲解内部的原理。
先来说说上面的代码的两个属性,一个是服务器ip,一个是服务器端口。它们的前面,都有一个@ConfigFieldAnno注解,其作用是取得配置项的值,给变量赋值。
其一,对于 SOCKET_SERVER_IP 属性,所赋之值,为配置文件中的socket.server.ip配置项的值,也就是 127.0.0.1 。
其二,对于SOCKET_SERVER_PORT 属性,所赋之值,为配置文件中的socket.server.port配置项的值,也就是 18899端口 。

先不用急于理解底层的原理如何。通过例子可以看到——通过注解,简单的完成了赋值的功能,并且非常的方便!非常的直观!

其次,再来说说所用到的两个注解。
其一,@ConfigFieldAnno注解。此注解的价值,就是将system.properties配置文件中的某个配置项,与SystemConfig 类中的某个属性,建立直观的对应关系。这个一个属性级别的注解,标记在属性的前面。


其二,@ConfigFileAnno注解。如果仅仅凭着一个@ConfigFieldAnno注解,还是不足以单独完成这个任务的。必须在依赖另外一个注解:@ConfigFileAnno。该注解的作用是:将SystemConfig 类与system.properties文件,对应起来。这个算是类级别的注解,需要标记在类的前面。

两个注解,名字比较类似,后一个注解中间的单词是file,前一个注解的中间的单词是field ,两个的级别完全不同,也算是——差之毫厘失之千里吧。

这个两个注解,JDK 没有提供,需要我们自己实现。实现的方法并不复杂, 看完此文,就知道了。

其他的场景

上面仅仅列了三种非常简单的注解使用场景。实际上,注解的使用已经非常普遍。 学会使用注解,已经是一项非常重要的基本功了。


 2.2Java预置注解


JAVA语言中,已经预先定义好了几个使用于语法检查场景的注解,也叫预置注解,常用的有如下几个:
 @Override 、@Deprecated、@SuppressWarnings、@FunctionalInterface。
   


2.2.1.@Override

@Override标记,主要是进行重写的检查。检查子类的重写方法,是否与父类的方法声明一致。

如果不是重写父类的方法,而是子类自身的扩展方法,加上@Override标记,对不起,JVM编译器也会报错。

2.2.2.@Deprecated

       程序的开发的过程,是一个不断修改和完善优化的过程。一些的方法,经过几轮迭代,就会发现了一些潜在的问题和风险,不建议再继续调用了。
      可能有的兄弟要问了,不能再继续调用了,为什么又不直接去掉呢?
      没有那么简单!
      还是用前面提到的Thread的stop方法来举例,众所周知的原因,这个方法可能破坏数据一致性,在一定程度上臭名昭著。
但是,是否能从Thread类中去掉stop方法呢? 答案是:不能。
      理由很简单:stop方法发生异常,并不是普遍情况,只是在非常少的细分场景下。谁又知道,有多少程序,之前或者现在还在调用这个stop方法呢?如果强行去掉了,会导致那些程序直接不可用或者需要修改。
       既然不能去掉,JAVA采用了一种折中的办法:给其加上标记,告诉大家,这个方法有风险,不建议使用。这个标记就是:@Deprecated注解。
     再举个小小的模拟栗子:
    在LittleDog类中,有两个报年龄的方法,一个已经过时,加上了@Deprecated注解,具体如下。

@Deprecated
public LittleDog sayAge() {
    Logger.info("我是" + name + ",我的年龄是:" + age);
    return this;
}   

public LittleDog sayPetAge() {
    Logger.info(",我的年龄是:" + age);
    return this;
}

在开发阶段,过时方法就能被很容易识别出来。编译器在会对这类过时方法,进行特别的标识。比方说,下面的sayAge方法,被标了一条中划线。

在编译阶段,使用过时方法也会被警告。编译器只要遇到这个@Deprecated注解,发出提醒警告,告诉开发者正在调用一个过期的方法。这个时刻,会有过时的编译信息输出,具体如下:

奇妙的是,世界的规律往往总是这样:一物降一物。
如果已经知道过期方法的风险和问题,并且已经排除掉了所有的风险,不需要编译器担这份儿心,报出一堆的告警干扰视线,看上去就心烦,怎么办呢?
有办法,去看一下个预置注解吧!

2.2.3.@SuppressWarnings

在使用过期方法时,如果确定没有风险,需要让编译器解除警报,可以用到另外的一个注解:@SuppressWarnings 注解。此注解时抑制警告的意思。
使用时,需要带上一个参数,表示抑制的目标警告类型。比如,如果需要抑制上面的过时警告,加上的参数是deprecation。具体如下:

@SuppressWarnings("deprecation")
public static void main(String[] args) {
    LittleDog dog=new LittleDog();
    dog.sayAge();
}


加上此注解后,编译器编译时,不再有过时的信息输出。开发环境IDE也不再进行中划线提升。


2.2.4.@FunctionalInterface

这个是JAVA 8引入的新注解,进行"函数式接口"的语法检查。放置在接口定义的前面,用来检查是否符合"函数式接口"的规则。
"函数式接口"的规则是:一个接口仅仅包含一个方法。不能有一个以上的方法,两个都不行。

Java中,常用的一些接口Callable、Runnable、Comparator等在JDK8中都添加了@FunctionalInterface注解。

@FunctionalInterface
public interface Runnable {
 
    public abstract void run();
}


再举个栗子。
创建一个简单的宠物接口,作为一个函数式接口,只包含一个方法——报年龄。代码如下:

package com.crazymakercircle.annoDemo;
@FunctionalInterface
public interface Pet {
    public LittleDog sayPetAge() ;
}

接口的前面加上了@FunctionalInterface 注解,JVM编译器编译正常,说明接口是符合"函数式接口"规则的。
那么,如果在接口中再加上一个方法呢?

package com.crazymakercircle.annoDemo;
@FunctionalInterface
public interface Pet {
    public LittleDog sayPetAge() ;
    public LittleDog sayHello();
}

这下严重了!
JVM编译器标红提示错误,编译成.class时不能通过。原因是:Pet包含多个抽象方法,不通过"函数式接口"语法检查。

特别说明:本小节所指的接口中的方法,在范围,仅仅限于普通的抽象方法,不包括定义在接口中的静态方法,也不包括在接口中定义的默认方法。静态方法和默认方法,不属于"函数式接口"规则约束的范围之内。
下面的修改,保证了语法上是对的。将另一个方法改成静态方法,保证只有一个抽象方法。

package com.crazymakercircle.annoDemo;
@FunctionalInterface
public interface Pet {
    public LittleDog sayPetAge() ;
   public static LittleDog sayHello(){
       System.out.println("hi,i am a lovely pet");
       return new LittleDog();
   }

}


讲到这里,讲了很多的注解,都是语法检查类的注解,并且都是java 的预置注解。
既然注解有这么大的作用,那么如何定义一个属于我们自己的注解呢?


2.3. 自定义注解


方法其实很简单。

2.3.1.两个注解实例


在前面讲给属性自动赋值的例子时,已经秀出来两个自定义的注解,它们是:@ConfigFieldAnno 、@ConfigFileAnno。
这个两个注解是如何定义的呢?
先来看建立属性级别对应关系的自定义注解@ConfigFieldAnno,代码如下:

public @interface ConfigFieldAnno {
      String proterty() ;
    }

再来看建立文件级别对应关系的自定义注解@ConfigFileAnno,代码如下:

public @interface ConfigFileAnno {
     String file() ;
    }

有没有感觉到一个词:简单。
是的:注解的定义,其实就是那么简单。
像极了接口的定义:和定义接口的语法,基本上是一模一样。连关键词,就就差了那么一点点。

2.3.2.关键词@interface


定义注解的关键词是 @interface。
整个关键词,与定义接口的关键词interface 相比,多了一个 @ 符号。


2.3.3.标记与对象

这里为了方便陈述,将注解的一次使用,解释为一个做一次标记。

 

在JAVA内部,每一个注解对应于一个接口,每一个标记对应于的一个实例对象,其类型是之前定义的注解类型。

注解与接口的对应关系如下:

一个自定义注解,在定义完成之后,如果要达成最初的目标,一般需要做另外的两步工作:第一步是标记应用,第二步是标记解析。


第一步很简单,以@ConfigFieldAnno注解为例,放在属性前面,作为属性的一个标记即可:

    //服务器ip
    @ConfigFieldAnno(proterty= "socket.server.ip")
    public static String SOCKET_SERVER_IP;

    //服务器地址
    @ConfigFieldAnno(proterty= "socket.server.port")
    public static int SOCKET_SERVER_PORT;

仅仅是加上标记,是不会给属性自动赋值的。这就需要第二步,需要对标记进行解析,完后幕后的真正的赋值工作。
第二步的工作——标记的解析。第一步相比,第二步复杂一些。第二步需要取得标记中设置的内容,这里暂时按下不表。稍后再做分析。


2.3.4.成员方法

讲清楚了注解和接口的对应关系,再来看一个比较重要的概念:成员方法。
定义注解时,也可以定义成员方法。
举个栗子。 举一个定义了成员方法的注解,如下:

public @interface Role {
    String name ();
}

这个注解在业务逻辑上,用于标记学生的角色(班长、课代表、小组长、普通学生等)。这个注解中定义了一个成员方法name,表示角色的名称。
参照接口的抽象方法,说明一下注解中方法的规则。
与接口的抽象方法相比,注解的成员方法有一点相同的地方,就是没有方法体(method body),只有方法的声明。
与接口的抽象方法相比,注解的成员方法不同的地方是:
一是注解的成员方法不能有形参,必须是无参的函数;
二是注解的成员方法可以加default, 设置默认的返回值;

public @interface Role {
    String name () default "普通学生";
}

三是注解的成员方法,在标记的阶段,可以当做属性使用,使用name=“值”的方式使用。

上面的Role注解的name方法,在标记阶段使用的方法为:

@Role(name = "普通学生")
public class Student {
}

如果注解不止一个成员方法,如下所示:

public @interface Role {
    String name () default "普通学生";
    int id();
}

在标记阶段,成员方法当做属性使用时,不同的属性之间,使用逗号隔开。如下是标记阶段的使用方法:

@Role(name = "普通学生",id=1)

public class Student {
}

有一个特殊的小场景:如果一个注解内,仅仅定义了一个成员方法,并且名字为 value 。则,在标记应用阶段,可以直接将属性值填写到括号内,不需要写明属性名称。

public @interface Role {
    String value () default "普通学生";
}

在标记阶段使用时,可以是这样的:

@Role("普通学生")

public class Student {
}

最后,如果注解没有成员方法,在标记应用阶段,可以省略后面的括号。


2.3.5.成员的两面性


从这一点来说,注解的成员方法,仿佛有一种变身的魔力。这个魔力就是:注解的成员方法就像煎鸡蛋,具有两面性,一面是属性,一面是方法。

前面反复讲到,一个自定义注解,在定义完成之后,如果要达成最初的目标,一般需要做另外的两步工作:第一步是标记应用,第二步是标记解析。

在第一步标记应用时,注解的成员方法,被当成了一个属性。


这一点看上去有点异常。明明是一个方法,怎么就换了个马甲?
当然可以确定的是,每一个标记,最后都会实例化成一个JAVA 对象。所设置的值,会保存在对象的内存中。在标记解析的阶段,可以取得这些成员属性的值。

在标记解析的阶段,注解的成员方法,终于可以不要马甲,直接上场了。
在标记解析的阶段,通过对成员方法的调用,可以取到标记应用阶段所设置的值。后面的勾魂小实例小节,对此做了详细的介绍。这块只是截取其中的两行代码,展示一下其使用:


在第二步标记解析时,终于回归正常。可以通过注解的成员方法,直接获取标记对象的属性值。
这就是,注解的成员方法具有两面性。

2.3.6.成员方法小节

总结一下,在定义注解时,其成员方法的规则如下:
(1)不能有参数,必须是无形参的方法
(2)成员方法可以加default, 设置默认的返回值
(3)一个注解内,仅仅定义了一个成员方法,并且名字为 value,在标记应用阶段,可以省略名称
(4)注解的成员方法具有两面性
(5)在标记应用阶段,方法名字当做属性使用
(6)在标记解析阶段,直接调用方法名称,取得属性值


2.4.注解的内部揭秘


注解是一个java类,最终会生成字节码,这里边藏有什么秘密呢?
使用javap指令,查看ConfigFieldAnno.class的内部结构,发现一个小秘密:一个注解其实就是一个接口,只是稍微有点儿特殊,继承了lang包的Annotation 接口。
具体如下:

 javap -l -p .\ConfigFieldAnno.class

Compiled from "ConfigFieldAnno.java"
public interface ConfigFieldAnno extends java.lang.annotation.Annotation {
  public abstract java.lang.String proterty();
}

建议大家亲自去试一试。
从这个角度来说,@interface关键词, 仅仅用于编码的阶段,并没有在底层实现,也算是Java的一个语法糖吧。

2.5.勾魂小实例

关于属性自动赋值的实例,已经讲解了其定义和使用,这里来进一步自定义剖析内部的原理。

2.5.1.类标记解析


前面提到了标记的解析,要想正确解析注解,离不开一个手段,那就是反射。这里为了讲清楚问题,将涉及到的反射分为类级别和属性级别。

类级别的注解反射操作,放在Class对象中,有两个:
(1)isAnnotationPresent() 方法
(2)getAnnotation() 方法

通过 Class 对象的 isAnnotationPresent() 方法,可以判断一个Class对象是否加上了某个注解标记。 其方法的声明如下:

public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {}

如果存在,可以通过 getAnnotation() 方法来获取某个注解标记。其方法的声明如下:

 public <A extends Annotation> A getAnnotation(Class<A> annotationClass) {}


2.5.2.类标记解析实战

讲了这么多,看看下面的这个定义在类前的类标记,如何解析呢?

@ConfigFileAnno(file = "/system.properties")
public class SystemConfig extends ConfigProperties {
.....
}

使用上面的两个反射方法,可以完成对自定义的@ConfigFileAnno的标记进行解析,并且获取其属性值,然后完成业务的操作。代码如下:

//判断是否使用了定义的注解接口
boolean exist = aClass.isAnnotationPresent(ConfigFileAnno.class);
if (!exist) {
    return null;
}

//获取注解接口中的
Annotation a = aClass.getAnnotation(ConfigFileAnno.class);

//强制转换成ConfigAnnotation类型
ConfigFileAnno configFile = (ConfigFileAnno) a;
//取得标记的属性值
String propertyFile = configFile.file();
Logger.info(aClass.getName() + ": " + propertyFile);
propertiesUtil = new ConfigProperties(propertyFile);
//加载配置文件
propertiesUtil.loadFromFile();


再来看看属性级别的标记。


2.5.3.属性标记解析


解析属性的标记,与类级别的标记一样,在反射的方法维度,也有这个两个方法,不过是放在Field类型的对象中:
(1)isAnnotationPresent() 方法
(2)getAnnotation() 方法

既然前面已经演示过,这里就不再赘述。除了这一组方法,还有另外的一个方法,可以一次取得一个属性上配置的所有标记:

public Annotation[] getAnnotations() {}

这个方法返回一个数组,返回注解到这个属性上的所有标记对象。然后可以迭代数组,取得所需要的标记。


2.5.4.属性标记解析实战

讲了这么多,看看下面的这个定义在属性前面的标记,如何解析呢?

 //服务器ip
    @ConfigFieldAnno(proterty= "socket.server.ip")
    public static String SOCKET_SERVER_IP;

 


使用上面的getAnnotations反射方法,对于每一个Field属性,可以取得一个标记数组。对于自定义的@ConfigFieldAnno的标记,进行特别的处理。

代码如下:

boolean exist = field.isAnnotationPresent(ConfigFieldAnno.class);
if (!exist) continue;
//获取注解接口中的
Annotation[] annotations = field.getAnnotations();
for (Annotation annotation : annotations) {
    if (!(annotation instanceof ConfigFieldAnno))
        continue;

    //业务操作: 取文件值,赋值
    loadField(configProperties, field, (ConfigFieldAnno) annotation);
}


上面有一个业务操作的方法:loadField,其代码大部分与本小节无关,为了不将陈述的逻辑搞得混乱,在这里不赘述内容,只是讲下思路。

大致的思路是:loadField就是根据注解标记的属性值,从配置的资源文件中加载配置项, 赋值给属性field。
具体的代码,可以来疯狂创客圈QQ群共享获取。

讲了这么多,有一个问题,怎么确定一个标记,是做类标记使用?还是作为属性标记使用呢?
不得不讲注解的最后一部分内容:元注解。

2.6.元注解


什么是元注解呢? 指的是用于标记注解的注解。 Java中,以下几个元注解,比较常见:
@Target、@Retention、@Documented、@Inherited、@Repeatable。

2.6.1.@Target


从英文字面意思上来说:target 是目标的意思。没有错,@Target 指定了注解所要标记的目标类型。
首先看下前面讲的@ConfigFileAnno注解:

package com.crazymakercircle.anno;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)

public @interface ConfigFileAnno {

    String file() ;

}


这个类级别的自定义注解,其元注解@Target中的值为:ElementType.TYPE,这个值表示@ConfigFileAnno注解目标类型为:类、接口、枚举。

再来看看属性级别的自定义注解@ConfigFieldAnno,源码如下:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)

public @interface ConfigFieldAnno {
    String proterty() ;

}

这个类级别的自定义注解,其元注解@Target中的值为:ElementType.FIELD,这个值表示@ConfigFileAnno注解目标类型为:属性。
如果使用@ConfigFileAnno去标记类、接口,JVM就会报错。

梳理一下,@Target 有下面的取值:

ElementType.ANNOTATION_TYPE 可以给一个注解进行注解
ElementType.CONSTRUCTOR  可以给构造方法进行注解
ElementType.FIELD  可以给属性进行注解
ElementType.LOCAL_VARIABLE 可以给局部变量进行注解
ElementType.METHOD 可以给方法进行注解
ElementType.PACKAGE 可以给一个包进行注解
ElementType.PARAMETER 可以给一个方法内的参数进行注解
ElementType.TYPE 可以给一个类型进行注解,比如类、接口、枚举

2.6.2.@Retention


从英文字面意思上来说:retention是保留的意思。没有错,@Retention指定了注解所要存在的生命周期。严格来说,这个指的是注解的实例对象、或者说标记的声明周期。
全部的生命周期,有3种: 开发阶段、编译阶段、运行阶段。对应这3种周期,@Retention的取值如下:
(1)开发阶段:对应的值为 RetentionPolicy.SOURCE
注解的标记,只在源码开发阶段保留,在编译器进行编译时它将被丢弃忽视。
(2)编译阶段:对应的值为RetentionPolicy.CLASS
注解的标记,能被保留到编译进行的阶段,它并不会被加载到 JVM 中。在编译完成之后,被丢弃。

 (3)运行阶段:对应的值为RetentionPolicy.RUNTIME

标记可以保留到程序运行的时候,它会被加载进入到 JVM 中,所以在程序运行时可以获取到它们。

一般来说,自定义的注解, 都需要保留到运行阶段,@Retention元注解的取值为RetentionPolicy.RUNTIME。

2.6.3.@Documented

它的作用是能够将注解中的元素包含到 Javadoc 中去。这个元注解很简单,这里不赘述。

2.6.4.@Repeatable

从英文字面意思上来说:repeatable是重复的意思。没有错,@Repeatable指的是,一个注解的标记,可以重复的加在同一个目标。
首先来看一个业务场景: 有一类学生,身兼多职,既是学生,也是班长,还是小组长。
使用前面的Role注解,如果需要表达上面的业务场景,结果应该是下面这样的:

@Role(name = "小组长")
@Role(name = "班长")
@Role(name = "课代表")
public class Master {
}

上面的代码,JVM会报错。原因:默认情况下,同一个注解只能在一个目标,加标记一次。上面在Master类之前,加了三个Role注解的标记。
如何解决这个问题呢?
JAVA提供了@Repeatable元注解,解决这种多重标记的问题。
使用@Repeatable注解@Role之后,代码如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Roles.class)
public @interface Role {
    String name () default "普通学生";
}

在上面的代码中,使用了 @Repeatable 源注解。  @Repeatable后面括号中,有一个特殊的小要求:需要给@Repeatable的value属性(可省略) 赋值,所赋值的内容,是一个特定的class对象。
这里用到的的class对象——Roles.class ,这是定义的一个新类,也是一个注解(后面讲到,注解其实就是类),但是这个注解和Role注解有关系,相当于容器和元素的关系,这个类Roles相当于一个容器类型,而Role注解对应于元素的类型。
Roles.class 的实际定义很简单,如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Roles {
    Role[] value ();
}

由于这个注解,需要用在@Repeatable后面,有几个强制的约束:
(1)它里面必须要有一个 value 的方法
(2)value成员的返回类型,必须是数组
(3)数组的元素类型,必须是@Repeatable元注解所标记的目标注解的类型,这里是Role注解类型
再次强调一下,在JAVA内部,注解就是一个接口,注解的名称,就是接口的名称。

在重复标记的业务场景下,标记解析的行为,发生了变化。
下面是带有多重角色的班长类的代码,来看一下:


@Role(name = "小组长")
@Role(name = "班长")
@Role(name = "课代表")
public class Master {
    public static void main(String[] args) throws NoSuchFieldException {
        Annotation annotation = Master.class.getAnnotation(Roles.class);
        System.out.println("Roles annotation = " + annotation);
        Annotation annotation2 = Master.class.getAnnotation(Role.class);
        System.out.println("Role annotation = " + annotation2);
    }


运行程序,会发现以下的结论:
(1)容器注解Roles 的标记对象是非空的,尽管——没有配置任何的Roles 标记。
(2)而元素注解Role 的标记对象,是空的,尽管——配置了3个Role标记。
再来看一个没有重复标记的场景。下面是普通学生类,只有一个角色,代码如下:


@Role(name = "普通学生")
public class Student {
    public static void main(String[] args) throws NoSuchFieldException {
        Annotation annotation = Student.class.getAnnotation(Roles.class);
        System.out.println("Roles annotation = " + annotation);
        Annotation annotation2 = Student.class.getAnnotation(Role.class);
        System.out.println("Role annotation = " + annotation2);
    }
}

而上面的程序,仅仅配置了一个角色,运行程序,行为又不同:
(1)容器注解Roles 的标记对象是空的,这一点,与重复配置的情况下不同。
(2)而元素注解Role 的标记对象,是非空的,并没有跑到容器对象里边去。

通过对比试验,可以发现:如果不重复配置, 仅仅配置一个元素标记,容器标记也不起作用。

2.6.5.@Inherited

从英文字面意思上来说:Inherited 是继承的意思, @Inherited 是一个元注解。如果一个注解加上了 @Inherited 元注解,其标记可以被继承。强调一下,不是注解可以继承,而是注解的标记可以被继承。

来看一个业务场景:和班长一样,副班长也身兼多职, 身兼多职,既是学生,也是班长,还是小组长。
那么,可以给@Role @Roles两个注解,加上@Inherited 元注解,代码如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Roles {
    Role[] value ();
}

Role注解的代码如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Repeatable(Roles.class)
public @interface Role {
    String name () default "普通学生";
}

那么,这个两个注解的标记,就可以被继承了。这样,副班长的类,可以直接继承班长的类标记。

public class ViceMaster extends  Master {
    public static void main(String[] args) throws NoSuchFieldException {
        Annotation annotation = ViceMaster.class.getAnnotation(Roles.class);
        System.out.println("Roles annotation = " + annotation);
      }
}


不需要在副班长类前面配置任何的注解标记, 发现已经继承了班长的标记。

运行程序,可以看到输出,具体如下:

Roles annotation =
@com.crazymakercircle.anno.Roles(
value=[@com.crazymakercircle.anno.Role(name=小组长),
 @com.crazymakercircle.anno.Role(name=班长),
 @com.crazymakercircle.anno.Role(name=课代表)])

猜你喜欢

转载自blog.csdn.net/crazymakercircle/article/details/82729700