Spring的组合注解是什么?
作者:程序员马丁
在线博客:https://open8gu.com
大话面试,技术同学面试必备的八股文小册,以精彩回答应对深度问题,助力你在面试中拿个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
扩展而来。@PostMapping
、GetMapping
、PutMapping
和DeleteMapping
注解基于RequestMapping
扩展而来。@RestContorller
注解基于@ResponseBody
和@Contorller
注解组合而来。
问题详解
想更深入技术底层的同学,可以参考 Spring 官方文档:
1. 实现原理
Spring 中通过 AnnotationUtils
或者 AnnotatedElemenUtils
的 getMergedAnnotation/findMergedAnnotation
相关 API 来获得组合注解。
进入该方法底层,则会发现其实是根据一个叫做 MergedAnnotations
类实现的,它是一个接口,即我们一般说的“组合注解”(Composed Annotation)。
这里涉及到四个类:
AnnotationTypeMapping
:注解类型映射,表示一个元注解,元注解会持有它的上级注解的引用,从而形成树结构,简而言之,就是可以知道自己被用在哪个注解上;AnnotationTypeMappings
:注解类型映射聚合,表示一个注解和它的全部元注解;TypeMappedAnnotations
:组合注解聚合,表示一个AnnotatedElement
上的全部注解;TypeMappedAnnotation
:组合注解,通过AnnotationTypeMapping
生成,通过其获取属性值是,会根据对应的AnnotationTypeMapping
与关联的上级AnnotationTypeMapping
计算真实的属性。
举个例子,当我们调用 AnnotatedElementUtils.getMergedAnnotation(Foo.class, Meta2.class)
方法时,Spring 做了这些事情:
- 为搜索元素构建组合注解聚合:为指定要搜索的
AnnotatedElement
,比如图例中的Foo.class
创建一个MergedAnnotations
(实现类实际为TypeMappedAnnotations
); - 搜索根注解:
MergedAnnotations
通过AnnotationScanner
(这是 Spring 内部的一个注解搜索器)依次扫描Foo.class
上根注解Annotation2
和Annotation1
; - 为根注解构建元注解聚合:为搜索到的
Annotation2
和Annotation1
构建一个AnnotationTypeMappings
:- 按广度优先遍历注解的层级结构,搜集对应的根注解上的全部元注解,然后将其包装为
AnnotationTypeMapping
; - 每一个元注解对应的
AnnotationTypeMapping
都持有其上级注解对应的AnnotationTypeMapping
的引用,从而最终形成一个以根注解作为根节点的树结构。 比如,以Annotation1
来说,Meta1
会持有Annotation1
的引用,而SuperMeta3
和SuperMeta4
会持有Meta2
的引用,它们最终形成了一个Annotation1
为根节点的树结构;
- 按广度优先遍历注解的层级结构,搜集对应的根注解上的全部元注解,然后将其包装为
- 从元注解聚合中查找注解:遍历
AnnotationTypeMappings
中收集到的每一个注解,直到找到Meta2
对应的AnnotationTypeMapping
为止; - 合成注解:将根据
Meta2
的AnnotationTypeMapping
创建一个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
中的同名属性(比如 name
、value
和 path
等)将会被 @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)
这种字面值,肯定是要更便于理解和搜索的,这也是组合注解的好处之一。