开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 23 天,点击查看活动详情
大家好,我是半夏之沫 😁😁 一名金融科技领域的JAVA系统研发😊😊
我希望将自己工作和学习中的经验以最朴实,最严谨的方式分享给大家,共同进步👉💓👈
👉👉👉👉👉👉👉👉💓写作不易,期待大家的关注和点赞💓👈👈👈👈👈👈👈👈
👉👉👉👉👉👉👉👉💓关注微信公众号【技术探界】 💓👈👈👈👈👈👈👈👈
前言
已知@Conditional注解用于指定能够注册为容器中的bean的条件。那么本篇文章将结合示例工程,从源码入手,分析@Conditional注解的如下几个方面。
- @Conditional注解的作用时机;
- Condition的执行顺序;
- 多个Condition之间的关系。
Springboot版本:2.4.1
Spring版本:5.3.2
正文
一. 示例工程搭建
示例工程结构如下所示。
Condition接口实现类如下所示。
public class MyControllerCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
System.out.println(this.getClass().getName());
return true;
}
}
public class MyDaoCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
System.out.println(this.getClass().getName());
return true;
}
}
public class MyFurtherCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
System.out.println(this.getClass().getName());
return true;
}
}
public class MyRepositoryCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
System.out.println(this.getClass().getName());
return true;
}
}
public class MyServiceCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
System.out.println(this.getClass().getName());
return true;
}
}
业务类定义如下所示。
@Controller
@Conditional(MyControllerCondition.class)
public class MyController {}
@Conditional(MyDaoCondition.class)
public class MyDao {}
public class MyFurtherService {}
@Conditional(MyRepositoryCondition.class)
public class MyRepository {}
public class MyService {}
配置类MyFurtherConfig定义如下。
@Configuration
@Conditional(MyFurtherCondition.class)
public class MyFurtherConfig {
@Bean
public MyFurtherService myFurtherService() {
return new MyFurtherService();
}
}
配置类MyConfig定义如下。
@ComponentScan
@Configuration
@Import(MyDao.class)
public class MyConfig {
@Bean
@Conditional(MyServiceCondition.class)
public MyService myService() {
return new MyService();
}
@Bean
public MyRepository myRepository() {
return new MyRepository();
}
}
测试类如下所示。
public class MyTest {
public static void main(String[] args) {
ApplicationContext applicationContext
= new AnnotationConfigApplicationContext(MyConfig.class);
}
}
在示例工程中,一共演示了如下几种使用@Conditional注解的情况。
- 业务类由@Conditional注解和@Controller(@Service,@Repository和@Component注解均可)注解修饰,并且业务类通过@ComponentScan注解扫描并注册到容器中
- MyController
- 业务类由@Conditional注解修饰,并且业务类通过@Import注解直接导入并注册到容器中
- MyDao
- 业务类通过@Bean注解修饰的方法注册到容器中,同时@Bean注解修饰的方法由@Conditional注解修饰
- MyService
- 配置类由@Conditional注解和@Configuration注解修饰,并且配置类通过@ComponentScan注解扫描并注册到容器中
- MyFurtherConfig
还有一种@Conditional注解无效的情况。
- 业务类由@Conditional注解修饰,并且业务类通过@Bean注解修饰的方法注册到容器中
- MyRepository
二. @Conditional作用时机
通过@Conditional注解修饰的类,在被注册为Spring中的BeanDefinition时,会多进行一步条件判断,如果判断返回true,则继续执行注册为BeanDefinition的逻辑,否则放弃注册。
@Conditional注解需要配合Condition接口使用,使用@Conditional注解时,需要通过@Conditional注解导入Condition接口的实现类,后续在向容器注册BeanDefinition的某些阶段,会调用到Condition接口的实现类实现的matches() 方法来进行判断。下面给出会调用到Condition接口的实现类实现的matches() 方法来进行判断的阶段。
- ConfigurationClassParser的processConfigurationClass() 方法解析ConfigurationClass时,如果ConfigurationClass对应的类由@Conditional注解修饰,那么会调用到Condition接口实现类的判断逻辑;
- ConfigurationClassParser的doProcessConfigurationClass() 方法解析ConfigurationClass对应的类的@ComponentScan注解时,会调用到ComponentScanAnnotationParser的parse() 方法开启对@ComponentScan注解内容的处理,在为@ComponentScan注解扫描范围内的每个目标类创建BeanDefinition时,如果目标类由@Conditional注解修饰,那么会调用到Condition接口实现类的判断逻辑(目标类:由@Controller,@Service,@Repository或@Configuration注解修饰的类);
- ConfigurationClassBeanDefinitionReader将ConfigurationClass解析为BeanDefinition时,如果ConfigurationClass对应的类由@Conditional注解修饰,那么会调用到Condition接口实现类的判断逻辑,如果判断返回值为false并且ConfigurationClass在注册表中已经存在一个BeanDefinition,那么还需要将这个BeanDefinition从注册表移除;
- ConfigurationClassBeanDefinitionReader将ConfigurationClass解析为BeanDefinition时,处理每个BeanMethod时,如果BeanMethod对应的方法由@Conditional注解修饰,那么会调用到Condition接口实现类的判断逻辑。
时序图如下。
(看不清可以点击图片并放大)
三. Condition执行顺序
Condition接口的实现类可以由@Conditional注解导入,并且@Conditional注解可以一次性导入多个Condition接口的实现类,并且默认情况下按导入顺序执行。如下给出一个例子进行说明。
业务如下所示。
public class MyService {}
定义两个Condition接口的实现类(后续称为条件类),如下所示。
public class MyFirstCondition implements Condition {
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
System.out.println(this.getClass().getName());
return true;
}
}
public class MySecondCondition implements Condition {
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
System.out.println(this.getClass().getName());
return true;
}
}
配置类如下所示,注意条件类的导入顺序。
@Configuration
public class MyConfig {
@Bean
@Conditional({MySecondCondition.class, MyFirstCondition.class})
public MyService myService() {
return new MyService();
}
}
测试类如下所示。
public class MyTest {
public static void main(String[] args) {
ApplicationContext applicationContext
= new AnnotationConfigApplicationContext(MyConfig.class);
}
}
打印结果如下。
可见条件类的调用顺序和条件类导入顺序是一致的。如果想要指定顺序,可以让条件类实现Ordered接口,或者实现PriorityOrdered接口,或者使用@Order注解修饰条件类。基于上述例子,再添加三个条件类,如下所示。
public class MyFirstOrderCondition implements Condition, Ordered {
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
System.out.println(this.getClass().getName());
return true;
}
public int getOrder() {
return 10;
}
}
public class MySecondOrderCondition implements Condition, PriorityOrdered {
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
System.out.println(this.getClass().getName());
return true;
}
public int getOrder() {
return 10;
}
}
@Order(5)
public class MyThirdOrderCondition implements Condition {
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
System.out.println(this.getClass().getName());
return true;
}
}
再修改一下配置类。
@Configuration
public class MyConfig {
@Bean
@Conditional({MySecondCondition.class, MyFirstCondition.class,
MyFirstOrderCondition.class, MySecondOrderCondition.class, MyThirdOrderCondition.class})
public MyService myService() {
return new MyService();
}
}
运行测试程序,打印如下。
通过上述打印结果可以总结如下。
- 实现了PriorityOrdered接口的条件类总是先于实现了Ordered接口的条件类执行;
- order值小的条件类总是先于order值大的条件类执行;
- 实现了PriorityOrdered接口,Ordered接口或者由@Order注解修饰的条件类总是先于普通条件类执行。
四. 多个Condition之间的关系
如果一个@Conditional注解导入了多个条件类,那么这些条件类的判断结果之间的关系是什么样的呢,下面对这个问题进行分析。Spring中有一个叫做ConditionEvaluator的类,这个类的shouldSkip() 方法专门用于调用条件类的判断逻辑,如下所示。
public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable ConfigurationPhase phase) {
// 如果当前类不由@Conditional注解修饰,直接返回false,表示不应该跳过当前类
if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) {
return false;
}
if (phase == null) {
if (metadata instanceof AnnotationMetadata &&
ConfigurationClassUtils.isConfigurationCandidate((AnnotationMetadata) metadata)) {
return shouldSkip(metadata, ConfigurationPhase.PARSE_CONFIGURATION);
}
return shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN);
}
// 把所有条件类实例化出来并添加到conditions集合中
List<Condition> conditions = new ArrayList<>();
for (String[] conditionClasses : getConditionClasses(metadata)) {
for (String conditionClass : conditionClasses) {
Condition condition = getCondition(conditionClass, this.context.getClassLoader());
conditions.add(condition);
}
}
AnnotationAwareOrderComparator.sort(conditions);
// 遍历每个条件类,并调用其matches()方法来进行判断
// 只要有一个条件类的matches()方法返回false,则表示应该跳过当前类
for (Condition condition : conditions) {
ConfigurationPhase requiredPhase = null;
if (condition instanceof ConfigurationCondition) {
requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase();
}
if ((requiredPhase == null || requiredPhase == phase) && !condition.matches(this.context, metadata)) {
return true;
}
}
return false;
}
ConditionEvaluator#shouldSkip方法返回true时表示当前类需要被跳过,而只要有一个条件类的matches() 方法返回false时,ConditionEvaluator#shouldSkip方法就会返回true,同时也不会再去执行剩下的条件类的matches() 方法。
也就是多个Condition之间是与的关系,只有满足所有的Condition,那么这个bean才能被注册到容器中。
总结
首先是@Conditional注解如何使用,总结如下。
- 业务类由@Conditional注解和@Controller(@Service,@Repository和@Component注解均可)注解修饰,并且业务类通过@ComponentScan注解扫描并注册到容器中;
- 业务类由@Conditional注解修饰,并且业务类通过@Import注解直接导入并注册到容器中;
- 业务类通过@Bean注解修饰的方法注册到容器中,同时@Bean注解修饰的方法由@Conditional注解修饰;
- 配置类由@Conditional注解和@Configuration注解修饰,并且配置类通过@ComponentScan注解扫描并注册到容器中。
然后是@Conditional注解作用时机,总的概括就是在将一个对象注册为容器中的BeanDefinition的过程中,会在各个环节调用到Condition接口实现类的判断逻辑来判断是否需要终止注册流程。
接下来是多个Condition的执行顺序的总结,总结如下。
- 默认情况按导入顺序来执行;
- 实现了PriorityOrdered接口的条件类总是先于实现了Ordered接口的条件类执行;
- order值小的条件类总是先于order值大的条件类执行;
- 实现了PriorityOrdered接口,Ordered接口或者由@Order注解修饰的条件类总是先于普通条件类执行。
最后是多个Condition之间的关系,关系是与的关系,也就是只要有一个Condition不满足,那么这个bean就不会被注册。
大家好,我是半夏之沫 😁😁 一名金融科技领域的JAVA系统研发😊😊
我希望将自己工作和学习中的经验以最朴实,最严谨的方式分享给大家,共同进步👉💓👈
👉👉👉👉👉👉👉👉💓写作不易,期待大家的关注和点赞💓👈👈👈👈👈👈👈👈
👉👉👉👉👉👉👉👉💓关注微信公众号【技术探界】 💓👈👈👈👈👈👈👈👈
开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 23 天,点击查看活动详情