Spring注解编程模型

概述

多年来,Spring框架不断发展其对注解、元注解组合注解的支持。本文旨在帮助开发人员开发和使用Spring注解。

本文件的目标

本文档的主要目标包括以下内容的解释:

  • 如何在Spring中使用注解。
  • 如何开发可与Spring一起使用的注解。
  • Spring如何查找注解(即Spring的注解搜索算法如何工作)。

本文件不包含的内容

本文档不旨在解释Spring框架中特定注解的语义或配置选项。有关特定注解的详细信息,开发人员应查阅相应的Javadoc或参考手册的相关部分。

术语

元注解

元注解(Meta-Annotations) 是声明在另一个注解上的注解。因此,如果一个注解被另一个注解标注,则它是元注解 的。例如,任何被声明为文档化 的注解都通过java.lang.annotation包中的@Documented进行元标注。

less 复制代码
@Component  // @Component作为元注解
public @interface Controller { /*...*/ }

原型注解

原型注解(Stereotype Annotations) 是用于声明组件在应用程序中所起作用的注解。例如,Spring框架中的@Repository注解是任何类符合存储库角色或架构的标记(也称为数据访问对象或DAO)。

@Component是任何Spring管理组件的通用标记。任何被标注为@Component的组件都是组件扫描的候选者。类似地,任何组件被标注为元注解@Component的注解也是组件扫描的候选者。例如,@Service通过元注解@Component进行标注。

核心Spring提供了一些现成的Stereotype注解,包括但不限于:@Component@Service@Repository@Controller@RestController@Configuration@Repository@Service等是@Component的特化形式。

组合注解

组合注解(Composed Annotations) 是使用一个或多个元注解进行元标注的注解,旨在将这些元注解相关联的行为合并到一个自定义注解中。例如,一个名为@TransactionalService的注解通过Spring的@Transactional@Service注解进行元标注,是一个组合注解,它将@Transactional@Service的语义结合起来。 @TransactionalService技术上也是一个自定义Stereotype注解

less 复制代码
@Transactional
@Service
public @interface TransactionalService { /*...*/ }

注解存在性

术语*直接存在(directly present)间接存在(indirectly present)存在(present) *与Java 8中java.lang.reflect.AnnotatedElement的类级Javadoc中定义的具有相同含义。

附:Java8注解存在性介绍

在Spring中,如果某个元素上的注解在声明的其他注解中是元注解,我们认为该注解在该元素上是元存在(meta-present) 。例如,考虑前述的@TransactionalService,我们会说在任何直接使用@TransactionalService进行标注的类中,@Transactional元存在的。

属性别名和覆盖

属性别名(attribute alias) 是从一个注解属性到另一个注解属性的别名。别名集合中的属性可以互换使用,并被视为等效的。属性别名可以分类如下。

  1. 显式别名(Explicit Aliases) : 如果一个注解中的两个属性通过@AliasFor被声明为互相之间的别名,它们是显式别名

    kotlin 复制代码
    [@One]
    ├── A ─ @AliasFor("B")
    │
    └── B ─ @AliasFor("A")
  2. 隐式别名(Implicit Aliases) : 如果一个注解中的两个或多个属性通过@AliasFor声明为对元注解中同一属性的显式覆盖,它们是隐式别名

    kotlin 复制代码
    [@Two]
    └── X
        ▲
        │
    [@One]
    ├── A ─ @AliasFor("Two", "X")
    │
    └── B ─ @AliasFor("Two", "X")
  3. 传递隐式别名(Transitive Implicit Aliases) : 给定一个注解中的两个或多个属性通过@AliasFor被声明为对元注解属性的显式覆盖,如果这些属性根据传递律有效地覆盖元注解中的同一属性,它们是传递隐式别名

    kotlin 复制代码
    [@Three]
    └── Y
        ▲
        │
    [@Two]
    └── X ─ @AliasFor("Three", "Y")
        ▲
        │
    [@One]
    ├── A ─ @AliasFor("Two", "X")
    │
    └── B ─ @AliasFor("Three", "Y")

属性覆盖(attribute override) 是覆盖(或掩盖)元注解中属性的注解属性。属性覆盖可以分类如下。

  1. 隐式覆盖(Implicit Overrides) : 注解@One中存在属性A且注解@Two中存在属性A,如果@One被元注解@Two标注,则注解@One中的属性A是对注解@Two中属性A隐式覆盖 ,这是单纯基于命名约定的(即,这两个属性都命名为A)。

    css 复制代码
    [@Two]
    └── A
        ▲
        │
    [@One]
    └── A
  2. 显式覆盖(Explicit Overrides) : 如果属性A通过@AliasFor被声明为元注解中属性B的别名,则AB显式覆盖

    css 复制代码
    [@Two]
    └── B
        ▲
        │
    [@One]
    └── A ─ @AliasFor("Two", "B")
  3. 传递显式覆盖(Transitive Explicit Overrides) : 如果注解@One中的属性A是对注解@Two中的属性B的显式覆盖,而B是对注解@Three中属性C的显式覆盖,则根据传递律,属性A是对C传递显式覆盖

    kotlin 复制代码
    [@Three]
    └── C
        ▲
        │
    [@Two]
    └── B ─ @AliasFor("Three", "C")
        ▲
        │
    [@One]
    └── A ─ @AliasFor("Two", "B")

示例

许多Spring框架和Spring系列项目中的注解都使用@AliasFor注解来声明属性别名属性覆盖 。常见示例包括Spring MVC中的@RequestMapping@GetMapping@PostMapping,以及Spring Boot中的注解如@SpringBootApplication@SpringBootTest

以下部分提供代码片段以演示这些功能。

使用@AliasFor声明属性别名

Spring Framework 4.2引入了对声明和查找注解属性别名的支持。@AliasFor注解可以用于在单个注解中声明一对别名属性,或者在自定义组合注解中的一个属性与一个元注解中的属性之间声明别名。

例如,spring-test模块的@ContextConfiguration声明如下。

less 复制代码
public @interface ContextConfiguration {
​
    @AliasFor("locations")
    String[ ] value() default {};
​
    @AliasFor("value")
    String[ ] locations() default {};
​
    // ...
}

locations属性被声明为value属性的别名,反之亦然。因此,以下@ContextConfiguration的声明是等价的。

kotlin 复制代码
@ContextConfiguration("/test-config.xml")
public class MyTests { /* ... */ }
kotlin 复制代码
@ContextConfiguration(value = "/test-config.xml")
public class MyTests { /* ... */ }
kotlin 复制代码
@ContextConfiguration(locations = "/test-config.xml")
public class MyTests { /* ... */ }

同样地,组合注解 若需要覆盖来自元注解 的属性,可以通过 @AliasFor 对注解层次结构中具体哪些属性被覆盖进行细粒度控制。事实上,甚至可以为元注解的 value 属性声明别名。

例如,开发人员可以如下开发一个具有自定义属性覆盖的组合注解。

less 复制代码
@ContextConfiguration
public @interface MyTestConfig {
​
    @AliasFor(annotation = ContextConfiguration.class, attribute = "value")
    String[ ] xmlFiles();
​
    // ...
}

上述示例演示了开发人员如何实现自己的自定义组合注解,而以下内容演示了Spring本身在许多核心Spring注解中使用了此功能。

less 复制代码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(method = RequestMethod.GET)
public @interface GetMapping {
​
  /**
   * Alias for {@link RequestMapping#name}.
   */
  @AliasFor(annotation = RequestMapping.class)
  String name() default "";
​
  /**
   * Alias for {@link RequestMapping#value}.
   */
  @AliasFor(annotation = RequestMapping.class)
  String[] value() default {};
​
  /**
   * Alias for {@link RequestMapping#path}.
   */
  @AliasFor(annotation = RequestMapping.class)
  String[] path() default {};
​
  // ...
}

Spring Composed与Spring Polyglot

Spring Composed项目是供Spring Framework 4.2.1及更高版本使用的一组组合注解 。在那里您会找到诸如@Get@Post@Put@Delete等注解,这些注解为现今Spring MVC和Spring WebFlux中的@GetMapping@PostMapping@PutMapping@DeleteMapping注解提供了灵感。

欢迎查看spring-composed以获取更多示例和灵感,了解如何实现自己的自定义组合注解 ,并体验一下进一步展示@AliasFor强大功能的极客幽默和娱乐性质的Spring Polyglot

MergedAnnotations API

Spring中主要提供了三个注解提取的工具类:AnnotationUtilsAnnotatedElementUtilsMergedAnnotations。在Spring Framework 5.x及后续版本中,AnnotationUtilsAnnotatedElementUtils的核心实现已被重构为基于MergedAnnotations的体系,本文仅解析MergedAnnotations

特性 AnnotationUtils AnnotatedElementUtils MergedAnnotations
关键词 类继承链、桥接方法处理 属性覆盖、组合注解解析 注解合并、搜索策略、流式编程
搜索策略控制 ✔️(SearchStrategy
属性别名 ✔️ ✔️ ✔️
属性覆盖 ✔️ ✔️
适用版本 Spring 2.0+ Spring 4.0+ Spring 5.2+
典型场景 查找继承链中的简单注解 解析组合注解(如Spring MVC) 内部注解处理、条件装配

高层概述

kotlin 复制代码
@see org.springframework.core.annotation.MergedAnnotations

MergedAnnotations 接口设计时考虑了同时支持 Java 反射类型和 ASM 字节码读取。未来可能还会支持其他来源。

ASM 的支持边界仅限于注解的使用场景。实际的 Annotation 类必须在运行时可用,并始终通过反射检查。换句话说,注解属性可以通过 ASM 读取,但实际注解类本身不可用,例如SpringBoot启动时会扫描项目路径下所有的SpringBean,此时会通过ASM快速锁定SpringBean相关的类,但最终还是会用反射来进行解析处理。

包私有设计

MergedAnnotations 是一个包含所有面向用户的API接口。其实现类被有意保持为包私有,不对外暴露,而以接口的静态方法作为访问入口。

静态工厂方法。把静态工具方法放在接口中,这种设计比较常见。要是早些年Java就具备在接口里写静态方法的能力,那么java.util.Collections里面的工具方法可以被移动到java.util.Collectionjava.util.List等接口中。

java 复制代码
public interface MergedAnnotations extends Iterable<MergedAnnotation<Annotation>> {
  // 接口方法用于操作合并注解
  <A extends Annotation> MergedAnnotation<A> get(Class<A> annotationType);
  // 此处省略其他接口方法...
​
  // 静态方法作为入口
  static MergedAnnotations from(AnnotatedElement element) {
    return from(element, SearchStrategy.DIRECT);
  }
  // 此处省略其他静态方法...
}
​
// 具体实现类都是不对外暴露的(包私有)
final class MergedAnnotationsCollection implements MergedAnnotations {/* ... */}
final class TypeMappedAnnotations implements MergedAnnotations {/* ... */}

类型缓存

计算注解属性的合并方式是代价高昂的,因此尽可能地使用了缓存结构。主要使用的两个缓存位于 AttributeMethodsAnnotationTypeMappings 中。

AttributeMethods

kotlin 复制代码
@see org.springframework.core.annotation.AttributeMethods

AttributeMethods 类通过构建预排序的 Method 对象缓存池,实现注解属性方法的快速检索。

为什么要排序?属性覆盖等机制的实现方式会通过索引数组进行映射,所以需要每一个注解中的属性方法具备确定性的次序。

php 复制代码
final class AttributeMethods {
  /** static字段,注解到属性方法的缓存 */
  private static final Map<Class<? extends Annotation>, AttributeMethods> cache = new ConcurrentReferenceHashMap<>();
  /** 注解Class */
  @Nullable
  private final Class<? extends Annotation> annotationType;
  /** 方法数组 */
  private final Method[] attributeMethods;
  // 此处省略其他字段...
​
  /** 获取注解的所有属性方法 */
  static AttributeMethods forAnnotationType(@Nullable Class<? extends Annotation> annotationType) {
    if (annotationType == null) {
      return NONE;
    }
    // 从缓存中取
    return cache.computeIfAbsent(annotationType, AttributeMethods::compute);
  }
​
  private static AttributeMethods compute(Class<? extends Annotation> annotationType) {
    // 缓存miss时,通过反射获取方法,再存到缓存中
    // 反射获取
    Method[] methods = annotationType.getDeclaredMethods();
    int size = methods.length;
    for (int i = 0; i < methods.length; i++) {
      if (!isAttributeMethod(methods[i])) {
        methods[i] = null;
        size--;
      }
    }
    if (size == 0) {
      return NONE;
    }
    // 根据方法的name进行排序
    Arrays.sort(methods, methodComparator);
    Method[] attributeMethods = Arrays.copyOf(methods, size);
    return new AttributeMethods(annotationType, attributeMethods);
  }
  // 此处省略其他方法...
}

AnnotationTypeMappings

less 复制代码
@see @see org.springframework.core.annotation.AnnotationTypeMappings
@see @see org.springframework.core.annotation.AnnotationTypeMapping

AnnotationTypeMappings 类的核心设计目标在于确保所有元注解无论其被使用多少次,仅需执行单次扫描操作。

AnnotationTypeMappings 类负责通过广度优先搜索(BFS)递归算法爬取注解的元注解。通过 AnnotationTypeMappings 可以获取 AnnotationTypeMapping 实例,并快速将属性映射回根注解。

关键要理解 AnnotationTypeMapping 表示注解在根注解上下文中的视图。

例如,@GetMappingAnnotationTypeMappings 实例会提供访问 @RequestMappingAnnotationTypeMapping。但这与通过 @PostMapping@PutMapping 访问的 @RequestMappingAnnotationTypeMapping 不同。需要完整考虑返回到根类型的注解链。

kotlin 复制代码
final class AnnotationTypeMappings {
​
  // 缓存 ==> 根据filter和annotationType,查找mappings
  private static final Map<AnnotationFilter, Cache> cache = new ConcurrentReferenceHashMap<>();
​
  // 当前mappings构建所基于的filter
  private final AnnotationFilter filter;
​
  // mapping列表,一个注解对应一个mapping
  // 由于是通过BFS创建的,列表中是按照到根注解的距离从小到大排序的,索引0位置的就是根类型的mapping
  private final List<AnnotationTypeMapping> mappings;
​
  private AnnotationTypeMappings(AnnotationFilter filter, Class<? extends Annotation> annotationType) {
    this.filter = filter;
    this.mappings = new ArrayList<>();
    /*
    BFS依次给根注解以及每一个元注解构建Mapping并加入mappings列表中。
    addAllMappings的实现比较简单直接,而元注解的爬取是基于{@code AnnotationScanner}实现的,感兴趣可以自行去查看源码。
    */
    addAllMappings(annotationType);
    // 进行校验:别名声明是否错误、别名是否冲突
    this.mappings.forEach(AnnotationTypeMapping::afterAllMappingsSet);
  }
​
  // 此处省略其他对象方法...
​
  // 获取mapping
  AnnotationTypeMapping get(int index) {
    return this.mappings.get(index);
  }
​
  // 从缓存中获取mappings
  static AnnotationTypeMappings forAnnotationType(
      Class<? extends Annotation> annotationType, AnnotationFilter annotationFilter) {
    return cache.computeIfAbsent(annotationFilter, Cache::new).get(annotationType);
  }
​
  // 此处省略其他静态方法...
​
  /**
   * Cache created per {@link AnnotationFilter}.
   */
  private static class Cache {
​
    private final AnnotationFilter filter;
​
    private final Map<Class<? extends Annotation>, AnnotationTypeMappings> mappings;
​
    Cache(AnnotationFilter filter) {
      this.filter = filter;
      this.mappings = new ConcurrentReferenceHashMap<>();
    }
​
    /**
     * 从缓存中获取mappings,如果没有就构建mappings再返回
     */
    AnnotationTypeMappings get(Class<? extends Annotation> annotationType) {
      return this.mappings.computeIfAbsent(annotationType, this::createMappings);
    }
​
    AnnotationTypeMappings createMappings(Class<? extends Annotation> annotationType) {
      return new AnnotationTypeMappings(this.filter, annotationType);
    }
  }
}

接下来,我们来看看AnnotationTypeMapping中的核心设计。

别名映射(Alias Mappings)

处理"显式覆盖"、"传递显式覆盖"

内部数组 AnnotationTypeMapping.aliasMappings 追踪通过 @AliasFor 将属性映射到根注解的方式。对于一个确定的 AnnotationTypeMapping对象,可以快速知道实际提供值的是根注解的哪个属性。

注意 AnnotationTypeMapping 仅知道注解类型的静态元数据。它不直接知晓实际声明时使用的属性值。例如:

scss 复制代码
@interface Bar {
  String name() default "";
}
​
@Bar
@interface Foo {
  @AliasFor(annotation=Bar.class, attribute="name")
  String barName() default "";
}

BarAnnotationTypeMapping(在 Foo 上下文中)知道 "name" 属性是根注解 "barName"的别名。当实际声明注解时(如 @Foo(barName="Spring")),可以快速知道在 @Bar 元注解上调用 name()(索引 0)会映射到根注解的 barName(索引 0)。

该映射逻辑也支持多层级元注解体系,例如 @ComposedFoo 注解同样会维护映射关系。


AnnotationTypeMapping.aliasMappings 构建原理:

  1. resolveAliasedForTargets :查找所有根据@AliasFor指向当前注解的属性方法,构建映射表Map<Method, List<Method>> aliasedBy
  1. processAliases :根据aliasedBy找到AnnotationTypeMapping中每一个属性的所有别名集合,从集合中获取第一个根注解别名,递归为所有别名集合中对应的所有MappingaliasedBy进行更新,把aliasedBy中的别名属性映射为第一个根注解别名。

示例:

less 复制代码
// 示例
@Retention(RetentionPolicy.RUNTIME)
@interface Meta {
  @AliasFor("b")
  String a() default "";
​
  @AliasFor("a")
  String b() default "";
}
​
@Retention(RetentionPolicy.RUNTIME)
@Meta
@interface Composed {
  @AliasFor(annotation=Meta.class, attribute="a")
  String c() default "";
​
  @AliasFor(annotation=Meta.class, attribute="b")
  String d() default "";
}
/*
(1)Composed是根注解,现在基于Composed的上下文构建Mappings。
(2)构建Composed的AnnotationTypeMapping
(2.1)构建aliasedBy映射为
{
  "Meta.a": ["Composed.c"],
  "Meta.b": ["Composed.d"]
}
(2.2)Composed是根注解,不需要构建aliasedBy
(3)构建Meta的AnnotationTypeMapping
(3.1)构建aliasedBy映射为:
{
  "Meta.a": ["Meta.b"],
  "Meta.b": ["Meta.a"]
}
(3.2)构建aliasMappings
(3.2.1)根据aliasedBy,Meta.a遍历元注解链,收集到它的别名集合为["Meta.a", "Meta.b", "Composed.c", "Composed.d"]
(3.2.2)根据别名集合,Meta.a的第一个根注解别名为"Composed.c"
(3.2.3)所以,Meta的AnnotationTypeMapping的aliasMappings为[0, 0],含义是
{
  "Meta.a": "Composed.c",
  "Meta.b": "Composed.c"
}
*/
​
@Composed(c = "bar")
class Bar {}
​
// 根据aliasMappings映射,快速获取把Composed.c的值映射给Meta.a
String a = MergedAnnotations.from(Bar.class, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY)
        .get(Meta.class)
        .synthesize()
        .a();
assertEquals("bar", a);
基于约定的映射(Convention-based Mappings)

处理"隐式覆盖"

为向下兼容,需要继续维护那些基于约定的隐式映射。这些映射在未使用@AliasFor注解时生效,适用于所有属性,但value属性除外。

例如下面的例子中,存在从 Foo.nameBar.name 的隐式映射:

scss 复制代码
@interface Bar {
  String name() default "";
}
​
@Bar
@interface Foo {
  String name() default "";
}

内部数组 AnnotationTypeMapping.conventionMappings 用于处理基于约定的映射,其工作原理与 aliasMappings 完全相同。

java 复制代码
final class AnnotationTypeMapping {
  // 根注解的Mapping
  private final AnnotationTypeMapping root;
  // 所有属性方法
  private final AttributeMethods attributes;
  // 处理基于约定的映射
  private final int[] conventionMappings;
  
  // 此处省略其他代码...
​
  AnnotationTypeMapping(@Nullable AnnotationTypeMapping source,
      Class<? extends Annotation> annotationType, @Nullable Annotation annotation) {
    // 构建AnnotationTypeMapping
    // 此处省略其他代码...
    addConventionMappings();
    // 此处省略其他代码...
  }
​
  private void addConventionMappings() {
    if (this.distance == 0) {
      return;
    }
    AttributeMethods rootAttributes = this.root.getAttributes();
    int[] mappings = this.conventionMappings;
    for (int i = 0; i < mappings.length; i++) {
      String name = this.attributes.get(i).getName();
      MirrorSet mirrors = getMirrorSets().getAssigned(i);
      // 获取根注解中属性方法名称相同的属性方法索引
      int mapped = rootAttributes.indexOf(name);
      if (!MergedAnnotation.VALUE.equals(name) && mapped != -1) {
        // 处理隐式属性覆盖
        mappings[i] = mapped;
        if (mirrors != null) {
          // 处理镜像集,镜像集的介绍见下文
          for (int j = 0; j < mirrors.size(); j++) {
            mappings[mirrors.getAttributeIndex(j)] = mapped;
          }
        }
      }
    }
  }
​
  // 根据属性方法索引,查找根注解的属性方法索引
  int getConventionMapping(int attributeIndex) {
    return this.conventionMappings[attributeIndex];
  }
​
  // 省略其他代码...
}
注解值映射(Annotation Value Mappings)

一些元注解属性值实际上可通过类型信息解析。例如,@PostMapping 上的 @RequestMapping.method() 属性始终返回 RequestMethod.POST

AnnotationTypeMapping.annotationValueMappingsAnnotationTypeMapping.annotationValueSource 数组用于解析这类情况。

在上例中,@RequestMappingAnnotationTypeMapping 会指向 @PostMapping 源和 method 属性。该值将直接从声明的元注解中读取:

less 复制代码
@RequestMapping(method = RequestMethod.POST)
@interface PostMapping {
   // ...
}
镜像集(Mirror Sets)

处理"显式别名"、"隐式别名"、"传递隐式别名"

@AliasFor 可用于声明表示相同值的多个属性。最简单形式如下:

less 复制代码
@interface Foo {
  @AliasFor("value")
  String name() default "";
  
  @AliasFor("name")
  String value() default "";
}

AnnotationTypeMapping 将这些称为"镜像(mirror)"属性。当两个或多个属性声明了相同元注解属性的 @AliasFor 时,也会形成镜像属性。

虽然可以从类型映射中检测镜像属性,但在获得实际属性值前无法确定应使用哪个。同时我们无法完全验证用户是否把属性值配置错误。

例如, @Foo("spring")@Foo(name = "spring") 是合法的,但 @Foo(value = "framework", name = "spring") 不合法。

getMirrorSets().resolve(...) 方法用于确定声明的注解中实际使用的镜像属性。它返回映射到真实属性的数组,或在用户对别名属性值配置错误时抛出异常。

例如,对 @Foo("spring") 调用 resolve 会返回 [1,1],对 @Foo(name = "spring") 返回 [0,0]。数组索引表示被请求的属性,值表示应使用的属性。

@Foo("spring") 案例中,查找 Foo.name() 对应索引 0 返回值 1Foo.value() 对应索引 1 返回值 1。即 value 属性值始终是最终访问的数值。

和其他设计一样,镜像集的设计目标是通过尽可能预计算,确保实际解析过程快速完成。


Mirror Sets结构示例:

arduino 复制代码
final class AnnotationTypeMapping {
  // Mapping的镜像集
  private final MirrorSets mirrorSets;
  // 注解的所有属性方法
  private final AttributeMethods attributes;
​
  // 此处省略其他代码...
​
  class MirrorSets {
    // 所有MirrorSet的集合
    private MirrorSet[] mirrorSets;
    // 属性方法索引到MirrorSet的映射
    private final MirrorSet[] assigned;
​
    // 此处省略其他方法及构造器...
​
    class MirrorSet {
      // indexs数组被预分配了attributes.size()大小,这个字段存储indexs数组的实际被使用的大小
      private int size;
      // 互为别名的属性方法的索引集合
      private final int[] indexes = new int[attributes.size()];
​
      // 此处省略其他方法及构造器...
    }
  }
}

在构建每一个AnnotationTypeMapping的过程中会根据元注解链,尝试逐个更新自己以及其他AnnotationTypeMapingmirrorSets,以处理隐式别名、传递隐式别名。

接口实现

最后一部分是为用户提供 MergedAnnotationsMergedAnnotation 接口的具体实现,主要类是 TypeMappedAnnotationsTypeMappedAnnotation

TypeMappedAnnotations

kotlin 复制代码
@see org.springframework.core.annotation.TypeMappedAnnotations

TypeMappedAnnotations 负责以统一方式暴露 AnnotatedElement 的声明注解。该类包含用于单个注解操作的 scan 方法,以及处理所有注解的 stream 方法。

注解和元注解都需要暴露。声明注解通过 AnnotationScanner 发现,然后用AnnotationTypeMappings 添加元注解。

内部类 AggregatesSpliterator 包含复杂的排序规则。

AnnotationScanner

kotlin 复制代码
@see org.springframework.core.annotation.AnnotationsScanner

AnnotationsScanner 用于从 AnnotatedElement 当中查找声明注解。它支持多种搜索策略,并处理继承方法的复杂性。

扫描器实现中有几个值得注意的特殊点:

  1. 传入 AnnotationsProcessor 回调的数组可能包含 null 元素(性能优化,避免为了消除null元素而复制数组)
  2. AnnotationsProcessor 的扫描过程可以提前退出(性能优化,在扫描到结果后提前退出,避免全量扫描)

由于 AnnotationsScanner 是包私有类,这些特性不会暴露给最终用户。

TypeMappedAnnotation

kotlin 复制代码
@see org.springframework.core.annotation.TypeMappedAnnotation

TypeMappedAnnotation 通过组合 AnnotationTypeMapping 和实际注解源来实现 MergedAnnotation。注解源可以是真实声明的Java 注解或通过 ASM 读取的值。valueExtractor函数为支持这两种不同来源提供了必要的间接抽象层。对于真实的注解,可以通过ReflectionUtils::invokeMethod方法进行调用。

该类的实现大部分较为直接,但 adapt... 系列方法值得注意。它们用于将提取值适配到不同类型来消费(例如将 ASM 读取的 String 转为类,或读取嵌套注解如 @ComponentScan.includeFilters)。

TypeMappedAnnotations.NONE

对没有注解的类调用 MergedAnnotations 时,会返回共享的 TypeMappedAnnotations 空实例。这避免了重复创建新实例,减少垃圾回收压力。

享元设计模式

MissingMergedAnnotation

kotlin 复制代码
@see org.springframework.core.annotation.MissingMergedAnnotation

MergedAnnotations API 设计尽可能避免返回 null。当注解不存在时,返回 MergedAnnotation.missing()。包私有类 MissingMergedAnnotation 实现了这种"null object"模式。

FAQ

1) @AliasFor能否用于对@Qualifier中的value属性进行别名?

不能,@Qualifier中的value属性不能被@AliasFor别名化。原因是对qualifiers中的value属性的特殊处理是在@AliasFor发明多年之前引入的,并且这种特殊处理仍然有效。

2) @AliasFor能否用于对@Component和原型注解中的value属性进行别名?

简短的回答是:不能,直到Spring Framework 6.1

在Spring Framework 6.1之前,原型注解中的value属性(例如@Component@Repository@Controller和任何自定义原型注解)不能被@AliasFor影响。其理由与上文解释的@Qualifier类似。然而,Spring Framework 6.1引入了通过@AliasFor@Component中的value属性进行别名化的完整支持。例如,请参阅@ControllerAdvicename属性的源代码声明。

附:Java8注解存在性介绍

css 复制代码
@see java.lang.reflect.AnnotatedElement

在编译后能够在class字节码中保留RuntimeVisibleAnnotationsRuntimeVisibleParameterAnnotationsRuntimeVisibleTypeAnnotations 属性,是注解"存在"的必要条件。

从注解开发者的角度来解释:本文提到的注解都是被@Retention(RetentionPolicy.RUNTIME)标注的,且用于标注在类、方法、字段、方法出入参、类或字段或方法出入参的泛型类型上(例如:标注在方法体内的局部变量上是不行的)。

从注解使用者的角度来解释:本文提到的注解都是能够通过反射获取到的。

术语解释

  • Directly Present:注解直接存在于目标的字节码属性中。
  • Indirectly Present :当注解 A 被 @Repeatable(A.Container.class) 标记时,若容器注解 A.Container 直接存在且包含 A 注解,则每个 A 注解被视为间接存在。
  • Present :注解直接存在,或通过 @Inherited 从父类继承的直接存在的注解(仅对类有效)。
  • Associated:直接存在、间接存在的注解,以及通过继承链传递的直接或间接存在的注解。

关系图:

复制代码
 ┌──────────────────────────────────────┐
 │             Associated               │
 │  ┌─────────────────────────────────┐ │
 │  │            Present              │ │
 │  │  ┌───────────────────────────┐  │ │
 │  │  │       Directly Present    │  │ │
 │  │  └───────────────────────────┘  │ │
 │  │                                 │ │
 │  │   Inherited Directly Present    │ │
 │  └─────────────────────────────────┘ │
 │                                      │
 │  ┌─────────────────────────────────┐ │
 │  │       Indirectly Present        │ │
 │  └─────────────────────────────────┘ │
 └──────────────────────────────────────┘

代码示例

  • Directly Present
less 复制代码
@interface MyAnnotation {}

@MyAnnotation
class Bar {}

myAnno = Bar.class.getDeclaredAnnatation(MyAnnotation.class);
assertNotNull(myAnno);
  • Indirectly Present
scss 复制代码
@Repeatable(MyRepeatableContainer.class)
@interface MyRepeatable {
  String value();
}

@interface MyRepeatableContainer {
    MyRepeatable[] value();
}

@MyRepeatable("fox1")
@MyRepeatable("fox2")
class Fox {}

myRepeatableArr = Bar.class.getDeclaredAnnatationsByType(MyRepeatable.class);
assertEquals(2, myRepeatableArr.length);
  • Present
less 复制代码
@Inherited
@interface MyInheritedAnnotation {}

@MyInheritedAnnotation
class Parent {}

class Child extends Parent {}

myInheritedAnno = Child.class.getAnnotation(MyInheritedAnnotation.class);
assertNotNull(myAnno);
  • Associated
less 复制代码
@Inherited
@Repeatable(MyInheritedRepeatableContainer.class)
@interface MyInheritedRepeatable {
String value();
}

@Inherited
@interface MyInheritedRepeatableContainer {
 MyInheritedRepeatable[] value();
}

@MyInheritedRepeatable("foo1")
@MyInheritedRepeatable("foo2")
class Parent {}

class Child extends Parent {}

myAnnoArr = Child.class.getAnnotationsByType(MyInheritedRepeatable.class);
assertEquals(2, myAnnoArr.length);

Java注解方法支持行为对比表

Method Directly Present Indirectly Present Present Associated
getAnnotation(Class<T>)
getAnnotations()
getAnnotationsByType(Class<T>)
getDeclaredAnnotation(Class<T>)
getDeclaredAnnotations()
getDeclaredAnnotationsByType(Class<T>)

API规律:名称包含Declared的方法无法获取Inherited传递到子类的注解,只有名称包含ByType的方法才可以获取间接存在(被@Repeatable标注)的注解。

附:桥接方法简介

桥接方法(bridge method)是 JDK 1.5 引入泛型后,由编译器自动生成的方法。它会被ACC_BRIDGEACC_SYNTHETIC修饰。

作用:使Java的泛型方法生成的字节码和 1.5 版本前的字节码相兼容,解决泛型类型擦除导致的方法签名不一致问题,确保多态性。

示例:java.util.function.Consumer实现类的字节码中通常包含如下桥接方法。

java 复制代码
public synthetic bridge accept(Ljava/lang/Object;)V

附:AnnotationUtils

kotlin 复制代码
@see org.springframework.core.annotation.AnnotationUtils
  • 通用工具类,处理注解、元注解、桥接方法和超类方法。
  • 不支持属性覆盖。
  • 关键特性:findAnnotation()会搜索类/方法的继承链(父类、接口)。

AnnotationUtils关键APIfindAnnotationgetAnnotation的特性对比表:

特性 findAnnotation getAnnotation
对别名的支持 ✔️ ✔️
对属性覆盖的支持
直接存在的注解 ✔️ ✔️
@Inherited的类继承 ✔️ ✔️
@Inherited的类继承 ✔️
实现的接口类注解 ✔️
一层元注解 ✔️ ✔️
多层元注解 ✔️
继承的方法注解 ✔️
实现的接口方法注解 ✔️

使用示例

  • 显式别名
less 复制代码
@Retention(RetentionPolicy.RUNTIME)
@interface One {
    @AliasFor("b")
    String a() default "default";

    @AliasFor("a")
    String b() default "default";
}

@One(a = "TestExplicitAliases")
class TestClass {}

One annotation = AnnotationUtils.getAnnotation(TestClass.class, One.class);
assertNotNull(annotation);
assertEquals("TestExplicitAliases", annotation.a());
assertEquals("TestExplicitAliases", annotation.b());	// 属性b的值与属性a相同
  • 实现接口方法
less 复制代码
@Retention(RetentionPolicy.RUNTIME)
@interface MyAnnotation {}

@Retention(RetentionPolicy.RUNTIME)
@interface MethodAnnotation {
    String value();
}

// 测试桥接方法
interface Generic<T> {
    @MyAnnotation
    @MethodAnnotation("interface-method")
    void process(T t);
}

class StringProcessor implements Generic<String> {
    @Override
    @MethodAnnotation("implement-method")
    public void process(String s) {}
}

Method bridgeMethod = StringProcessor.class.getMethod("process", Object.class);
MyAnnotation ann1 = AnnotationUtils.findAnnotation(bridgeMethod, MyAnnotation.class);
assertNotNull(ann1);	// 查找接口方法上的注解

MethodAnnotation ann2 = AnnotationUtils.findAnnotation(bridgeMethod, MethodAnnotation.class);
assertNotNull(ann2);
assertEquals("implement-method", ann2.value());   // 优先获取当前实现类上的注解

附:AnnotatedElementUtils

kotlin 复制代码
@see org.springframework.core.annotation.AnnotatedElementUtils
  • 通用工具类,用于在 AnnotatedElements 上查找注解、元注解和可重复注解(@Repeatable)。

  • 支持注解属性覆盖。 如果不需要支持注解属性覆盖,则考虑使用 AnnotationUtils

  • 提供了诸多实用方法,如:

    • getMergedAnnotationAttributes()
    • getMergedAnnotation()
    • getAllMergedAnnotations()
    • getMergedRepeatableAnnotations()
    • findMergedAnnotationAttributes()
    • findMergedAnnotation()
    • findAllMergedAnnotations()
    • findMergedRepeatableAnnotations()
  • get开头的方法与以find开头的方法的特性对比表:

    特性 find semantics get semantics
    对别名的支持 ✔️ ✔️
    对属性覆盖的支持 ✔️ ✔️
    直接存在的注解 ✔️ ✔️
    @Inherited的类继承 ✔️ ✔️
    @Inherited的类继承 ✔️
    实现的接口类注解 ✔️
    多层元注解 ✔️ ✔️
    继承的方法注解 ✔️
    实现的接口方法注解 ✔️

使用示例

less 复制代码
@Retention(RetentionPolicy.RUNTIME)
@interface Three {
  String y() default "default";
}

@Retention(RetentionPolicy.RUNTIME)
@Three	// 元注解
@interface Two {
  @AliasFor(annotation = Three.class, attribute = "y")
  String x() default "default";
}

@Retention(RetentionPolicy.RUNTIME)
@Two	// 元注解
@interface One {
  @AliasFor(annotation = Two.class, attribute = "x")
  String a() default "default";

  @AliasFor(annotation = Three.class, attribute = "y")
  String b() default "default";
}

@One(a = "test")
class TestClass {}

// One的a、b属性:传递隐式别名
One one = AnnotatedElementUtils.getMergedAnnotation(TestClass.class, One.class);
assertNotNull(oneAttributes);
assertEquals("test", one.a());
assertEquals("test", one.b());

// One的a属性对Two的x属性:显式覆盖
AnnotationAttributes twoAttributes = AnnotatedElementUtils.getMergedAnnotationAttributes(TestClass.class, Two.class);
assertNotNull(twoAttributes);
assertEquals("test", twoAttributes.getString("x"));

// One的a属性对Three的y属性:传递显式覆盖
Three three;
three = AnnotatedElementUtils.getMergedAnnotation(TestClass.class, Three.class);
assertNotNull(three); // 支持多层元注解
assertEquals("test", three.y());
three = AnnotatedElementUtils.findMergedAnnotation(TestClass.class, Three.class);
assertNotNull(three);
assertEquals("test", three.y());

附:测试代码库

spring-annotation-testgitee.com/csuyth/spri...

kotlin 复制代码
spring-annotation-test
├── package org.example.javatest.annotation (Java8注解存在性测试)
└── package org.example.springtest.annotation (Spring注解API测试)

参考资料

相关推荐
Query*7 分钟前
Java 设计模式——代理模式:从静态代理到 Spring AOP 最优实现
java·设计模式·代理模式
梵得儿SHI9 分钟前
Java 反射机制深度解析:从对象创建到私有成员操作
java·开发语言·class对象·java反射机制·操作类成员·三大典型·反射的核心api
JAVA学习通13 分钟前
Spring AI 核心概念
java·人工智能·spring·springai
望获linux15 分钟前
【实时Linux实战系列】实时 Linux 在边缘计算网关中的应用
java·linux·服务器·前端·数据库·操作系统
绝无仅有23 分钟前
面试真实经历某商银行大厂数据库MYSQL问题和答案总结(二)
后端·面试·github
绝无仅有25 分钟前
通过编写修复脚本修复 Docker 启动失败(二)
后端·面试·github
..Cherry..27 分钟前
【java】jvm
java·开发语言·jvm
老K的Java兵器库36 分钟前
并发集合踩坑现场:ConcurrentHashMap size() 阻塞、HashSet 并发 add 丢数据、Queue 伪共享
java·后端·spring
冷冷的菜哥1 小时前
go邮件发送——附件与图片显示
开发语言·后端·golang·邮件发送·smtp发送邮件
向葭奔赴♡1 小时前
Spring Boot 分模块:从数据库到前端接口
数据库·spring boot·后端