欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

死磕,Java注解(Annotation )的秒懂之道

程序员文章站 2024-01-03 11:27:04
...

疯狂创客圈:如果说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。
   


[email protected]

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

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

 

[email protected]

 

       程序的开发的过程,是一个不断修改和完善优化的过程。一些的方法,经过几轮迭代,就会发现了一些潜在的问题和风险,不建议再继续调用了。
      可能有的兄弟要问了,不能再继续调用了,为什么又不直接去掉呢?
      没有那么简单!
      还是用前面提到的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方法,被标了一条中划线。

死磕,Java注解(Annotation )的秒懂之道

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

死磕,Java注解(Annotation )的秒懂之道

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

[email protected]

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

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


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


[email protected]

这个是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注解(Annotation )的秒懂之道

 

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

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

死磕,Java注解(Annotation )的秒懂之道

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


第一步很简单,以@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注解(Annotation )的秒懂之道
这一点看上去有点异常。明明是一个方法,怎么就换了个马甲?
当然可以确定的是,每一个标记,最后都会实例化成一个JAVA 对象。所设置的值,会保存在对象的内存中。在标记解析的阶段,可以取得这些成员属性的值。

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

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

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。

[email protected]


从英文字面意思上来说: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 可以给一个类型进行注解,比如类、接口、枚举

 

[email protected]


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

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

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

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

 

[email protected]

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

[email protected]

从英文字面意思上来说: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 的标记对象,是非空的,并没有跑到容器对象里边去。

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

 

[email protected]

 

从英文字面意思上来说: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=课代表)])

 

上一篇:

下一篇: