Spring Boot系列之条件注解

概述

想要搞懂Spring Boot自动配置,绕不过条件注解,即@Conditional,可用于根据某个特定的条件来判断是否需要创建某个特定的Bean。本文分析基于spring-boot-autoconfigure-3.2.4版本。

@Conditional注解可以添加在被@Configuration、@Component、@Service等修饰的类,或在被@Bean修饰的方法上,用于控制类或方法对应的Bean是否需要创建。

@Conditional注解需要和Condition接口搭配一起使用。通过对应Condition接口来告知是否满足匹配条件。

扩展注解

条件注解对应Condition处理类解释
ConditionalOnClassOnClassCondition类加载器中存在指定类
ConditionalOnMissingClassOnClassCondition类加载器中不存在指定类
ConditionalOnBeanOnBeanConditionSpring容器中存在指定Bean
ConditionalOnMissingBeanOnBeanConditionSpring容器中不存在指定Bean
ConditionalOnSingleCandidateOnBeanConditionSpring容器中是否存在且只存在一个对应的实例,或虽然有多个但是指定首选的Bean生效
ConditionalOnJavaOnJavaCondition指定Java版本符合要求生效
ConditionalOnJndiOnJndiCondition存在JNDI
ConditionalOnCloudPlatformOnCloudPlatformCondition云平台,支持:CLOUD_FOUNDRY、HEROKU、SAP、NOMAD、KUBERNETES
ConditionalOnCheckpointRestore存在类orc.crac.Resource
ConditionalOnWebApplicationOnWebApplicationConditionWeb应用生效
ConditionalOnNotWebApplicationOnWebApplicationCondition不是Web应用生效
ConditionalOnWarDeploymentOnWarDeploymentConditionWar应用生效
ConditionalOnNotWarDeploymentOnWarDeploymentCondition不是War应用生效
ConditionalOnResourceOnResourceCondition当指定资源文件出现则生效
ConditionalOnPropertyOnPropertyCondition应用环境中的属性满足条件生效
ConditionalOnExpressionOnExpressionCondition判断SpEL表达式成立生效
ConditionalOnThreadingOnThreadingCondition指定线程处于active状态

ConditionalOnCheckpointRestore源码如下:

@ConditionalOnClass(name = {"org.crac.Resource"})
public @interface ConditionalOnCheckpointRestore {
}

CRaC是OpenJDK项目,有兴趣可延伸阅读。

原理

条件注解存在的意义在于动态识别,即代码自动化执行。如@ConditionalOnClass会检查类加载器中是否存在对应的类,如果有的话被注解修饰的类就有资格被Spring容器所注册,否则会被skip。

如FreemarkerAutoConfiguration这个自动化配置类的定义如下:

@AutoConfiguration
@ConditionalOnClass({ freemarker.template.Configuration.class, FreeMarkerConfigurationFactory.class })
@EnableConfigurationProperties(FreeMarkerProperties.class)
@Import({ FreeMarkerServletWebConfiguration.class, FreeMarkerReactiveWebConfiguration.class, FreeMarkerNonWebConfiguration.class })
public class FreeMarkerAutoConfiguration {
}

这个自动化配置类被@ConditionalOnClass条件注解修饰,判断类加载器中是否存在freemarker.template.Configuration和FreeMarkerConfigurationFactory这两个类,如果都存在的话会在Spring容器中加载这个FreeMarkerAutoConfiguration配置类;否则不会加载。

@Conditional源码:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {Class<? extends Condition>[] value();
}

需要传入一个Class数组,数组类型是Condition。而Condition是个接口,用于匹配组件是否有资格被容器注册:

@FunctionalInterface
public interface Condition {// ConditionContext内部会存储Spring容器、应用程序环境信息、资源加载器、类加载器boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}

@Conditional注解属性中可以持有多个Condition接口的实现类,所有的Condition接口需要全部匹配成功后这个@Conditional修饰的组件才有资格被注册。

Condition有个子接口ConfigurationCondition:

public interface ConfigurationCondition extends Condition {ConfigurationPhase getConfigurationPhase();public static enum ConfigurationPhase {PARSE_CONFIGURATION,REGISTER_BEAN}
}

这个子接口是一种特殊的条件接口,多一个getConfigurationPhase方法,也就是条件注解的生效阶段。只有在ConfigurationPhase中定义的两种阶段下才会生效:

  • PARSE_CONFIGURATION
  • REGISTER_BEAN

Condition接口有个抽象类SpringBootCondition,SpringBoot中所有条件注解对应的条件类都继承这个抽象类,并需要实现matches方法:

@Override
public final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {String classOrMethodName = getClassOrMethodName(metadata); // 得到类名或者方法名(条件注解可以作用的类或者方法上)try {ConditionOutcome outcome = getMatchOutcome(context, metadata); // 抽象方法,具体子类实现。ConditionOutcome记录了匹配结果boolean和log信息logOutcome(classOrMethodName, outcome); // log记录一下匹配信息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);}
}

基于Class的条件注解

有两个

  • @ConditionalOnClass
  • @ConditionalOnMissingClass

@ConditionalOnClass注解定义如下:

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnClassCondition.class)
public @interface ConditionalOnClass {Class<?>[] value() default {}; // 需要匹配的类String[] name() default {}; // 需要匹配的类名
}

它有2个属性,分别是类数组和字符串数组(作用一样,类型不一样),而且被@Conditional注解所修饰。

对应条件类是OnClassCondition:

@Order(Ordered.HIGHEST_PRECEDENCE) // 优先级最高级别
class OnClassCondition extends FilteringSpringBootCondition {@Overrideprotected final ConditionOutcome[] getOutcomes(String[] autoConfigurationClasses,AutoConfigurationMetadata autoConfigurationMetadata) {// Split the work and perform half in a background thread if more than one// processor is available. Using a single additional thread seems to offer the// best performance. More threads make things worse.if (autoConfigurationClasses.length > 1 && Runtime.getRuntime().availableProcessors() > 1) {return resolveOutcomesThreaded(autoConfigurationClasses, autoConfigurationMetadata);}else {OutcomesResolver outcomesResolver = new StandardOutcomesResolver(autoConfigurationClasses, 0,autoConfigurationClasses.length, autoConfigurationMetadata, getBeanClassLoader());return outcomesResolver.resolveOutcomes();}}
}

比如FreemarkerAutoConfiguration中的@ConditionalOnClass注解中有value属性是freemarker.template.Configuration.classFreeMarkerConfigurationFactory.class。在OnClassCondition执行过程中得到的最终ConditionalOutcome中的log message如下:
@ConditionalOnClass classes found: freemarker.template.Configuration,org.springframework.ui.freemarker.FreeMarkerConfigurationFactory

基于Bean的条件注解

有3个:

  • @ConditionalOnBean
  • @ConditionalOnMissingBean
  • @ConditionalOnSingleCandidate

和基于类的条件注解比较类似。

激活机制

这部分有点难,想通过阅读源码来理清楚前后调用及解析关系。好在我们可以断点调试。通过断点调试发现关键类和方法:

  • ConfigurationClassParser
  • ConditionEvaluator
  • ComponentScanAnnotationParser

SpringBoot使用ConditionEvaluator这个内部类完成条件注解的解析和判断。在Spring容器的refresh过程中,只有跟解析或者注册bean有关系的类都会使用ConditionEvaluator完成条件注解的判断,这个过程中一些类不满足条件的话就会被skip。这些类比如有AnnotatedBeanDefinitionReader、ConfigurationClassBeanDefinitionReader、ConfigurationClassParse、ClassPathScanningCandidateComponentProvider等。

比如ConfigurationClassParser的构造函数会初始化内部属性conditionEvaluator:

public ConfigurationClassParser(MetadataReaderFactory metadataReaderFactory,ProblemReporter problemReporter, Environment environment, ResourceLoader resourceLoader,BeanNameGenerator componentScanBeanNameGenerator, BeanDefinitionRegistry registry) {this.metadataReaderFactory = metadataReaderFactory;this.problemReporter = problemReporter;this.environment = environment;this.resourceLoader = resourceLoader;this.registry = registry;this.componentScanParser = new ComponentScanAnnotationParser(resourceLoader, environment, componentScanBeanNameGenerator, registry);// 构造ConditionEvaluator用于处理条件注解this.conditionEvaluator = new ConditionEvaluator(registry, environment, resourceLoader);
}

ConfigurationClassParser对每个配置类进行解析的时候都会使用ConditionEvaluator:

if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {return;
}

ConditionEvaluator的skip方法:

public boolean shouldSkip(AnnotatedTypeMetadata metadata, ConfigurationPhase phase) {// 如果这个类没有被@Conditional注解所修饰,不会skipif (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) {return false;}// 如果参数中沒有设置条件注解的生效阶段if (phase == null) {// 是配置类的话直接使用PARSE_CONFIGURATION阶段if (metadata instanceof AnnotationMetadata && ConfigurationClassUtils.isConfigurationCandidate((AnnotationMetadata) metadata)) {return shouldSkip(metadata, ConfigurationPhase.PARSE_CONFIGURATION);}// 否则使用REGISTER_BEAN阶段return shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN);}// 要解析的配置类的条件集合List<Condition> conditions = new ArrayList<Condition>();// 获取配置类的条件注解得到条件数据,并添加到集合中for (String[] conditionClasses : getConditionClasses(metadata)) {for (String conditionClass : conditionClasses) {Condition condition = getCondition(conditionClass, this.context.getClassLoader());conditions.add(condition);}}// 对条件集合做个排序AnnotationAwareOrderComparator.sort(conditions);// 遍历条件集合for (Condition condition : conditions) {ConfigurationPhase requiredPhase = null;if (condition instanceof ConfigurationCondition) {requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase();}// 没有这个解析类不需要阶段的判断或者解析类和参数中的阶段一致才会继续进行if (requiredPhase == null || requiredPhase == phase) {// 阶段一致切不满足条件的话,返回true并跳过这个bean的解析if (!condition.matches(this.context, metadata)) {return true;}}}return false;
}

SpringBoot在条件注解的解析log记录在ConditionEvaluationReport类中,可通过BeanFactory获取。BeanFactory是有父子关系的;每个BeanFactory都存有一份ConditionEvaluationReport,互不相干:

ConditionEvaluationReport conditionEvaluationReport = beanFactory.getBean("autoConfigurationReport", ConditionEvaluationReport.class);
Map<String, ConditionEvaluationReport.ConditionAndOutcomes> result = conditionEvaluationReport.getConditionAndOutcomesBySource();
for(String key : result.keySet()) {ConditionEvaluationReport.ConditionAndOutcomes conditionAndOutcomes = result.get(key);Iterator<ConditionEvaluationReport.ConditionAndOutcome> iterator = conditionAndOutcomes.iterator();while(iterator.hasNext()) {ConditionEvaluationReport.ConditionAndOutcome conditionAndOutcome = iterator.next();System.out.println(key + " -- " + conditionAndOutcome.getCondition().getClass().getSimpleName() + " -- " + conditionAndOutcome.getOutcome());}
}

打印出条件注解下的类加载信息:

...
org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration -- OnClassCondition -- required @ConditionalOnClass classes not found: freemarker.template.Configuration,org.springframework.ui.freemarker.FreeMarkerConfigurationFactory
org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration -- OnClassCondition -- required @ConditionalOnClass classes not found: groovy.text.markup.MarkupTemplateEngine
org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration -- OnClassCondition -- required @ConditionalOnClass classes not found: com.google.gson.Gson
org.springframework.boot.autoconfigure.h2.H2ConsoleAutoConfiguration -- OnClassCondition -- required @ConditionalOnClass classes not found: org.h2.server.web.WebServlet
org.springframework.boot.autoconfigure.hateoas.HypermediaAutoConfiguration -- OnClassCondition -- required @ConditionalOnClass classes not found: org.springframework.hateoas.Resource,org.springframework.plugin.core.Plugin
org.springframework.boot.autoconfigure.hazelcast.HazelcastAutoConfiguration -- OnClassCondition -- required @ConditionalOnClass classes not found: com.hazelcast.core.HazelcastInstance
...

实战

自定义

需要自定义一个condition类实现Condition接口,假设根据系统类型来加载不同的Bean:

public class OnSystemCondition implements Condition {@Overridepublic boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {Map<String, Object> annotationAttributes = metadata.getAnnotationAttributes(ConditionalOnSystem.class.getName());if (annotationAttributes == null) {return false;}ConditionalOnSystem.SystemType systemType = (ConditionalOnSystem.SystemType) annotationAttributes.get("type");switch (systemType) {case WINDOWS:return context.getEnvironment().getProperty("os.name").contains("Windows");case LINUX:return context.getEnvironment().getProperty("os.name").contains("Linux ");case MAC:return context.getEnvironment().getProperty("os.name").contains("Mac ");}return false;}
}

自定义条件注解并指定对应的处理condition类:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
@Conditional(OnSystemCondition.class)
public @interface ConditionalOnSystem {/*** 指定系统*/SystemType type() default SystemType.WINDOWS;/*** 系统类型*/enum SystemType {WINDOWS,LINUX,MAC;}
}

参考

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/831159.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Ps 滤镜:智能锐化

Ps菜单&#xff1a;滤镜/锐化/智能锐化 Filter/Sharpen/Smart Sharpen 智能锐化 Smart Sharpen滤镜可以用来提高图像的视觉清晰度和边缘细节&#xff0c;同时最大限度地减少常见的锐化问题如噪点和光晕等。 “智能锐化”滤镜通过自适应算法分析图像内容&#xff0c;针对不同的细…

省级财政收入、支出、第一、二、三产业增加值、工业增加值、金融业增加值占GDP比重数据(1978-2022年)

01、数据介绍 财政收支作为国家治理的基础&#xff0c;越来越受到社会各界的关注。同时&#xff0c;产业结构的优化与升级也是中国经济持续增长的关键因素。本数据对中国省级财政收入、支出占GDP的比重以及第一、二、三产业的增加值占GDP的比重和工业增加值占GDP的比重、金融业…

Pandas入门篇(二)-------Dataframe篇5(进阶)(Dataframe的时间序列Dataframe最终篇!!)(机器学习前置技术栈)

目录 概述一、pandas的日期类型&#xff08;一&#xff09;datetime64类型的特点&#xff08;二&#xff09; 时间序列的创建1.从字符串创建datetime64类型2. 整数&#xff08;Unix时间戳&#xff09;创建datetime64类型3.导入数据时直接转换 &#xff08;三&#xff09;dateti…

打印机-STM32版本 硬件部分

最终PCB EDA工程: 一、确定芯片型号 根据项目需求&#xff0c;梳理需要用到的功能&#xff0c; 电量检测&#xff1a;ADC 按键&#xff1a;IO input外部中断 LED&#xff1a;IO output 温度检测&#xff1a;ADC 电机控制&#xff1a;IO output 打印通讯&#xff1a;SPI …

C++string类使用大全

目录 温馨提示&#xff1a;这篇文章有约两万字 什么是string类&#xff1f; 一. 定义和初始化string对象 1.string的构造函数的形式&#xff1a; 2.拷贝赋值运算符 3.assign函数 二.string对象上的操作 1.读写string对象 2.读取未知数量的string对象 3.使用getline …

windows ubuntu sed,awk,grep篇:10.awk 变量的操作符

目录 62. 变量 64. 算术操作符 65. 字符串操作符 66. 赋值操作符 67. 比较操作符 68. 正则表达式操作符 62. 变量 Awk 变量以字母开头&#xff0c;后续字符可以是数字、字母、或下划线。关键字不能用作 awk 变量。 不像其他编程语言&#xff0c; awk 变量可以直接使…

实习面试之算法准备:数学题

目录 1 技巧2 例题2.1 Nim 游戏2.2 石子游戏2.3 灯泡开关 1 技巧 稍加思考&#xff0c;找到规律 2 例题 2.1 Nim 游戏 你和你的朋友&#xff0c;两个人一起玩 Nim 游戏&#xff1a; 桌子上有一堆石头。 你们轮流进行自己的回合&#xff0c; 你作为先手 。 每一回合&#xf…

SpringBoot 打包所有依赖

SpringBoot 项目打包的时候可以通过插件 spring-boot-maven-plugin 来 repackage 项目&#xff0c;使得打的包中包含所有依赖&#xff0c;可以直接运行。例如&#xff1a; <plugins><plugin><groupId>org.springframework.boot</groupId><artifact…

2024五一杯数学建模B题思路代码文章教学-交通需求规划与可达率问题

交通需求规划与可达率问题 问题总结&#xff1a; 问题一&#xff1a;在一个小型交通网络中&#xff0c;给定的起点和终点之间的交通需求需分配到相应路径上。目标是最大化任意一条路段出现突发状况时的交通需求期望可达率。 问题二&#xff1a;在一个较大的交通网络中&#xff…

负债56亿,购买理财产品遭违约,操纵虚假粉丝,流量在下滑,客户数量减少,汽车之家面临大量风险(三)

本文由猛兽财经历时5个多月完成。猛兽财经将通过以下二十二个章节、8万字以上的内容来全面、深度的分析汽车之家这家公司。 由于篇幅限制&#xff0c;全文分为&#xff08;一&#xff09;到&#xff08;十&#xff09;篇发布。 本文为全文的第七章、第八章、第九章。 目录 …

【Linux—进程间通信】共享内存的原理、创建及使用

什么是共享内存 共享内存是一种计算机编程中的技术&#xff0c;它允许多个进程访问同一块内存区域&#xff0c;以此作为进程间通信&#xff08;IPC, Inter-Process Communication&#xff09;的一种方式。这种方式相对于管道、套接字等通信手段&#xff0c;具有更高的效率&…

一文入门交叉编译

前言: 在阅读本文之前&#xff0c;你哦需要了解makefile文件的编写规则&#xff0c;这里我们推荐两篇入门: Makefile 规则-CSDN博客 Makefile 快速入门-CSDN博客 编译定义 编译是指将源代码文件&#xff08;如C/C文件&#xff09;经过预处理、编译、汇编和链接等步骤&#x…

如何从0深入PostgreSQL内核写一个执行器算子?

如何从0深入PostgreSQL内核写一个执行器算子&#xff1f; 大家好&#xff0c;我叫光城&#xff0c;昨天分享了一个主题&#xff1a;如何从0深入PostgreSQL内核写一个执行器算子&#xff1f;今天来总结一下&#xff0c;本篇文章的直播回放可以在b站观看&#xff0c;点击原文或者…

[PS小技能学习]抠图和切图

详情见视频教程&#xff1a;PS小技巧--抠图与切图 今天我们来学习如何使用PS对表情包合辑进行抠图和裁剪保存 1、首先&#xff0c;将图片导入&#xff0c;双击图层新建一个图层 2、然后点击工具栏的魔棒工具&#xff0c;再点击顶部菜单栏的添加到选区 3、点击图片的空白区域即…

IMU状态预积分功能实现与测试

IMU状态预积分功能实现与测试 前言实现IMU状态预积分类测试程序验证预积分与直接积分的效果结果 前言 预积分&#xff1a;是一种十分常见的IMU数据处理方法。 与传统的IMU运动学积分不同&#xff0c;预积分可以将一段时间内的IMU测量数据累积&#xff0c;建立预积分测量&#…

两院院士泌尿外科专家吴阶平教授

吴阶平&#xff08;1917-2011&#xff09;&#xff0c;男&#xff0c;江苏常州人&#xff0c;1933年天津汇文中学毕业&#xff0c;保送到北平燕京大学医预科&#xff0c;1937年毕业于北平燕京大学获理学士学位&#xff0c;1942年毕业于北平协和医学院获医学博士学位&#xff0c…

银行卡归属地查询API接口快速对接

银行卡归属地查询API接口指的是通过银行卡号查询该银行卡详细信息&#xff0c;包括银行卡名称、卡种、卡品牌、发卡行、编号以及归属地等信息&#xff0c;支持一千多家银行返回归属地信息&#xff0c;那么银行卡归属地查询API接口如何快速对接呢&#xff1f; 首先找到有做银行…

SpringBoot集成Kafka开发

4.SpringBoot集成Kafka开发 4.1 创建项目 4.2 配置文件 application.yml spring:application:name: spring-boot-01-kafka-basekafka:bootstrap-servers: 192.168.2.118:90924.3 创建生产者 package com.zzc.producer;import jakarta.annotation.Resource; import org.spri…

Thread类及常见方法

目录 1.Thread类概念 2.Thread的常见构造方法 3.Thread的几个常见属性 4.启动一个线程—start( ) 5.中断一个线程 1.使用自定义的变量来作为标志位 2.使用interrupt() 3.观察标志位是否被清除 6.等待一个线程-join() 7.获取当前线程引用 8.休眠当前线程 1.Thread类概…

GitHub Copilot 简单使用

因为公司安全原因&#xff0c;并不允许在工作中使用GitHub Copilot&#xff0c;所以&#xff0c;一直没怎么使用。最近因为有一些其它任务&#xff0c;所以&#xff0c;试用了一下&#xff0c;感觉还是很不错的。&#xff08;主要是C和Python编程&#xff09; 一&#xff1a;常…