如何获取注解中的值_如何在运⾏时利⽤注解信息
注解( annontation )是 Java 1.5 之后引⼊的⼀个为程序添加元数据的功能。注解本⾝并不是魔法,只是在代码⾥添加了描述代码⾃⾝的信息,⾄于如何理解和使⽤这些信息,则需要专门的解析代码来负责。
本⽂⾸先介绍注解的基本知识,包括注解的分类和运⽤时的领域知识。随后,给出⼀个通过的在运⾏时解析注解的框架代码,介绍处理注解的⼀般思路。最后,通过现实世界⾥使⽤注解的例⼦,来加深对注解的实⽤性⽅⾯的认识。
注解的基本知识
注解作为程序中的元数据,其本⾝的性质也被其上的注解所描述。
刚刚我们提到,理解和使⽤注解信息,需要专门的解析代码。其中,Java 的编译器和虚拟机也包含解析注解信息的逻辑,⽽它们判断⼀个注解的性质,就是依赖注解之上的元注解。
能够注解⼀个注解的注解就是元注解,Java 本⾝能够识别的元注解有以下⼏个。
@Retention
Retention 注解的相关定义如下
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
RetentionPolicy value();
}
public enum RetentionPolicy {
SOURCE,
CLASS,
RUNTIME
}
⾸先我们看到它⾃⼰也被⼏个元注解包括⾃⾝所注解,因此在注解的源头有⼀个类似于⾃举的概念,最终触发⾃举的是编译器和源代码中的先验知识。
再看到 Retention 注解的值,是⼀个注解保留性质的枚举,包括三种情况。
1. SOURCE 表⽰注解信息仅在编译时保留,在编译之后就被丢弃,这样的注解为代码的编译提供原信息。例如常⽤的 @Override 注解
就提⽰ Java 编译器进⾏重写⽅法的检查。
2. CLASS 表⽰注解信息保留在字节码中,但在运⾏时不可见。这是注解的默认⾏为,如果定义注解时没有使⽤ Retention 注解显式表
明保留性质,默认的保留性质就是这个。
3. RUNTIME 表⽰注解信息在运⾏时可见,当然,也就必须保留在字节码中。
SOURCE 标注的注解通常称为编译期注解,Lombok 项⽬提供⼤量的编译期注解,以帮助开发者简写⾃⼰的代码。例如 @Setter 注解注解在类上时,在编译期由 Lombok 的注解处理器处理,为被注解的类的每⼀个字段⽣成 Setter ⽅法。
编译期的注解需要专门的注解处理器来处理,并且在编译时指定处理器的名字提⽰编译期使⽤该处理器进⾏处理。技术上说,编译期处理注解和运⾏时处理注解完全是两个概念的事情。本⽂主要介绍运⾏时处理注解的技术,关于编译期处理注解的资料,可以参考这篇ANNOTATION PROCESSING 101 的⽂章以及 Lombok 的源码。
CLASS 性质虽然是默认的保留性质,但实际使⽤中⼏乎没有采⽤这⼀保留性质的。准确需要这⼀性质的情形应该是某些专门的字节码处理框架,⼤多数时候使⽤这⼀性质的注解仅仅是在编译期使⽤,使⽤ SOURCE ⾜以,且使⽤ SOURCE 还可以减少字节码⽂件的⼤⼩。
本⽂介绍运⾏时处理注解的技术,所有在运⾏时可见的注解都需要显式地标注 @Retention(RetentionPolicy.RUNTIME) 注解。CLASS 和RUNTIME 性质的注解都会出现在字节码中。编译器将注解信息写成字节码时,通过为 CLASS 性质的注解赋予RuntimeInvisibleAnnotations 属性,为 RUNTIME 性质的注解赋予 RuntimeVisibleParameterAnnotations 来提⽰虚拟机在运⾏时加载的时候区别对待。
运⾏时,我们可以调⽤被注解对象的相应⽅法取得其上的注解,具体⼿段在【注解解析的框架代码】⼀节中介绍。
@Target
上⼀节最后我们提到,注解有不同的注解对象,这正是 Target 注解加⼊的元数据,其定义如下
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
ElementType[] value();
}enum怎么用
public enum ElementType {
TYPE,
FIELD,
METHOD,
PARAMETER,
CONSTRUCTOR,
LOCAL_VARIABLE,
ANNOTATION_TYPE,
PACKAGE,
TYPE_PARAMETER,
TYPE_USE,
MODULE
}
Target 元注解的信息解释了⼀个注解能够被注解在什么位置上,或者说能够接受该注解的对象集合。⼀个注解可以有多种类型的注解对象,所有这些对象类型存在 ElementType 枚举中。
⼤多数枚举值的含义就是字⾯含义,值得⼀提的取值包括
TYPE 在 Java 中指类、接⼝、注解或者枚举类
TYPE_PARAMETER 在 Java 1.8 中被引⼊,指的是泛型中的类型参数
TYPE_USE 在 Java 1.8 中被引⼊,指的是所有可以出现类型的位置,具体参考 Java 语⾔标准的对应章节
常见的 Override 注解只能注解在⽅法上,Spring 框架中的 Component 注解只能注解在类型上。SuppressWarnings 注解能注解在除了本地变量和类型参数以外的⼏乎所有地⽅,Spring 框架中的 Autowired 注解也能注解在字段、构造器、⽅法参数和注解等多种位置上。
@Inherited
Inherited 主要⽤来标注注解在类继承关系之间的传递关系。它本⾝不携带⾃定义信息,仅作为⼀个布尔信息存在,即是或者不是 Inherited 的注解。
标注 Inherited 元注解的注解,标注在某个类型上时,其⼦类也默认视为标注此注解。或者换个⽅向说,获取某个类的注解时,会递归的搜索其⽗类的注解,并获取其中标注 Inherited 元注解的注解。注意,标注 Inherited 元注解的注解在⼦类上也标注时,⼦类上的注解优先级最⾼。
技术上说,可以通过 getAnnotations 和 getDeclaredAnnotations 的区别来获取确切标注在当前类型上的注解和按照上⾯描述的⽅法查的注解。另⼀个值得强调的是这种继承仅发⽣在类的继承上,实现接⼝并不会导致标注 Inherited 元注解的注解的传递。
值得注意的是,注解本⾝是不能继承的。为了实现类似继承的效果,开发者们从基于原型的继承到灵感,采⽤本节后续将讲到的组合注解技术来达到注解继承的⽬的。
@Repeatable
Repeatable 注解在 Java 1.8 中被引⼊,主要是为了解决相同的注解只能出现⼀次的情况下,为了表达实际中需要的相同注解被标注多次的逻辑,开发者不得不⾸先创建出⼀个容器注解,然后使⽤者在单个和多个注解的情况下分别使⽤基础注解和容器注解的繁琐逻辑。具体例⼦如下
@ComponentScan(basePackages = "my.package")
class MySimpleConfig { }
@ComponentScans({
@ComponentScan(basePackages = "my.package")
@ComponentScan(basePackages = "my.another.package")
})
class MyCompositeConfig { }
有了 Repeatable 注解,从注解处理⽅,代码不会精简,仍然需要分开处理两种注解类型,但是使⽤⽅就可以精简代码。例如上⾯MyCompositeConfig 的标注可以变为
@ComponentScan(basePackages = "my.package")
@ComponentScan(basePackages = "my.another.package")
class MyCompositeConfig { }
对应的注解定义为
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Repeatable(ComponentScans.class)
public @interface ComponentScan {
// ...
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
public @interface ComponentScans {
ComponentScan[] value();
}
对于注解的处理⽅,重复注解会在背后由 Java 编译器转化为容器注解的形式传递。就上⾯的例⼦⽽⾔,⽆论有没有 Repeatable 注
解,MyCompositeConfig 在获取注解时,都会获取到 ComponentScans 注解及其 ComponentScan[] 形式的元数据信息。
值得注意的是,重复注解和容器注解不能同时存在,即在标记了 @Repeatable(ComponentScans.class) 之后,ComponentScans 和ComponentScan 不能同时标注同⼀个对象。
@Documented
这个注解没有太多好说的,注解信息在⽣成⽂档时默认是不会留存的。如果使⽤此注解标注某个注解,那么被标注的注解注解的对象的⽂档会显⽰它被对应的注解所标注。
组合注解
严格来说,组合注解是⼀种设计模式⽽不是语⾔特性。
由于注解⽆法继承,例如 Spring 框架中具有 "is-a" 关系的 Service 注解和 Component 注解,⽆法通过继承将 Service 定义为Component 的特例。但是在实际使⽤的时候,⼜确实有表达这样 "is-a" 关系的需求。
在框架代码中,⽆法穷尽对下游项⽬扩展注解实质上的继承关系的情况,但是⼜需要⽀持下游项⽬⾃定义框架注解的扩展。如何将下游项⽬⾃定义的注解和框架注解之间的继承关系表达出来,就是⼀个技术上实际的需求。
为了解决这个问题,开发者们注意到在注解设计之初,留下了注解能够标注注解的路径。这⼀路径使得我们可以采⽤⼀种类似基于原型的继承的⽅式,通过递归获取注解上的注解来追溯注解的链条,从⽽类似原型链上⽗类的⽅式到当前注解逻辑上继承的注解。
这⼀技术在 Spring 框架中被⼴泛使⽤,例如 Service/Repository/Controller 等注解组合了 Component 注解,从⽽在下⼀节的注解解析的框架代码中能够作为 Component 的某种意义上的⼦注解被识别,同时在需要时取出继承的注解的元数据信息。
注解解析的框架代码
Java 语⾔提供的⽅法
注解解析最基础的⼿段是通过 Java 语⾔本⾝提供的⽅法。哪怕是其他框架增强注解解析的功能,最终也需要依赖基本⽅法的⽀持。
运⾏时获取注解信息,可想⽽知是通过反射的⼿段来获取的。Java 为被注解的元素定义了⼀个 Annotat
edElement 的接⼝,通过这⼀接⼝的⽅法可以在运⾏时取得被注解元素之上的注解。该接⼝的实现类是运⾏时通过反射拿到的元素⾥⾯能够被注解的类。
我们先看到这⼀接⼝提供的⽅法。
public interface AnnotatedElement {
<T extends Annotation> T getAnnotation(Class<T> annotationClass);
Annotation[] getAnnotations();
<T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass);
Annotation[] getDeclaredAnnotations();
<T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass);
<T extends Annotation> T[] getDeclaredAnnotationsByType(Class<T> annotationClass);
boolean isAnnotationPresent(Class<? extends Annotation> annotationClass);
}
这些⽅法没必要⼀个⼀个讲,其实可以简单地分成两类
获取被注解对象上声明的注解,即 getDeclaredAnnotations 系列的⽅法
获取被注解对象所拥有的注解,即 getAnnotations 系列的⽅法,⽐起上⼀类,额外包括 @Inherited 的注解
最后 isAnnotationPresent ⽅法仅仅是⼀个判断标签式注解的简易⽅法,内容只有⼀⾏。
default boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {
return getAnnotation(annotationClass) != null;
}
我们可以通过 Java 语⾔⾃⾝的 AnnotationSupport#getIndirectlyPresent ⽅法来看看怎么⽤这套基础⽀持解析注解。
private static <A extends Annotation> A[] getIndirectlyPresent(
Map<Class<? extends Annotation>, Annotation> annotations,
Class<A> annoClass
) {
Repeatable repeatable = DeclaredAnnotation(Repeatable.class);
if (repeatable == null)
return null; // Not repeatable -> no indirectly present annotations
Class<? extends Annotation> containerClass = repeatable.value();
Annotation container = (containerClass);
if (container == null)
return null;
// Unpack container
A[] valueArray = getValueArray(container);
checkTypes(valueArray, container, annoClass);
return valueArray;
}
以上这段代码是在 Java 1.8 引⼊ Repeatable 注解后,由于默认的会将重复的 Repeatable 的注解在获取时直接合并成容器注解,为了提供⼀个⽅便的按照基础注解来获取注解信息的⼿段提供的⽅法。
我们看到,传⼊的内容包括⼀个根据 Class 对象查实现类对象的映射,这个是被注解类所取得的所拥有的注解的类到实例的字典,不⽤过多关注。另⼀⽅⾯ annoClass 则是我们想要获取的基础注解的类。
例如,annoClass 为上⾯提过的 Spring 的 ComponentScan 类,对于仅注解了 ComponentScans 的类来说,以 ComponentScan.class 作为参数调⽤ getDeclaredAnnotationsByType ⽅法⼀路⾛到上⾯这个⽅法⾥,代码逻辑将会看到 ComponentScan 标注了
@Repeatable(ComponentScans.class) 注解,从⽽在 annotations 映射⾥查 ComponentScans 注解的信息,并将它转换为ComponentScan 的数组返回。
Spring 解析注解的⽅案
Spring 解析注解的核⼼是 MergedAnnotation 接⼝及相关的⼯具类。
Spring 框架重度使⽤了注解来简化开发的复杂度。对于具体的某⼀个或某⼏个注解,围绕它展开的代码散布在其逻辑链条的各处。但
是,Spring 的注解处理的特别之处就在于它定义了 MergedAnnotation 接⼝,并⽀持了基于组合注解和 AliasFor 的注解增强机制。
AliasFor 注解的解析⾮常简单,就是查看当前注解或者 targetAnnotation 注解⾥⾯相应名称的注解。在 5.2.7.RELEASE 版本中,其解析逻辑基本在 AnnotationTypeMapping#resolveAliasTarget ⽅法⾥,最终组装出来的 AnnotationTypeMapping 对象能够在获取属性值的时候显⽰处理了 AliasFor 之后的属性值。
下⾯我们展开说⼀下如何递归解析组合注解。
为了⽀持前⾯提到的组合注解,即注解上的注解的递归查,Spring 中提供了 AnnotationUtils#findAnnotation 系列⽅法来做查询,区别于AnnotationUtils#getAnnotation 的单层查。
Spring 对这个查逻辑的演化花了很多⼼思。
在最新的 Spring 5.2.7.RELEASE 版本中,这两个⽅法都对 AnnotatedElement 构造了 MergedAnnotation 实例,在最终查的时候通过不同的谓词策略来做筛选。构造 MergedAnnotation 实例的过程经由⼏个⼯⼚函数之后构造出⼀个 TypeMappedAnnotations 的实例,调⽤其上的 get ⽅法构造出实际的 MergedAnnotation 对象,这个对象就是对要查的注解递归查的结果。
相关逻辑为了定制各种策略变得⾮常复杂,我们从 4.3.8.RELEASE 版本⼊⼿,查看在复杂的定制引⼊之前,这⼀查过程核⼼逻辑的实现框架。
Annotation[] anns = DeclaredAnnotations();
for (Annotation ann : anns) {
if (ann.annotationType() == annotationType) {
return (A) ann;
}
}
for (Annotation ann : anns) {
if (!isInJavaLangAnnotationPackage(ann) && visited.add(ann)) {
A annotation = findAnnotation(ann.annotationType(), annotationType, visited);
if (annotation != null) {
return annotation;
}
}
}
⽆论后期代码演化得再复杂,其核⼼还是⼀个递归查的过程,也就是以上的代码。
1. ⾸先,获取当前的类上的注解,注意这⾥的类可以是⼀个注解类,如果此次获取的注解就包含了我们要查的注解,那么直接返回。
2. 如果没有包含,对刚才取得的注解递归的查。注意这⾥有⼀个类似于深度优先搜索的 visited 集合。这是因为有些注解可以以⾃⼰
为⽬标,导致出现递归查的⾃环。典型的例如 Java ⾃带的元注解 Retention 也被⾃⼰所注解。
3. 如果深度优先搜索穷尽之后没有得到结果,则返回空。
可以看到,上⾯的逻辑中对 Repeatable 和 Inherited 等元注解的复杂组合情况没有定制的逻辑,⽽是采⽤了⼀些默认的硬编码策略。
最新版本的 Spring 之所以变得相当复杂,有⼀部分代码量是为了解决搜索的不同策略以及跟进新版 Java 的注解特性。另⼀部分,注意到上述逻辑在获取注解时没有关⼼ AliasFor 注解的逻辑,在早期版本中这是由 AnnotationUtils 中的⼀个全局静态映射来管理的。在最新版本中,产⽣ MergedAnnotation 时将构造并维护⼀个本地的 alias 映射。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论