Spring Boot系列之条件注解

news/2024/5/15 12:48:52

概述

想要搞懂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.mrgr.cn/p/84576154

相关文章

Web前端安全问题分类综合以及XSS、CSRF、SQL注入、DoS/DDoS攻击、会话劫持、点击劫持等详解,增强生产安全意识

前端安全问题是指发生在浏览器、单页面应用、Web页面等前端环境中的各类安全隐患。Web前端作为与用户直接交互的界面&#xff0c;其安全性问题直接关系到用户体验和数据安全。近年来&#xff0c;随着前端技术的快速发展&#xff0c;Web前端安全问题也日益凸显。因此&#xff0c…

环境安装:python环境迁移(无网和有网)

前言 环境部署或迁移是一项简单而又考验应对能力的一项工作&#xff0c;需要考虑到网络环境的情况&#xff0c;无网环境下需要采取离线方式进行操作&#xff0c;有网环境则可以直接通过在线安装完成。 在进行Python环境迁移时&#xff0c;需要注意保持环境的一致性&#xff0c;…

html-docx-js 导出word

1:列表页面按钮<el-button type="warning" plain icon="el-icon-download" size="mini" @click="exportWorddata" >导出word</el-button>…

vue2集成ElementUI编写登录页面

目录 1. 整理目录文件&#xff1a; a. app.vue文件如下&#xff1a; b. Login.vue文件如下&#xff1a; c. router/index.js文件如下&#xff1a; d. 删除components中的文件&#xff1a; e. 最终项目目录整理如下&#xff1a; 2. 集成ElementUI编写登录页面 a. 安装El…

notepadd++下载

现在网上下载个软件越来越麻烦了,各种广告各种隐藏陷阱。 为了大家以后能更快速的下载到正版的notepad++ 这里上传到git做了备份下载地址 : https://gitee.com/liu_chaos/notepad-8.5.3-official-version/blob/master/Notepad_downloader.8.5.3.exe本文来自博客园,作者:lan…

AI视频分析技术的常用开源模型及TSINGSEE青犀AI视频识别分析能力介绍

AI视频分析技术是指利用人工智能技术来对视频数据进行分析和处理的技术。开源模型是指可以免费获取和使用的代码模型&#xff0c;可以帮助开发人员快速构建和部署AI视频分析应用程序。 以下是一些业内常用的用于AI视频分析技术的开源模型&#xff1a; OpenCV&#xff1a;Open…

【稳定检索|投稿优惠】2024年应用数学、建模与计算机工程国际会议(IASAMCE 2024)

2024 International Conference on Applied Mathematics, Modeling, and Computer Engineering 一、大会信息 会议名称&#xff1a;2024年应用数学、建模与计算机工程国际会议 会议简称&#xff1a;IASAMCE 2024 收录检索&#xff1a;提交Ei Compendex,CPCI,CNKI,Google Schola…

linux安装selenium步骤

1,安装selenium模块pip3 install selenium2,安装谷歌浏览器yum install https://dl.google.com/linux/direct/google-chrome-stable_current_x86_64.rpm -y3安装chromedriver1)运行下面命令查看浏览器版本google-chrome --version 出现这个代表谷歌浏览器安装成功 2)谷歌浏览…

tada68键盘的设置

一、到网址:https://config.qmk.fm/,选择tada68 二、选择键位层,设置键位时可从键位码直接拖拽即可,此处设计0键位层,键位是:此处设计1键位层,键位是:键位码:三、设置好了,可以编译了,点击右上角的编译图标编译过程会出来一个大土豆旋转,直到出现:Checking file s…

Spring AI 抢先体验,5 分钟玩转 Java AI 应用开发

Spring Cloud Alibaba AI 以 Spring AI 为基础,并在此基础上提供阿里云通义系列大模型全面适配,让用户在 5 分钟内开发基于通义大模型的 Java AI 应用。作者:刘军 Spring AI 是 Spring 官方社区项目,旨在简化 Java AI 应用程序开发,让 Java 开发者像使用 Spring 开发普通应…

GPT的全面历史和演变:从GPT-1到GPT-4

人工智能新篇章&#xff1a;GPT-4与人类互动的未来&#xff01; 本文探讨了生成式预训练 Transformer (GPT) 的显着演变&#xff0c;提供了从开创性的 GPT-1 到复杂的 GPT-4 的旅程。 每次迭代都标志着重大的技术飞跃&#xff0c;深刻影响人工智能领域以及我们与技术的互动。 我…

开发工具IDEA

IDEA个人使用偏好,笔记。图居多,文字少,可自行参考(个人使用版本:2021.2.2)由于某些原因限制,所以不解释为什么还不更新使用新版本,可自行检索,答案很少,但存在即合理。有疑问可私信! 下载 步骤: 进官网 官网,https://www.jetbrains.com.cn/ 切换语言选择工具进入…

将针孔模型相机 应用到3DGS

Motivation 3DGS 的 投影采用的是 CG系的投影矩阵 P P P, 默认相机的 principal point (相机光心) 位于图像的中点处。但是 实际应用的 绝大多数的 相机 并不满足这样一个设定&#xff0c; 因此我们 需要根据 f , c x , c y {f,c_x, c_y} f,cx​,cy​ 这几个参数重新构建3D …

IDEA Plugins:Show Comment(快捷显示注释)安装及使用

感谢友情分享此插件的同学--夏生 简介 Show doc comment at the Project view Tree, line End, json, other 在文件树、行末、JSON 等地方显示注释. 说明 强制依赖被引用字段、方法等的注释,若是被引用的对象没有注释,则不会显示 效果下载安装插件:Ctrl+Alt+S 搜索Plugins,…

IDEA Plugins:Show Comment 代码行后快捷显示注释

感谢友情分享此插件的同学--夏生 简介 Show doc comment at the Project view Tree, line End, json, other 在文件树、行末、JSON 等地方显示注释. 说明 强制依赖被引用字段、方法等的注释,若是被引用的对象没有注释,则不会显示 效果下载安装插件:Ctrl+Alt+S 搜索Plugins,…

Window 安装 Python 失败 0x80070643,发生严重错误

问题现象 用安装包在 window 安装python,会遇到没有安装成功,卸载后,再次双击安装包安装的时候,直接无法安装了。 这个问题在github issue中有提到,但是都是 2022 年的时,代码修复,但是在 2024 年,安装的时候还是遇到了这个问题。 测试下来:python 3.7, 3.8 没有这个问…

vue3左树的全选和反选

<el-input v-model"filterText" placeholder"" style"width: 48%"/><el-button type"primary" click"handleSearch" class"ml-2">查找</el-button><el-radio-group v-model"form.choic…

java 数据库编程(一)JDBC连接Sql Server数据库

java 数据库编程(一)JDBC连接Sql Server数据库 一、JDBC简介 java数据库连接技术(Java Database Connection,JDBC)是由java提供的一组与平台无关的数据库的操作标准,其本身由一类与接口组成,并且在操作中将按照严格的顺序执行。由于数据库属于资源操作,所以所有的数据库操作…

服务网关GateWay基础

1. 网关基础介绍1.1 网关是什么1.2 为啥要用网关1.3 常见的网关组件NginxNetflix ZuulSpring Cloud GatewayKongAPISIX综合比较 2. gateWay的使用2.1 springCloud整合gateway2.2 GateWay的相关用法2.3 GateWay路由使用示例基本用法转发/重定向负载请求动态路由 2.5 断言(Predic…

记录一次tomcat80端口访问静态资源

根据 域名+/static/weixin/文件名.txt访问对应的文件