Skip to main content

Spring的组合注解是什么?

作者:程序员马丁

在线博客:https://open8gu.com

note

大话面试,技术同学面试必备的八股文小册,以精彩回答应对深度问题,助力你在面试中拿个offer。

回答话术

Java 原生的注解不支持继承,而 Spring 通过一套组合注解机制实现了类似的功能

在 Spring 中,如果将 A 注解作为 B 注解的元注解(即 A 注解注解在 B 注解的类上),则在添加 B 注解时,等同于同时添加了 A 注解(Spring 称其为 meta-presen,即以元注解的形式存在)。

比如:

@Service 注解使用了 @Component 注解作为元注解,所以被 @Service注解的 bean 等同于被 @Component注解:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component // 使用 @Component 注解元注解
public @interface Service {
// ...
}

我们熟悉的很多常用注解都是通过某个基础注解组合而来的,比如:

  • @Controler@Service@Repository 注解都基于 @Component 扩展而来。
  • @PostMappingGetMappingPutMappingDeleteMapping注解基于 RequestMapping 扩展而来。
  • @RestContorller 注解基于 @ResponseBody@Contorller 注解组合而来。

问题详解

想更深入技术底层的同学,可以参考 Spring 官方文档:

1. 实现原理

Spring 中通过 AnnotationUtils 或者 AnnotatedElemenUtilsgetMergedAnnotation/findMergedAnnotation 相关 API 来获得组合注解。

进入该方法底层,则会发现其实是根据一个叫做 MergedAnnotations 类实现的,它是一个接口,即我们一般说的“组合注解”(Composed Annotation)。

这里涉及到四个类:

  • AnnotationTypeMapping注解类型映射,表示一个元注解,元注解会持有它的上级注解的引用,从而形成树结构,简而言之,就是可以知道自己被用在哪个注解上;
  • AnnotationTypeMappings注解类型映射聚合,表示一个注解和它的全部元注解;
  • TypeMappedAnnotations组合注解聚合,表示一个 AnnotatedElement上的全部注解;
  • TypeMappedAnnotation组合注解,通过 AnnotationTypeMapping 生成,通过其获取属性值是,会根据对应的 AnnotationTypeMapping 与关联的上级 AnnotationTypeMapping 计算真实的属性。

image.png

举个例子,当我们调用 AnnotatedElementUtils.getMergedAnnotation(Foo.class, Meta2.class) 方法时,Spring 做了这些事情:

  1. 为搜索元素构建组合注解聚合:为指定要搜索的 AnnotatedElement,比如图例中的 Foo.class 创建一个 MergedAnnotations (实现类实际为 TypeMappedAnnotations);
  2. 搜索根注解MergedAnnotations 通过 AnnotationScanner (这是 Spring 内部的一个注解搜索器)依次扫描 Foo.class 上根注解 Annotation2Annotation1
  3. 为根注解构建元注解聚合:为搜索到的 Annotation2Annotation1构建一个 AnnotationTypeMappings
    1. 按广度优先遍历注解的层级结构,搜集对应的根注解上的全部元注解,然后将其包装为 AnnotationTypeMapping
    2. 每一个元注解对应的 AnnotationTypeMapping 都持有其上级注解对应的 AnnotationTypeMapping 的引用,从而最终形成一个以根注解作为根节点的树结构。 比如,以 Annotation1 来说,Meta1 会持有 Annotation1 的引用,而 SuperMeta3SuperMeta4 会持有 Meta2 的引用,它们最终形成了一个 Annotation1为根节点的树结构;
  4. 从元注解聚合中查找注解:遍历 AnnotationTypeMappings 中收集到的每一个注解,直到找到 Meta2 对应的 AnnotationTypeMapping 为止;
  5. 合成注解:将根据 Meta2AnnotationTypeMapping 创建一个 MergedAnnotation(实现类实际为 TypeMappedAnnotation),然后调用 MergedAnnotation.synthesize 方法,通过动态代理为目标类型的注解创建一个代理对象。

碍于篇幅,此处仅做必要的介绍,足够理解后文的层级搜索和属性增强机制即可,如果想要更进一步了解,还是推荐在看过官方文档后直接阅读源码。

2. 层级结构搜索

我们知道,Java 默认的注解只有加了 @Inherited 元注解以后,才支持从接口或者父类传递到实现类上,除此之外没有其他的办法来传递注解。但是,在 Spring 中,很多注解类并没有加 @Inherited 元注解,但是这些注解在放到父类或父接口时依然可以生效。

比如我们在父类上添加 @Component 注解,那么子类同样会被 Spring 容器作为 Bean 管理:

@Component
publc class Super {}

public class Child extends Supper {}

此外,有时候我们也可以注意到,在接口的抽象方法加的了一个注解,对子类中的方法依然能够生效,比如 @Transactional 注解:

public interface FooService {
@Transactional
Integer save(Foo foo);
}

public class FooServiceImpl implements FooService {
@Override
public Integer save(Foo foo) {
// do something
}
}

这功能来源于 Spring 的特殊搜索机制。Spring 在 AnnotatedElementUtils 中定义了两种语义的搜索:

  • 直接查找:即 getXXX,它查找在 AnnotatedElement 上直接存在的注解,对标 AnnotatedElement.getDeclaredAnnotation
  • 层级结构查找:即 findXXX,它会查找 AnnotatedElement 所在层级结构中的所有注解,相当于超级强化版的 AnnotatedElement.getAnnotation

对于 find 语义的查找,Spring 将会在 MergedAnnotations 中通过 AnnotationScanner 搜索 AnnotatedElement 的层级结构:

  • 如果是 Class:那么就查找所有它继承的父类或实现的接口
  • 如果是 Method:那么就查找所有它所在类继承的父类或实现的接口中,所有与它具备相同方法签名的方法(即被重写的方法)。
  • 如果是其他类型,则只查找它本身。

层级结构搜索配合元注解搜索,使得 Spring 支持从一个 AnnotatedElement 的任何维度找到一个可能存在的注解。

3. 属性别名与重写

除此之外,有时候我们会注意到,一些注解中会使用 @AliasFor 注解关联两个属性,比如我们比较熟悉的 @Transactional

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {

@AliasFor("transactionManager")
String value() default "";

@AliasFor("value")
String transactionManager() default "";

// ... ...
}

这种情况下,当我们使用 @Transactional 注解时,对 value 或者 transactionManager 赋值,都等同于同时对这两个属性赋值,这就是属性别名

而在一些特殊的注解中,还会换一个写法,比如 @PostMapping

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(method = RequestMethod.POST) // 使用 @RequestMapping 作为元注解
public @interface PostMapping {

@AliasFor(annotation = RequestMapping.class)
String name() default "";

@AliasFor(annotation = RequestMapping.class)
String[] value() default {};

// ... ...
}

在这种情况下,当我们获取 @RequestMapping 元注解时,元注解 @RequestMapping 中的同名属性(比如 namevaluepath 等)将会被 @PostMapping 覆盖

简单的来说,当我们尝试从 MergedAnnotation 获取属性值的时候, 由于 AnnotationTypeMapping 能获取到上级注解的 AnnotationTypeMapping,这使得组合注解允许使用上级注解的属性值来替代本身的属性值。

具体内容请参见 @AliasFor 注解是怎么生效的?

4. 在开发中使用

Spring 的增强注解机制在开发一些涉及到注解的通用功能时尤其好用,这里简单围绕日常中的使用举个例子。

在项目中,如果我们引入的 Kafka,那么多半会用到 @KafkaListener 注解。在一些场景下,我们可能会针对某些方法级的消费者进行配置:

@KafkaListener(
containerFactory = "orderContainerFactory",
errorHandler = "orderConsumerErrorHandler",
topics = "${order.message.new-order}",
containerGroup = "group1"
)
@ConsumerLog // 记录消费日志
public void processNewOrder(
ConsumerRecord<?, ?> records, Acknowledgment acknowledgment) {
// do something
}

在示例中,我们分别为两个方法指定了主题、异常拦截器、容器工厂与容器组等配置,并且通过自定义的 @ConsumerLog注解添加了消费日志。

实际业务中,根据订单类型的不同我们可能会有更多的消费方法,这些方法都需要加上注解,并且注解配置中除了主题以外其他的配置可能都是一样的。因此我们可以将其简化为一个组合注解 @KafkaOrderListener

@KafkaListener( // 原本的注解配置注解搬过来
containerFactory = "orderContainerFactory",
errorHandler = "orderConsumerErrorHandler",
containerGroup = "group1"
)
@ConsumerLog // 记录消费日志
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(ElementType.METHOD)
@interface KafkaOrderListener {

@AliasFor(annotation = KafkaListener.class, attribute = "topics")
String[] value() default {};
}

此时,我们的在方法上的注解就可以简化为:

@KafkaOrderListener("${order.message.new-order}")
public void processNewOrder(
ConsumerRecord<?, ?> records, Acknowledgment acknowledgment) {
// do something
}

组合注解机制提供了针对注解层做抽象的可能性,因此我们还可以根据情况将注解根据维度进一步的拆分为更细力度的注解,比如消费者属于哪个消费者组、来自哪个数据源……等等。

除了便于代码复用外,单纯从区分度来说,一个 @KafkaOrderListener注解相比起 @KafkaListener(xxx = xxx) 这种字面值,肯定是要更便于理解和搜索的,这也是组合注解的好处之一。