一、Conditional注解介绍

对SpringBoot有足够了解的小伙伴应该都用过Conditional系列注解,该注解可用在类或者方法上用于控制Bean的初始化。

常用的Conditional注解有以下几种:

  1. @ConditionalOnBean:如果存在对应的Bean,则进行当前Bean的初始化。

  2. @ConditionalOnClass:如果项目的classpath下存在对应的类文件,则进行当前Bean的初始化。

  3. @ConditionalOnExpression:如果满足SpEL表达式,则进行当前Bean的初始化。

  4. @ConditionalOnMissingBean:如果不存在对应的Bean,则进行当前Bean的初始化。

  5. @ConditionalOnMissingClass:如果项目的classpath下不存在对应的类文件,则进行当前Bean的初始化。

  6. @ConditionalOnProperty:如果配置文件上的属性值符合预期值,则进行当前Bean的初始化。

注意如果存在多个Conditional注解,只有都满足条件时才会生效。这里只作简单介绍,更多用法可以搜索其他文章。

二、源码分析

我们先以@ConditionalOnBean为例,分析SpringBoot是如何实现该功能的?

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnBeanCondition.class)
public @interface ConditionalOnBean { /**
* The class types of beans that should be checked. The condition matches when beans
* of all classes specified are contained in the {@link BeanFactory}.
* @return the class types of beans to check
*/
Class<?>[] value() default {}; /**
* The class type names of beans that should be checked. The condition matches when
* beans of all classes specified are contained in the {@link BeanFactory}.
* @return the class type names of beans to check
*/
String[] type() default {}; /**
* The annotation type decorating a bean that should be checked. The condition matches
* when all of the annotations specified are defined on beans in the
* {@link BeanFactory}.
* @return the class-level annotation types to check
*/
Class<? extends Annotation>[] annotation() default {}; /**
* The names of beans to check. The condition matches when all of the bean names
* specified are contained in the {@link BeanFactory}.
* @return the names of beans to check
*/
String[] name() default {}; /**
* Strategy to decide if the application context hierarchy (parent contexts) should be
* considered.
* @return the search strategy
*/
SearchStrategy search() default SearchStrategy.ALL; /**
* Additional classes that may contain the specified bean types within their generic
* parameters. For example, an annotation declaring {@code value=Name.class} and
* {@code parameterizedContainer=NameRegistration.class} would detect both
* {@code Name} and {@code NameRegistration<Name>}.
* @return the container types
* @since 2.1.0
*/
Class<?>[] parameterizedContainer() default {}; }

首先,查看@ConditionalOnBean注解的源码,发现该注解上有个元数据注解 @Conditional,那么这个注解是干嘛的呢?

点进去查看其注释,发现有如下一行话。

/**
*
* <p>The {@code @Conditional} annotation may be used in any of the following ways:
* <ul>
* <li>as a type-level annotation on any class directly or indirectly annotated with
* {@code @Component}, including {@link Configuration @Configuration} classes</li>
* <li>as a meta-annotation, for the purpose of composing custom stereotype
* annotations</li>// 作为元注解,用于编写自定义构造型注解
* <li>as a method-level annotation on any {@link Bean @Bean} method</li>
* </ul>
* ....
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional { /**
* All {@link Condition} classes that must {@linkplain Condition#matches match}
* in order for the component to be registered.
*/
Class<? extends Condition>[] value(); }

由此可知,@Conditional有三种用法,当作为元注解时可以用来自定义条件注解,其核心逻辑是由其value指定的Condition接口的实现类来完成的。

@FunctionalInterface
public interface Condition { /**
* Determine if the condition matches.
* @param context the condition context
* @param metadata the metadata of the {@link org.springframework.core.type.AnnotationMetadata class}
* or {@link org.springframework.core.type.MethodMetadata method} being checked
* @return {@code true} if the condition matches and the component can be registered,
* or {@code false} to veto the annotated component's registration
*/
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata); }

Condition是个函数式接口,里面只有一个mathes方法,如果返回true则进行Bean的创建。其中matches方法有两个参数ConditionContext和AnnotatedTypeMetadata,其作用如下:

ConditionContext: 可以拿到上下文信息,包括beanFactory、environment和resourceLoader等。

AnnotatedTypeMetadata: 可以获取被@Conditional标记的注解信息。

public interface ConditionContext {

	BeanDefinitionRegistry getRegistry();

	@Nullable
ConfigurableListableBeanFactory getBeanFactory(); Environment getEnvironment(); ResourceLoader getResourceLoader(); @Nullable
ClassLoader getClassLoader(); }

那么问题来了,上面的@Conditional的value属性可以指定多个Condition类A和B,如何控制Bean的初始化呢?

其实这里是与的逻辑,只有当所以Condition实现类的mathes方法都返回true时,才会进行Bean的初始化,否则不生效。具体原因,这里不扩展了。

由此可知@ConditionalOnBean的核心逻辑就在OnBeanCondition类里,OnBeanCondition也实现了Condition接口,但是其mathes()方法是在SpringBootCondition中实现的。

/**
* 所有Condition实现的基类
* Base of all {@link Condition} implementations used with Spring Boot. Provides sensible
* logging to help the user diagnose what classes are loaded.
*
* @author Phillip Webb
* @author Greg Turnquist
* @since 1.0.0
*/
public abstract class SpringBootCondition implements Condition { private final Log logger = LogFactory.getLog(getClass()); @Override
public final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
String classOrMethodName = getClassOrMethodName(metadata);
try {
// getMatchOutcome是个抽象方法,由各个子类去实现
ConditionOutcome outcome = getMatchOutcome(context, metadata);
logOutcome(classOrMethodName, outcome);
recordEvaluation(context, classOrMethodName, outcome);
return outcome.isMatch();
}
catch (NoClassDefFoundError ex) {
throw new IllegalStateException("Could not evaluate condition on " + classOrMethodName + " due to "
+ ex.getMessage() + " not found. Make sure your own configuration does not rely on "
+ "that class. This can also happen if you are "
+ "@ComponentScanning a springframework package (e.g. if you "
+ "put a @ComponentScan in the default package by mistake)", ex);
}
catch (RuntimeException ex) {
throw new IllegalStateException("Error processing condition on " + getName(metadata), ex);
}
}
// 省略其他代码...
} public class ConditionOutcome { private final boolean match; private final ConditionMessage message; // 省略其他代码...
}

ConditionOutcome是个匹配结果类,里面只有两个属性字段,匹配逻辑在getMatchOutcome方法由子类实现,刚好OnBeanCondition.java中有该方法的实现,代码如下。

@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
ConditionMessage matchMessage = ConditionMessage.empty();
MergedAnnotations annotations = metadata.getAnnotations();
if (annotations.isPresent(ConditionalOnBean.class)) {
// 首先包装成Spec类,Spec里的属性与ConditionalOnBean注解的基本一致
Spec<ConditionalOnBean> spec = new Spec<>(context, metadata, annotations, ConditionalOnBean.class);
// 获取所有的匹配的bean
MatchResult matchResult = getMatchingBeans(context, spec);
if (!matchResult.isAllMatched()) {
// 由于@ConditionalOnBean可以指定多个bean,所以这里要求全匹配,否则返回不匹配的原因
String reason = createOnBeanNoMatchReason(matchResult);
return ConditionOutcome.noMatch(spec.message().because(reason));
}
matchMessage = spec.message(matchMessage).found("bean", "beans").items(Style.QUOTE,
matchResult.getNamesOfAllMatches());
}
if (metadata.isAnnotated(ConditionalOnSingleCandidate.class.getName())) {
Spec<ConditionalOnSingleCandidate> spec = new SingleCandidateSpec(context, metadata, annotations);
MatchResult matchResult = getMatchingBeans(context, spec);
if (!matchResult.isAllMatched()) {
return ConditionOutcome.noMatch(spec.message().didNotFind("any beans").atAll());
}
else if (!hasSingleAutowireCandidate(context.getBeanFactory(), matchResult.getNamesOfAllMatches(),
spec.getStrategy() == SearchStrategy.ALL)) {
return ConditionOutcome.noMatch(spec.message().didNotFind("a primary bean from beans")
.items(Style.QUOTE, matchResult.getNamesOfAllMatches()));
}
matchMessage = spec.message(matchMessage).found("a primary bean from beans").items(Style.QUOTE,
matchResult.getNamesOfAllMatches());
}
if (metadata.isAnnotated(ConditionalOnMissingBean.class.getName())) {
Spec<ConditionalOnMissingBean> spec = new Spec<>(context, metadata, annotations,
ConditionalOnMissingBean.class);
MatchResult matchResult = getMatchingBeans(context, spec);
if (matchResult.isAnyMatched()) {
String reason = createOnMissingBeanNoMatchReason(matchResult);
return ConditionOutcome.noMatch(spec.message().because(reason));
}
matchMessage = spec.message(matchMessage).didNotFind("any beans").atAll();
}
return ConditionOutcome.match(matchMessage);
}

获取bean的逻辑在getMatchingBeans方法中,大家可以自己去看下(我才不会说是我懒)。

至此通过上面的分析总结下,自定义一个ConditionalOnXX注解大概分为如下几步

  1. 创建一个Condition类并实现Condition接口
  2. 根据自己的需求写一个ConditionalOnXX注解,并指定Condition类
  3. 完善Condition类的matches方法逻辑

三、自定义ConditionalOnXX注解

需求背景: 自定义ConditionalOnXX注解,实现设置的多个配置项中,任意一个配置匹配中即可完成Bean的初始化。

1.创建Conditional注解

/**
* @Author: Ship
* @Description: 任意name-value匹配即可
* @Date: Created in 2021/10/18
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(value = OnAnyMatchCondition.class)
public @interface ConditionalOnAnyMatch { String[] name() default {}; String[] value() default {};
}

2.实现Condition类

/**
* @Author: Ship
* @Description:
* @Date: Created in 2021/10/18
*/
public class OnAnyMatchCondition implements Condition { @Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Environment environment = context.getEnvironment();
MergedAnnotations annotations = metadata.getAnnotations();
if (!annotations.isPresent(ConditionalOnAnyMatch.class)) {
return true;
}
MergedAnnotation<ConditionalOnAnyMatch> annotation = annotations.get(ConditionalOnAnyMatch.class);
// 获取注解的属性值
String[] names = annotation.getValue("name", String[].class).orElse(new String[]{});
String[] values = annotation.getValue("value", String[].class).orElse(new String[]{});
for (int i = 0; i < names.length; i++) {
// 通过环境变量拿到项目配置信息
String property = environment.getProperty(names[i]);
String value = values[i];
if (value != null && value.equals(property)) {
// 任意一个匹配则返回true
return true;
}
}
return false;
}
}

四、测试

测试前需要写一些测试代码,首先创建一个用于测试的Bean类TestBean

public class TestBean {

}

其次,为了分别测试@ConditionalOnAnyMatch用于类上和方法上的效果,分别创建TestClassBean和TestConfiguration。

/**
* @Author: Ship
* @Description:
* @Date: Created in 2021/10/18
*/
@ConditionalOnAnyMatch(name = {"test.aa", "test.bb"}, value = {"1", "2"})
@Component
public class TestClassBean { @PostConstruct
public void init(){
System.out.println("Initialized bean:testClassBean...");
}
}

TestConfiguration.java

/**
* @Author: Ship
* @Description:
* @Date: Created in 2021/10/18
*/
@Configuration
public class TestConfiguration { @Bean
@ConditionalOnAnyMatch(name = {"test.aa", "test.bb"}, value = {"1", "2"})
public TestBean testBean() {
System.out.println("Initialized bean:testBean...");
return new TestBean();
}
}

通过代码可以看到,test.aa=1或者test.bb=2任意一个条件成立,就会创建Bean。

  • 测试不符合的场景

    配置文件application.properties添加如下配置

    test.aa=3
    test.bb=4

    然后启动项目,可以看到控制台日志没有打印任何信息,说明testBean和testClassBean都没被创建。

  • 测试符合一个的场景

    修改配置如下

    test.aa=1
    test.bb=4

    再重启项目,可以看到控制台日志打印了如下信息,说明testBean和testClassBean都被创建了

    Initialized bean:testClassBean...
    Initialized bean:testBean...
    2021-10-20 21:37:50.319 INFO 7488 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8081 (http) with context path ''
    2021-10-20 21:37:50.328 INFO 7488 --- [ main] cn.sp.SpringExtensionApplication : Started SpringExtensionApplication in 1.725 seconds (JVM running for 2.325)

说明@ConditionalOnAnyMatch成功的控制了Bean的初始化,本文代码已经上传至github,如果对你有用希望能点个star,不胜感激。

自定义ConditionalOnXX注解的更多相关文章

  1. [译]SpringMVC自定义验证注解(SpringMVC custom validation annotations)

    在基于SpringMVC框架的开发中,我们经常要对用户提交的字段进行合法性验证,比如整数类型的字段有个范围约束,我们会用@Range(min=1, max=4).在实际应用开发中,我们经常碰到一些自己 ...

  2. asp.net mvc3 数据验证(三)—自定义数据注解

    原文:asp.net mvc3 数据验证(三)-自定义数据注解         前两节讲的都是asp.net mvc3预先设定的数据注解,但是系统自由的数据注解肯定不适合所有的场合,所以有时候我们需要 ...

  3. jsr-303 参数校验—自定义校验注解

    1.为什么要自定义? 通过上篇学习,了解到很多常用注解了,但是呢,总是有那么些需求....   2.案例分析(手机号格式) 2.1.需要验证的实体 Bean public class LoginVo ...

  4. [转]Java中实现自定义的注解处理器

    Java中实现自定义的注解处理器(Annotation Processor) 置顶2016年07月25日 19:42:49 阅读数:9877 在之前的<简单实现ButterKnife的注解功能& ...

  5. Springboot中使用自定义参数注解获取 token 中用户数据

    使用自定义参数注解获取 token 中User数据 使用背景 在springboot项目开发中需要从token中获取用户信息时通常的方式要经历几个步骤 拦截器中截获token TokenUtil工具类 ...

  6. SpringBoot自定义Condition注解

        最近碰到个这样的需求,需要同一套代码适配个版本数据库(数据库不同,且部分表的字段及关联关系可能会不同),即这套代码配置不同的数据库都能跑.项目采用的框架为SpringBoot+Mybatis. ...

  7. 【Spring Cloud】Spring Cloud之自定义@SpringCloudProfile注解实现@Profile注解的功能

    一.为什么会想到定义@SpringCloudProfile这样的注解 首页提一下@Profile注解:它主要用与Spring Boot多环境配置中,指定某个类只在指定环境中生效,比如swagger的配 ...

  8. 自定义jsr-269注解处理器 Error:服务配置文件不正确,或构造处理程序对象javax.annotation.processing.Processor: Provider not found

    出现的原因 自定义处理器还没有被编译就被调用,所以报 not found在根据配置寻找自定义的注解处理器时,自定义处理器还未被编译12解决方式 maven项目可以配置编译插件,在编译项目之前先编译处理 ...

  9. 自定义日志注解 + AOP实现记录操作日志

      需求:系统中经常需要记录员工的操作日志和用户的活动日志,简单的做法在每个需要的方法中进行日志保存操作, 但这样对业务代码入侵性太大,下面就结合AOP和自定义日志注解实现更方便的日志记录   首先看 ...

随机推荐

  1. 性能测试工具JMeter 基础(七)—— 测试元件: 逻辑控制器之if逻辑控制器

    逻辑控制器线程组指定了其取样器执行的逻辑条件.顺序,并且执行顺序是按照位置顺序从上至下执行的 if逻辑控制器(If Controller) 在逻辑控制器中可设置条件,当条件满足的时候才会被执行 一共有 ...

  2. JVM详解(一)——概述

    Test https://www.cnblogs.com/yrxing/p/14651346.html#gc的基础知识 https://www.cnblogs.com/yinzhengjie/p/92 ...

  3. Python常见问题 - python3 requests库提示警告InsecureRequestWarning的问题

    当使用 requests 库发送请求时报了以下警告 D:\python3.6\lib\site-packages\urllib3\connectionpool.py:847: InsecureRequ ...

  4. python基础--网站推荐

    Python教程 - 廖雪峰的官方网站 Python 基础教程 | 菜鸟教程 随笔分类 - 机器学习

  5. Android使用百度语音识别api代码实现。

    第一步 ① 创建平台应用 点击百度智能云进入,没有账号的可以先注册账号,这里默认都有账号了,然后登录. 然后左侧导航栏点击找到语音技术 然后会进入一个应用总览页面, 然后点击创建应用 立即创建 点击查 ...

  6. 238 day02_Collection、泛型

    day02[Collection.泛型] 主要内容 Collection集合 迭代器 增强for 泛型 教学目标 [ ] 能够说出集合与数组的区别 [ ] 说出Collection集合的常用功能 [ ...

  7. 论文解读(SimCLR)《A Simple Framework for Contrastive Learning of Visual Representations》

    1 题目 <A Simple Framework for Contrastive Learning of Visual Representations> 作者: Ting Chen, Si ...

  8. 痞子衡嵌入式:嵌入式Cortex-M中断向量表对齐原则的深入研究

    大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家分享的是Cortex-M中断向量表对齐原则. 今天这篇文章的内容主要来自于五年前做 Kinetis K32W 系列双核启动时的发现,最近正好有同 ...

  9. 深入学习Composer原理(一)

    Composer作为PHP的包管理工具,为PHPer们提供了丰富的类库,并且让PHP重焕新生,避免被时代淘汰的悲剧.可以说,Composer和PHP7是现在PHP开发者的标配,如果你还没用过Compo ...

  10. Jmeter扩展组件开发(5) - 初始化方法的作用与实现

    CODE //URLNAME 就是在图形化界面当中显示的变量名称private static final String URLNAME = "URL";//设置界面当中默认显示的变 ...