用什么软件做网站模板,四川石油天然气建设工程有限责任公司网站,平面设计外包平台,如何查询百度收录注解#xff08; annontation #xff09;是 Java 1.5 之后引入的一个为程序添加元数据的功能。注解本身并不是魔法#xff0c;只是在代码里添加了描述代码自身的信息#xff0c;至于如何理解和使用这些信息#xff0c;则需要专门的解析代码来负责。本文首先介绍注解的基本…注解 annontation 是 Java 1.5 之后引入的一个为程序添加元数据的功能。注解本身并不是魔法只是在代码里添加了描述代码自身的信息至于如何理解和使用这些信息则需要专门的解析代码来负责。本文首先介绍注解的基本知识包括注解的分类和运用时的领域知识。随后给出一个通过的在运行时解析注解的框架代码介绍处理注解的一般思路。最后通过现实世界里使用注解的例子来加深对注解的实用性方面的认识。注解的基本知识注解作为程序中的元数据其本身的性质也被其上的注解所描述。刚刚我们提到理解和使用注解信息需要专门的解析代码。其中Java 的编译器和虚拟机也包含解析注解信息的逻辑而它们判断一个注解的性质就是依赖注解之上的元注解。能够注解一个注解的注解就是元注解Java 本身能够识别的元注解有以下几个。RetentionRetention 注解的相关定义如下Documented
Retention(RetentionPolicy.RUNTIME)
Target(ElementType.ANNOTATION_TYPE)
public interface Retention {RetentionPolicy value();
}public enum RetentionPolicy {SOURCE,CLASS,RUNTIME
}首先我们看到它自己也被几个元注解包括自身所注解因此在注解的源头有一个类似于自举的概念最终触发自举的是编译器和源代码中的先验知识。再看到 Retention 注解的值是一个注解保留性质的枚举包括三种情况。SOURCE 表示注解信息仅在编译时保留在编译之后就被丢弃这样的注解为代码的编译提供原信息。例如常用的 Override 注解就提示 Java 编译器进行重写方法的检查。CLASS 表示注解信息保留在字节码中但在运行时不可见。这是注解的默认行为如果定义注解时没有使用 Retention 注解显式表明保留性质默认的保留性质就是这个。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();
}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 注解也能注解在字段、构造器、方法参数和注解等多种位置上。InheritedInherited 主要用来标注注解在类继承关系之间的传递关系。它本身不携带自定义信息仅作为一个布尔信息存在即是或者不是 Inherited 的注解。标注 Inherited 元注解的注解标注在某个类型上时其子类也默认视为标注此注解。或者换个方向说获取某个类的注解时会递归的搜索其父类的注解并获取其中标注 Inherited 元注解的注解。注意标注 Inherited 元注解的注解在子类上也标注时子类上的注解优先级最高。技术上说可以通过 getAnnotations 和 getDeclaredAnnotations 的区别来获取确切标注在当前类型上的注解和按照上面描述的方法查找的注解。另一个值得强调的是这种继承仅发生在类的继承上实现接口并不会导致标注 Inherited 元注解的注解的传递。值得注意的是注解本身是不能继承的。为了实现类似继承的效果开发者们从基于原型的继承找到灵感采用本节后续将讲到的组合注解技术来达到注解继承的目的。RepeatableRepeatable 注解在 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 为被注解的元素定义了一个 AnnotatedElement 的接口通过这一接口的方法可以在运行时取得被注解元素之上的注解。该接口的实现类是运行时通过反射拿到的元素里面能够被注解的类。我们先看到这一接口提供的方法。public interface AnnotatedElement {T extends Annotation T getAnnotation(ClassT annotationClass);Annotation[] getAnnotations();T extends Annotation T getDeclaredAnnotation(ClassT annotationClass);Annotation[] getDeclaredAnnotations();T extends Annotation T[] getAnnotationsByType(ClassT annotationClass);T extends Annotation T[] getDeclaredAnnotationsByType(ClassT 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(MapClass? extends Annotation, Annotation annotations,ClassA annoClass
) {Repeatable repeatable annoClass.getDeclaredAnnotation(Repeatable.class);if (repeatable null)return null; // Not repeatable - no indirectly present annotationsClass? extends Annotation containerClass repeatable.value();Annotation container annotations.get(containerClass);if (container null)return null;// Unpack containerA[] 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 clazz.getDeclaredAnnotations();
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;}}
}无论后期代码演化得再复杂其核心还是一个递归查找的过程也就是以上的代码。首先获取当前的类上的注解注意这里的类可以是一个注解类如果此次获取的注解就包含了我们要查找的注解那么直接返回。如果没有包含对刚才取得的注解递归的查找。注意这里有一个类似于深度优先搜索的 visited 集合。这是因为有些注解可以以自己为目标导致出现递归查找的自环。典型的例如 Java 自带的元注解 Retention 也被自己所注解。如果深度优先搜索穷尽之后没有得到结果则返回空。可以看到上面的逻辑中对 Repeatable 和 Inherited 等元注解的复杂组合情况没有定制的逻辑而是采用了一些默认的硬编码策略。最新版本的 Spring 之所以变得相当复杂有一部分代码量是为了解决搜索的不同策略以及跟进新版 Java 的注解特性。另一部分注意到上述逻辑在获取注解时没有关心 AliasFor 注解的逻辑在早期版本中这是由 AnnotationUtils 中的一个全局静态映射来管理的。在最新版本中产生 MergedAnnotation 时将构造并维护一个本地的 alias 映射。现实世界的注解解析上一节介绍了处理注解的两个通用套路背后的思想是基础的注解信息获取和递归的注解信息获取。本节我们将从现实世界的注解解析入手介绍实际项目里面特定的注解是如何被解析的。FlinkRpcTimeoutFlink 采用类似 RMI 的方式来进行远程调用为了避免无限阻塞方法调用时可以传递一个超时参数。本地拦截远端调用的动作时从方法的签名中反射取得标注 RpcTimeout 的参数将它作为超时参数传递到实际的方法调用过程中以在超过限定时间时返回超时异常而非阻塞等待远端调用的返回。取得标注 RpcTimeout 的参数的逻辑代码展开如下final Annotation[][] parameterAnnotations method.getParameterAnnotations();for (int i 0; i parameterAnnotations.length; i) {for (Annotation annotation : parameterAnnotations[i]) {if (annotation.annotationType().equals(RpcTimeout.class)) {if (args[i] instanceof Time) {return (Time) args[i];} else {throw new RuntimeException(/* ... */)}}}
}return defaultTimeout;可以看到是针对先验知识能得知的可能出现该注解的位置进行遍历获取。其实所有的注解解析代码都遵循这样的模式这也是最基础的模式。JUnit 4TestJUnit 4 测试框架的用户最熟悉的就是 Test 注解了。不同于上一节提到的基础解析和递归解析JUnit 4 的 Test 注解有一个特殊的场景需要支持即在获取当前类的所有待测试方法时获取到父类中的 Test 标注的方法。这是因为我们常常把相似的测试的配置和基础测试方法抽成抽象基类在根据不同的实现场景实现不同的测试子类。虽然类似的功能可以用 Parameterized Runner 和 Parameter 注解来实现但是 Parameter 的方案只能支持参数化字段如果测试方法是有和没有的区别而不是参数的不同子类是比使用 Parameter 向量并加入 Enable 开关更好的解决方案。总之JUnit 4 支持查找父类中标注 Test 的其他方法此逻辑实现如下。// TestClass#scanAnnotatedMembers
for (Class? eachClass : getSuperClasses(clazz)) {for (Method eachMethod : MethodSorter.getDeclaredMethods(eachClass)) {addToAnnotationLists(new FrameworkMethod(eachMethod), methodsForAnnotations);}// ensuring fields are sorted to make sure that entries are inserted// and read from fieldForAnnotations in a deterministic orderfor (Field eachField : getSortedDeclaredFields(eachClass)) {addToAnnotationLists(new FrameworkField(eachField), fieldsForAnnotations);}
}// TestClass#addToAnnotationLists
for (Annotation each : member.getAnnotations()) {Class? extends Annotation type each.annotationType();ListT members getAnnotatedMembers(map, type, true);T memberToAdd member.handlePossibleBridgeMethod(members);if (memberToAdd null) {return;}if (runsTopToBottom(type)) {members.add(0, memberToAdd);} else {members.add(memberToAdd);}
}其实也很简单在初始化 TestClass 时遍历测试的候选类及其父类的所有方法和字段将它们的注解信息存在一个注解类型到被注解对象的列表的映射中。后续需要查找的时候从该映射查找即可查找到标注对应注解的所有方法或字段。RunWith另一个常见的注解是 RunWith 注解用于标注运行测试时采用自定义的 Runner 实现。其代码如下for (Class? currentTestClass testClass; currentTestClass ! null;currentTestClass getEnclosingClassForNonStaticMemberClass(currentTestClass)) {RunWith annotation currentTestClass.getAnnotation(RunWith.class);if (annotation ! null) {return buildRunner(annotation.value(), testClass);}
}可以看到是从内到外层层查找的形式。注意这里没有去查找父类的 RunWith 注解这是由于 RunWith 注解本身被 Inherited 所标注调用 Java 提供的基础方法获取类的注解时已经做了相应的处理。SpringSpringBootApplicationSpringBootApplication 可以说是最好的解释 Spring 中重度使用组合注解的例子了。对于这一注解的解析我们甚至不需要或者说不能列举出任何解析代码因为 SpringBootApplication 从来没有作为它自己被解析。该注解的定义如下Target(ElementType.TYPE)
Retention(RetentionPolicy.RUNTIME)
Documented
Inherited
SpringBootConfiguration
EnableAutoConfiguration
ComponentScan(excludeFilters { Filter(type FilterType.CUSTOM, classes TypeExcludeFilter.class),Filter(type FilterType.CUSTOM, classes AutoConfigurationExcludeFilter.class) })
public interface SpringBootApplication {AliasFor(annotation EnableAutoConfiguration.class)Class?[] exclude() default {};AliasFor(annotation EnableAutoConfiguration.class)String[] excludeName() default {};AliasFor(annotation ComponentScan.class, attribute basePackages)String[] scanBasePackages() default {};AliasFor(annotation ComponentScan.class, attribute basePackageClasses)Class?[] scanBasePackageClasses() default {};AliasFor(annotation ComponentScan.class, attribute nameGenerator)Class? extends BeanNameGenerator nameGenerator() default BeanNameGenerator.class;AliasFor(annotation Configuration.class)boolean proxyBeanMethods() default true;
}这里有两件事情值得关注分别对应介绍 Spring 的注解解析框架的时候指出的 Spring 的两个关键的增强实际使用 SpringBootApplication 时Spring 框架的解析代码是通过 findAnnotation 查找其组合的注解来实现具体功能的。SpringBootApplication 通过 AliasFor 支持用户在使用该注解时覆盖其所组合的注解的属性。从这里我们也看出组合注解仅仅是一种形式上相关联的组合与任一形式的继承不同不会以某种形式继承属性。AutowiredAutowired 可以说是 Spring 框架中使用最为广泛的注解之一了它和 Value 注解以及 JSR-330 的 Inject 注解一起组成了注入 Bean 的核心手段。Autowired 的处理逻辑在 AutowiredAnnotationBeanPostProcessor 中即 Bean 被创造和加载之后的一个后处理逻辑或者成为装饰逻辑。其中涉及到 Autowired 等注解的地方主要是筛选出需要为目标注入 Bean 的候选。首先在初始化的时候会将对应的 Autowired 系列注解保存到 autowiredAnnotationTypes 集合字段中。随后当 Bean 处理框架调用后处理逻辑时调用后处理器的 findAutowiringMetadata 方法通过标记型注解找到需要 Autowired 的候选。整个过程通过反射将被 Autowired 注解的对象及 Autowired 注解中持有的是否必须 required 的信息保存到 InjectElement 中。再之后对获取到的所有 InjectElement 调用 inject 方法进行注入。根据不同的被注入对象注入的逻辑有所不同。例如对于字段的注入由 AutowiredFieldElement 对象处理从 BeanFactory 中根据依赖关系初始化 Bean 并将 Bean 赋值给字段。这一套逻辑支持了 Bean 注入最常用的字段注入的功能以及运行配置方法的功能。Autowired 注解还能被用在参数和构造函数上其中参数上的标注目前仅用于在 JUnit Jupiter 框架测试时使用而构造函数的标注广泛替代了直接标注字段的用法其代码路径存在于 AbstractAutowireCapableBeanFactory 创建 Bean 实例的时候。从结果来说标注在构造函数的 Autowired 能够将参数对应类型的 Bean 作为构造函数的实参调用构造函数以构造出对象。