背景
如今常用的Java框架中已经使用到了大量的注解(Annotation) 这些自定义的注解都是通过JDK提供的元注解构建了, 于是花了一点时间研究了一下JDK1.8 中的元注解, 在此总结一下:
注解的作用
注解从字面上的意思理解为注释, 解释. 就是在源代码中添加一些额外的信息(比如@author等用在源文件中). 其实Java注解的功能远不止如此, 本质上讲注解是往源代码中添加了一些额外的信息, 以便于在其他的类中可以将这些信息获取出来加以使用. 总的来讲注解有三个作用:
- 帮助生成API文档. 注解可以看成一些特殊的标签, 方便使用javadoc工具将源代码中的一些注释生成API文档, 比如使用@Documented 标注的注解生成的API文档中就可以看见.
- 代码跟踪, 替代配置文件. 这应该是注解最重要的一个作用, 因为注解中可以包含一些数据, 这些数据可以代表配置信息, 使用反射可以提取这些信息. 因此使用注解我们就可以不需要额外的配置文件.
- 编译时格式检查. 比如@Override 这样的注解 表示该方法覆盖父类, 如果不是覆盖父类则编译时会报错.
总之注解赋予了源代码更多的信息, 而通过灵活的使用这些信息可以帮助更好的编码.
注解的声明
下面来看一个最简单的注解声明
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface User {
// 使用@interface 定义一个注解 注解之中定义的都是方法 每一个方法其实是声明的参数 方法名称即是参数名称 方法返回类型即是参数类型
// 参数类型可谓基本类型 Class类型 Enum(枚举) String Annotaion 或者以上的数组[],不能为任何封装的类型以及自定义的类
// 方法后可以接default 指定方法的默认值
// 非基本类型如String 素默认值不能为null
// 所有的自定义注解都是继承自 Annotation 类型 且不可再继承其他接口以及注解. 代码中不直接写明继承自Annotation 这个过程的细节是编译器完成的
String name() default "hello";
}
首先注解的类型是@Interface 声明的时候不要和接口弄混淆了, 注解的顶部是一些其他的注解, 此处使用到的是元注解(后面会解释) 注解中定义的是方法, 首先方法的名称代表的是这个注解的参数, 方法返回的数据类型则是这个参数的类型, 返回类型有严格的限制, 类型为以下的几种类型:1. 基本类型(short long float double int char boolean byte) 2. Class 类型 3. enum 枚举类型 4. Annotation 类型 5. String 类型 6. 以上几种类型构成的数组[]. 所有的注解都自动继承自java.lang.annotation.Annotation 接口, 这个接口在声明的时候不写出,且不允许自定义注解继承其他类或者实现其他接口. 注解中的方法允许存在默认值, default value.2. Class 类型 3. enum 枚举类型 4. Annotation 类型 5. String 类型 6. 以上几种类型构成的数组[]. 所有的注解都自动继承自java.lang.annotation.Annotation 接口, 这个接口在声明的时候不写出,且不允许自定义注解继承其他类或者实现其他接口. 注解中的方法允许存在默认值, default value.
元注解
上面的例子中我们看到在声明注解的那一行(@interface)上面使用到了三个注解, 这三个注解就是元注解.元注解构成所有的其他自定义注解. 所以要明白其它自定义注解的作用和意义, 需要清楚元注解的作用. JDK1.8 中包含了五种元注解@Target @Retention @Documented @Inherited @Repeatable 下面分别对这五种注解功能进行解释说明:
@Documented
这个注解是最简单的元注解, 它表示使用此注解标注的自定义注解再通过javadoc工具生成API文档的时候会出现在API文档里面. 举个例子@Deprecated 这个注解再Java中非常常见.
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}
看它的声明里面就有@Documented, 所以生成的API文档里面存在deprecated 的方法的地方都能够看到这个注解.
@Target
这个元注解是非常重要的元注解, 字面意思为"目标" 指定的是注解的有效范围, 我们看这个注解的声明:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
/**
* Returns an array of the kinds of elements an annotation type
* can be applied to.
* @return an array of the kinds of elements an annotation type
* can be applied to
*/
ElementType[] value();
}
它只存在一个方法, 返回的类型是ElementType 这个枚举类型, 这个枚举类型取值有:
public enum ElementType {
/** Class, interface (including annotation type), or enum declaration */
TYPE,
/** Field declaration (includes enum constants) */
FIELD,
/** Method declaration */
METHOD,
/** Formal parameter declaration */
PARAMETER,
/** Constructor declaration */
CONSTRUCTOR,
/** Local variable declaration */
LOCAL_VARIABLE,
/** Annotation type declaration */
ANNOTATION_TYPE,
/** Package declaration */
PACKAGE,
/**
* Type parameter declaration
*
* @since 1.8
*/
TYPE_PARAMETER,
/**
* Use of a type
*
* @since 1.8
*/
TYPE_USE
}
从上往下可以很容易理解它指明的作用范围:1.TYPE作用于类上 2.FIELD作用于成员变量 3.METHOD 作用于成员方法 4.PARAMETER 作用于方法参数上 5.CONSTRUCTOR 作用于构造函数 6.LOCAL_VARIABLE 作用于局部变量 7. ANNOTATION_TYPE 作用于注解上, 比如@Target 这个元注解, 它的@Target 取值是ANNOTATION_TYPE 8.PACKAGE 作用于包的注解.
@Retention
这个注解可以理解为保留时间(生命周期) 看它的声明中也只有一个方法:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
/**
* Returns the retention policy.
* @return the retention policy
*/
RetentionPolicy value();
}
返回值类型依然是一个枚举类型:
public enum RetentionPolicy {
/**
* Annotations are to be discarded by the compiler.
*/
SOURCE,
/**
* Annotations are to be recorded in the class file by the compiler
* but need not be retained by the VM at run time. This is the default
* behavior.
*/
CLASS,
/**
* Annotations are to be recorded in the class file by the compiler and
* retained by the VM at run time, so they may be read reflectively.
*
* @see java.lang.reflect.AnnotatedElement
*/
RUNTIME
}
这三个值也比较好理解 1. SOURCE 表示该注解只是存在于源代码中, 编译时会舍弃 2. CLASS 表示该注解编译时会编译到CLASS文件中, 但是JVM加载CLASS文件时会舍弃 3.RUNTIME 表示JVM加载CLASS文件时会加载该注解, 因此可以通过反射获取这些注解信息. 绝大多数的注解生命周期类型都是RUNTIME这一种.
@Inherited
与@Documented 注解一样, 它只是起标记作用, 此注解声明的时候内部没有定义函数, 所以在使用时不需要传递参数.
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Inherited {
}
该注解表示的是使用含有该元注解去标注的类, 在其继承的子类中也是自动继承此注解. 有两个点特别重要:1. 注解要生效必须标注在类上面, 方法以及成员变量是无效的 2. 标注在interface上面其实现类是无法继承改接口. 此注解提供了一种向上查询的机制, 当在某一个类上查询此类型注解时, 会向上查询, 知道找到含有此标注的父类或者是找不到此标注的父类才结束. 举个例子
首先声明一个@Inherited 标记的注解
@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestInherit {
String value() default "hello";
}
然后用这个注解去标注一个类
@TestInherit("the test string")
public class UseAnnotation {
private String name;
public UseAnnotation(){
}
public UseAnnotation(String name){
this.name = name;
}
public String getName(){
return this.name;
}
}
将这个类称为标记的父类.
使用反射去获取标记信息
public static void main(String[] args) throws ClassNotFoundException{
Class clazz = Class.forName("com.myannotation.testinherited.UseAnnotation");
if(clazz.isAnnotationPresent(TestInherit.class)){
System.out.println("inherited");
TestInherit t = (TestInherit) clazz.getAnnotation(TestInherit.class);
System.out.println(new UseAnnotation(t.value()).getName());
}else{
System.out.println("not inherited");
}
}
看输出结果:
inherited
the test string
这个理所当然, 现在扩展这个标记父类
public class SubUseAnnotation extends UseAnnotation {
private int age;
public SubUseAnnotation(String name, int age){
super(name);
this.age = age;
}
public int getAge(){
return this.age;
}
}
然后使用反射在这个子类上面获取注解信息
public static void main(String[] args) throws ClassNotFoundException{
Class clazz = Class.forName("com.myannotation.testinherited.SubUseAnnotation");
if(clazz.isAnnotationPresent(TestInherit.class)){
System.out.println("inherited");
TestInherit t = (TestInherit) clazz.getAnnotation(TestInherit.class);
System.out.println(new SubUseAnnotation(t.value(), 10).getName());
}else{
System.out.println("not inherited");
}
}
输出结果
inherited
the test string
可见在继承的子类中也可以获取到此注解.
@Repeatable
先看这个注解的声明:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Repeatable {
/**
* Indicates the <em>containing annotation type</em> for the
* repeatable annotation type.
* @return the containing annotation type
*/
Class<? extends Annotation> value();
}
参数范围类型为存储容器注解类型. 表示此注解修饰的注解是可重复的修饰某一个元素, 传递的参数即是该可重复注解的存储容器注解类型. 可重复很好理解, 就是在某一个元素上面可以多次使用这个注解. 比如先定义一个普通的注解:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface NormalAnn {
String value() default "";
}
然后重复在一个类上面使用这个注解:
@NormalAnn("hello")
@NormalAnn("world")
public class UseNormalClass {
}
此时IDE中会报错, 错误信息是:XXXX does not have a valid Repeatable annotation
即是没有使用@Repeatable 标注的注解是不能够在其修饰的元素上面重复使用. 首先明白要使用这个重复标注功能 需要一个可重复标注的注解以及存储该注解的容器, 定义这个可重复标注的注解如下:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(RepeatContainer.class)
public @interface RepeatAnn {
String name() default "";
int age() default -1;
}
指定了Container为RepeatContainer 然后定义的RepeatContainer如下:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RepeatContainer {
RepeatAnn[] value();
}
指的注意的一点是这个存储容器的@Target是重复标注(此处为RepeatAnn的@Target的子集, 其次这个容器中需要有一个value方法其返回值为重复标注(RepeatAnn) 构成的数组. 下面在一个类中使用这个可重复的标注
public class UseRepeatAnn {
@RepeatAnn(name="aa", age=1)
@RepeatAnn(name="bb", age=2)
public void method1(){
}
@RepeatAnn(name="cc", age=3)
public void method2(){
}
}
编译器没有报错, 注解使用没有问题. 下面来看如何正确的通过反射来获取这些重复的注解
public class UseRepeatAnn {
@RepeatAnn(name="aa", age=1)
@RepeatAnn(name="bb", age=2)
public void method1(){
}
@RepeatAnn(name="cc", age=3)
public void method2(){
}
public static void main(String[] args) throws NoSuchMethodException {
Class clazz = UseRepeatAnn.class;
Method m = clazz.getMethod("method1");
System.out.println("-----------------case1----------------");
Annotation t1 = m.getAnnotation(RepeatContainer.class);
System.out.println(t1);// 正确的访问方式 获取到这个容器注解
for(Annotation item : ((RepeatContainer) t1).value()){
System.out.println(((RepeatAnn)item).age());
}
System.out.println("-----------------case2----------------");
Annotation t2 = m.getAnnotation(RepeatAnn.class);
System.out.println(t2);// 错误的访问方式输出为null 因为这些注解存储在容器里面 不是表面上直接标注于方法上
System.out.println("-----------------case3----------------");
Annotation[] t3 = m.getAnnotations();
System.out.println(t3.length);// 这个长度为1 因为返回的是容器类型 RepeatContainer
for(Annotation item : ((RepeatContainer)t3[0]).value()){
System.out.println(((RepeatAnn)item).age());
}
System.out.println("-----------------case4----------------");
Annotation[] t4 = m.getAnnotationsByType(RepeatAnn.class);
System.out.println(t4.length);
for(Annotation item : t4){
System.out.println(((RepeatAnn)item).age());
}
}
}
自定义注解使用流程
自定义注解使用流程包含:1.声明一个注解;2.将注解标注在对应的元素上并添加信息;3.使用反射获取标记元素上的注解信息加以使用.关于注解的声明以及标注在对应的元素上面前面的例子已经说过, 下面主要是说一下反射获取元素上的注解信息.
查看反射Class中的关于注解操作的方法,主要方法如下
1.isAnnotationPresent(Class<? extends Annotation> annotationClass)
判断该元素是否受到指定注解类型(annotationClass)的标注.
2.isAnnotation()
判断该元素是否是注解类型
后面的方法需要分为两类Declared方法和非Declared方法.
1.getAnnotation(Class<A> annotationClass)
返回指定标注类型的实例, 如果没有返回为null
2 getAnnotations()
返回该元素上的所有标注 如果没有返回一个长度为0的数组.
3.getAnnotationsByType(Class<A> annotationClass)
这个在上面的@Repeatable的例子中已经使用了, 专门获取重复注解的实例.
另外一个版本是Declared 版本, 即是
getDeclaredAnnotation(Class<A> annotationClass)
getDeclaredAnnotations()
和 getDeclaredAnnotationsByType(Class<A> annotationClass)
这个版本的方法区别即是获取的注解全是直接注解, 即是没有@Inherited标注的注解, 父类上的@Inherited注解是无法通过这些方法获取到的.