Java注解的系统化解读
1、Java注解的个人学习经验总结
Java注解诞生于Java 5,其官方文档是这样说的:Java注解用于为Java代码提供元数据。作为元数据,注解不直接影响代码的执行。注解通常拿来与注释做对比:注解与注释是有一定区别的,可以把注解理解为代码里的特殊标记,这些标记可以在编译,类加载,运行时被读取,并执行相应的处理。通过注解开发人员可以在不改变原有代码和逻辑的情况下在源代码中嵌入补充信息。
既然注解能够拿来与注释做对比,其内部联系自然不言而喻了,此处是参悟注解的一个切入点。我觉得,注解的本质是注释,所以,在我眼里压根就没有“注解”这个东西。当我想用“注释”的时候,我就用“注解”的格式来写,其实对于注解的各种语法规范,我也没有记得那么清楚,通常是即用即查,查找一下“注解”的书写格式是什么。很多人可能还在费劲心思地学习“注解”的各种语法规范,但是学完之后内心空虚仍然茫然失措无法灵活运用“注解”,而我直接无视了这些,比起他们来说,我省事多了,能抓住“注解”的本质,而且运用的比他们也熟练多了。有知识不代表有能力,因为能力来源于悟性。我对“注解”的悟性来源于时间的沉淀,以及研究mybatis框架时候迸发出来的灵感。知识易得,因为遍地都是,而经验和悟性难得,是时间的沉淀和灵感的迸发,本文主要解读注解相关的知识,关于“如何灵活运用注解”的经验和悟性,则仅限于传授给徒弟们。
2、注解的诞生
从JDK5开始,Java增加对元数据的支持,也就是注解,通过注解开发人员可以在不改变原有代码和逻辑的情况下在源代码中嵌入补充信息。本文将从注解的定义、元注解、注解属性、自定义注解、注解解析JDK提供的注解这几个方面系统化解读Java注解(Annotation)
3、注解的定义
日常开发中新建Java类,我们使用Class、Interface比较多,而注解和它们一样,也是一种类的类型,它是用的修饰符为 @interface。顾名思义,注解和接口有一定的内在联系。后文会有介绍。
4、注解的写法
我们新建一个注解MyAnnotation
public @interface MyAnnotation
{
}
接着我们就可以在类或者方法上加入我们刚刚新建的注解,如下所示:
@MyAnnotation
public class Test
{
@MyAnnotation
public static void main(String[] args)
{
}
}
以上我们只是了解了注解的写法,但是我们定义的注解中还没写任何代码,现在这个注解毫无意义,要如何使注解工作呢?接下来我们接着了解元注解。
5、元注解
元注解,顾名思义我们可以理解为注解的注解,它是作用在注解中,方便我们使用注解实现想要的功能。元注解分别有@Retention、 @Target、 @Document、 @Inherited和@Repeatable(JDK1.8加入)五种。
@Retention
Retention英文意思有保留、保持的意思,它表示注解存在阶段是保留在源码(编译期),字节码(类加载)或者运行期(JVM中运行)。在@Retention注解中使用枚举RetentionPolicy来表示注解保留时期,如下所示:
- @Retention(RetentionPolicy.SOURCE),注解仅存在于源码中,在class字节码文件中不包含
- @Retention(RetentionPolicy.CLASS),默认的保留策略,注解会在class字节码文件中存在,但运行时无法获得
- @Retention(RetentionPolicy.RUNTIME),注解会在class字节码文件中存在,在运行时可以通过反射获取到
如果我们是自定义注解,则通过前面分析,我们自定义注解如果只存着源码中或者字节码文件中就无法发挥作用,而在运行期间能获取到注解才能实现我们目的,所以自定义注解中肯定是使用 @Retention(RetentionPolicy.RUNTIME)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation
{
}
@Target
Target的英文意思是目标,这也很容易理解,使用@Target元注解表示我们的注解作用的范围就比较具体了,可以是类,方法,方法参数变量等,同样也是通过枚举类ElementType表达作用类型。
- @Target(ElementType.TYPE) 作用接口、类、枚举、注解
- @Target(ElementType.FIELD) 作用属性字段、枚举的常量
- @Target(ElementType.METHOD) 作用方法
- @Target(ElementType.PARAMETER) 作用方法参数
- @Target(ElementType.CONSTRUCTOR) 作用构造函数
- @Target(ElementType.LOCAL_VARIABLE)作用局部变量
- @Target(ElementType.ANNOTATION_TYPE)作用于注解(@Retention注解中就使用该属性)
- @Target(ElementType.PACKAGE) 作用于包
- @Target(ElementType.TYPE_PARAMETER) 作用于类型泛型,即泛型方法、泛型类、泛型接口 (jdk1.8加入)
- @Target(ElementType.TYPE_USE) 类型使用.可以用于标注任意类型除了 class (jdk1.8加入)
一般比较常用的是ElementType.TYPE类型,如下所示:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyAnnotation
{
}
@Documented
Document的英文意思是文档,它的作用是能够将注解中的元素包含到 Javadoc 中去。
@Inherited
Inherited的英文意思是继承,其实这个继承和我们平时理解的继承大同小异,一个被@Inherited注解了的注解修饰了一个父类,如果它的子类没有被其他注解修饰,则它的子类也继承了父类的注解。下面我们来看个@Inherited注解例子
/**自定义注解*/
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyAnnotation
{
}
/**父类标注自定义注解*/
@MyAnnotation
public class FatherClass
{
}
/**子类*/
public class SonClass extends FatherClass
{
}
/**测试子类获取父类自定义注解*/
public class Test
{
public static void main(String[] args)
{
//获取SonClass的class对象
Class<Son> sonClass = SonClass.class;
// 获取SonClass类上的注解MyAnnotation可以执行成功
MyAnnotation annotation = sonClass.getAnnotation(MyAnnotation.class);
}
}
@Repeatable
Repeatable的英文意思是可重复的。顾名思义说明被这个元注解修饰的注解可以同时作用一个对象多次,但是每次作用注解又可以代表不同的含义。在生活中一个人往往是具有多种身份,例如我是一家公司的老板,同时我还是我妻子的丈夫,更是我父母的孩子,如果希望借助注解的方式来表达该如何呢?首先定义一个Role注解来表示我所有的身份:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Role
{
Person[] value();
}
这里@Target是声明Role注解的作用范围,参数ElementType.Type代表可以给一个类型进行注解,比如类,接口,枚举。@Retention是注解的有效时间,RetentionPolicy.RUNTIME是指程序运行的时候。
接下来我们就定义一个注解,这里用到了@Repeatable注解,来真正表达我们的身份:
@Repeatable(Role.class)
public @interface Person
{
String role() default "";
}
备注:@Repeatable括号内的就相当于用来保存该注解内容的容器。
然后,为“我”来创建一个实体类:
@Person(role = "CEO")
@Person(role = "Husband")
@Person(role = "Father")
@Person(role = "Son")
public class Me
{
}
上述是匿名注解Role,等同于下面的显示注解:
@Role({
@Person(role = "CEO"),
@Person(role = "Husband"),
@Person(role = "Father"),
@Person(role = "Son")
})
public class Me
{
}
最后测试一下,获取所有的身份信息并输出:
public class Test
{
public static void main(String[] args)
{
if (Me.class.isAnnotationPresent(Role.class))
{
Role role = Me.class.getAnnotation(Role.class);
for (Person person : role.value())
{
System.out.println(person.role());
}
}
}
}
以上两种方式都能得到如下输出结果:
CEO
Husband
Father
Son
6、注解的属性与属性赋值
注解的属性
注解的属性也叫做注解类的成员变量,需要注意的是:注解只有成员变量,没有方法。
更颠覆通常思维认知的是:注解的成员变量在注解的定义中以“无形参的方法”形式来声明,其方法名定义了该成员变量的名字,其返回值定义了该成员变量的类型。如下所示:
public @interface Author
{
String name();
//注解属性1
int age();
//注解属性2
}
上面代码定义了 Author 这个注解,其中拥有 name 和 age 两个属性。
注解属性赋值
在使用注解的时候,我们应该给它们的属性进行赋值。赋值的方式是在注解的括号内以 key=value 形式,多个属性之前用逗号隔开,如下所示:
@Author(name = "Bill", age = 35)
public class Book
{
}
注解属性的默认值
注解中属性可以有默认值,默认值需要用 default 关键值指定。比如:
public @interface Author
{
String name() default "";
int age() default 0;
}
它可以这样应用:
@Author()
public class Book
{
}
因为有默认值,所以无需要再在 @Author 后面的括号里面进行赋值了。
注解默认的value属性
如果一个注解内仅仅只有一个名字为 value 的属性时,使用这个注解时可以直接把属性值填写到括号内,如下所示:
public @interface Author
{
String value() default "";
}
上面代码中,Author这个注解只有 value 这个属性,所以可以这样使用:
@Author("Tom")
public class Book
{
}
这和下面的效果是一样的
@Author(value="Tom")
public class Book
{
}
无属性注解
一个注解没有任何属性,比如下面代码:
public @interface MyAnnotation
{
}
那么在应用这个注解的时候,括号都可以省略:
@MyAnnotation
public class Test
{
}
注解属性的类型
注解属性类型可以是以下列出的类型:
(1)基本数据类型
(2)String
(3)枚举类型
(4)注解类型,讲上文的@Repeatable注解的应用场景。
(5)Class类型
(6)以上类型的一维数组类型
获取注解属性
前面我们说了很多注解如何定义,放在哪,现在我们可以开始学习注解属性的提取了,这才是使用注解的关键,获取属性的值才是使用注解的目的。如果获取注解属性,当然是反射啦,主要有三个基本的方法
/**是否存在对应 Annotation 对象*/
public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass)
{
return GenericDeclaration.super.isAnnotationPresent(annotationClass);
}
/**获取 Annotation 对象*/
public <A extends Annotation> A getAnnotation(Class<A> annotationClass)
{
return (A) annotationData().annotations.get(annotationClass);
}
/**获取所有 Annotation 对象数组*/
public Annotation[] getAnnotations()
{
return AnnotationParser.toArray(annotationData().annotations);
}
7、注解的本质(内涵本质与外形本质)
注解的本质是注释,这是从内涵的角度来说的。如何把注解的语言规范融入现有的Java体系呢?JDK作者采用的方法是改造接口。大家可以想一想为什么要把注解融入到接口里面而不是其他东东呢?留作文末思考题吧。因为此处的改造有点不伦不类,导致大家对注解的认知产生困惑,暂且不表,先请看一下Java接口的特性吧:
(1)接口中可以含有变量,但是接口中的变量会被隐式的指定为 public static final 变量(并且只能是 public,用 private 修饰会报编译错误)。
(2)接口中每一个方法是隐式抽象的,接口中的方法会被隐式的指定为 public abstract(只能是 public abstract,其他修饰符都会报错)。
备注:JDK 1.8 以后,接口里可以有静态方法和方法体了。
要想把接口改造成注解,我们应该立足于接口特征进行微调,也就是说注解中其实是可以有属性和方法,但是接口中的属性都是static final的,对于注解来说没什么意义,所以可以扔掉,再看看接口方法是否可以改造。接口方法是可以改造的,我们定义接口的方法就相当于注解的属性,也就对应了前面说的为什么注解只有属性成员变量,其实它就是接口的方法,这就是为什么成员变量会有括号,不同于接口我们可以在注解的括号中给成员变量赋值。
8、文末思考题:语法规范解读与思维认知习惯
大家可以想一想为什么要把注解融入到接口里面而不是其他东东呢?我个人的理解是这样的:接口的应用场景有一种情况为:标记接口,即接口不包含任何方法。在Java里很容易找到标记接口的例子,比如JDK里的Serializable接口就是一个标记接口。注解本质就是注释,把标记接口进行升华改造,存放注释内容,如此设计才是更好的。
9、后记:本文的创新点总结
注解的内容繁多,本文的撰写时间不足,存在毛刺难以避免,请大家小心阅读。我觉得,死记注解的语法并不重要,重要的能把注解运用自如。对于注解的理解深度因人而异,不同的人理解的深度不同。知识易得,因为遍地都是,本文的撰写过程也参考了大量网络内容,而经验和悟性最难得,是时间的沉淀和灵感的迸发。本文主要解读注解相关的知识,关于“如何灵活运用注解”的经验和悟性,属于文中最大的创新点,且更多细节介绍仅限于传授给徒弟们,欢迎关注站长收徒。