前言

不知道大家在使用Spring Boot开发的日常中有没有用过@Conditionalxxx注解,比如@ConditionalOnMissingBean。相信看过Spring Boot源码的朋友一定不陌生。

@Conditionalxxx这类注解表示某种判断条件成立时才会执行相关操作。掌握该类注解,有助于日常开发,框架的搭建。

今天这篇文章就从前世今生介绍一下该类注解。

Spring Boot 版本

本文基于的Spring Boot的版本是2.3.4.RELEASE

@Conditional

@Conditional注解是从Spring4.0才有的,可以用在任何类型或者方法上面,通过@Conditional注解可以配置一些条件判断,当所有条件都满足的时候,被@Conditional标注的目标才会被Spring容器处理。

@Conditional的使用很广,比如控制某个Bean是否需要注册,在Spring Boot中的变形很多,比如@ConditionalOnMissingBean@ConditionalOnBean等等,如下:

该注解的源码其实很简单,只有一个属性value,表示判断的条件(一个或者多个),是org.springframework.context.annotation.Condition类型,源码如下:

@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注解实现的原理很简单,就是通过org.springframework.context.annotation.Condition这个接口判断是否应该执行操作。

Condition接口

@Conditional注解判断条件与否取决于value属性指定的Condition实现,其中有一个matches()方法,返回true表示条件成立,反之不成立,接口如下:

@FunctionalInterface
public interface Condition {
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}

matches中的两个参数如下:

  1. context:条件上下文,ConditionContext接口类型的,可以用来获取容器中上下文信息。
  2. metadata:用来获取被@Conditional标注的对象上的所有注解信息

ConditionContext接口

这个接口很重要,能够从中获取Spring上下文的很多信息,比如ConfigurableListableBeanFactory,源码如下:

public interface ConditionContext {

    /**
* 返回bean定义注册器,可以通过注册器获取bean定义的各种配置信息
*/
BeanDefinitionRegistry getRegistry(); /**
* 返回ConfigurableListableBeanFactory类型的bean工厂,相当于一个ioc容器对象
*/
@Nullable
ConfigurableListableBeanFactory getBeanFactory(); /**
* 返回当前spring容器的环境配置信息对象
*/
Environment getEnvironment(); /**
* 返回资源加载器
*/
ResourceLoader getResourceLoader(); /**
* 返回类加载器
*/
@Nullable
ClassLoader getClassLoader();
}

如何自定义Condition?

举个栗子:假设有这样一个需求,需要根据运行环境注入不同的BeanWindows环境和Linux环境注入不同的Bean

实现很简单,分别定义不同环境的判断条件,实现org.springframework.context.annotation.Condition即可。

windows环境的判断条件源码如下

/**
* 操作系统的匹配条件,如果是windows系统,则返回true
*/
public class WindowsCondition implements Condition {
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata metadata) {
//获取当前环境信息
Environment environment = conditionContext.getEnvironment();
//获得当前系统名
String property = environment.getProperty("os.name");
//包含Windows则说明是windows系统,返回true
if (property.contains("Windows")){
return true;
}
return false; }
}

Linux环境判断源码如下

/**
* 操作系统的匹配条件,如果是windows系统,则返回true
*/
public class LinuxCondition implements Condition {
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata metadata) {
Environment environment = conditionContext.getEnvironment(); String property = environment.getProperty("os.name");
if (property.contains("Linux")){
return true;
}
return false; }
}

配置类中结合@Bean注入不同的Bean,如下

@Configuration
public class CustomConfig { /**
* 在Windows环境下注入的Bean为winP
* @return
*/
@Bean("winP")
@Conditional(value = {WindowsCondition.class})
public Person personWin(){
return new Person();
} /**
* 在Linux环境下注入的Bean为LinuxP
* @return
*/
@Bean("LinuxP")
@Conditional(value = {LinuxCondition.class})
public Person personLinux(){
return new Person();
}

简单的测试一下,如下

@SpringBootTest
class SpringbootInterceptApplicationTests { @Autowired(required = false)
@Qualifier(value = "winP")
private Person winP; @Autowired(required = false)
@Qualifier(value = "LinuxP")
private Person linP; @Test
void contextLoads() {
System.out.println(winP);
System.out.println(linP);
}
}

Windows环境下执行单元测试,输出如下

com.example.springbootintercept.domain.Person@885e7ff
null

很显然,判断生效了,Windows环境下只注入了WINP

条件判断在什么时候执行?

条件判断的执行分为两个阶段,如下:

  1. 配置类解析阶段(ConfigurationPhase.PARSE_CONFIGURATION):在这个阶段会得到一批配置类的信息和一些需要注册的Bean

  2. Bean注册阶段(ConfigurationPhase.REGISTER_BEAN):将配置类解析阶段得到的配置类和需要注册的Bean注入到容器中。

默认都是配置解析阶段,其实也就够用了,但是在Spring Boot中使用了ConfigurationCondition,这个接口可以自定义执行阶段,比如@ConditionalOnMissingBean都是在Bean注册阶段执行,因为需要从容器中判断Bean。

这个两个阶段有什么不同呢?:其实很简单的,配置类解析阶段只是将需要加载配置类和一些Bean(被@Conditional注解过滤掉之后)收集起来,而Bean注册阶段是将的收集来的Bean和配置类注入到容器中,如果在配置类解析阶段执行Condition接口的matches()接口去判断某些Bean是否存在IOC容器中,这个显然是不行的,因为这些Bean还未注册到容器中

什么是配置类,有哪些?:类上被@Component@ComponentScan@Import@ImportResource@Configuration标注的以及类中方法有@Bean的方法。如何判断配置类,在源码中有单独的方法:org.springframework.context.annotation.ConfigurationClassUtils#isConfigurationCandidate

ConfigurationCondition接口

这个接口相比于@Condition接口就多了一个getConfigurationPhase()方法,可以自定义执行阶段。源码如下:

public interface ConfigurationCondition extends Condition {

    /**
* 条件判断的阶段,是在解析配置类的时候过滤还是在创建bean的时候过滤
*/
ConfigurationPhase getConfigurationPhase(); /**
* 表示阶段的枚举:2个值
*/
enum ConfigurationPhase { /**
* 配置类解析阶段,如果条件为false,配置类将不会被解析
*/
PARSE_CONFIGURATION, /**
* bean注册阶段,如果为false,bean将不会被注册
*/
REGISTER_BEAN
}
}

这个接口在需要指定执行阶段的时候可以实现,比如需要根据某个Bean是否在IOC容器中来注入指定的Bean,则需要指定执行阶段为Bean的注册阶段ConfigurationPhase.REGISTER_BEAN)。

多个Condition的执行顺序

@Conditional中的Condition判断条件可以指定多个,默认是按照先后顺序执行,如下:

class Condition1 implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
System.out.println(this.getClass().getName());
return true;
}
} class Condition2 implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
System.out.println(this.getClass().getName());
return true;
}
} class Condition3 implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
System.out.println(this.getClass().getName());
return true;
}
} @Configuration
@Conditional({Condition1.class, Condition2.class, Condition3.class})
public class MainConfig5 {
}

上述例子会依次按照Condition1Condition2Condition3执行。

默认按照先后顺序执行,但是当我们需要指定顺序呢?很简单,有如下三种方式:

  1. 实现PriorityOrdered接口,指定优先级
  2. 实现Ordered接口接口,指定优先级
  3. 使用@Order注解来指定优先级

例子如下:

@Order(1)
class Condition1 implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
System.out.println(this.getClass().getName());
return true;
}
} class Condition2 implements Condition, Ordered {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
System.out.println(this.getClass().getName());
return true;
} @Override
public int getOrder() {
return 0;
}
} class Condition3 implements Condition, PriorityOrdered {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
System.out.println(this.getClass().getName());
return true;
} @Override
public int getOrder() {
return 1000;
}
} @Configuration
@Conditional({Condition1.class, Condition2.class, Condition3.class})
public class MainConfig6 {
}

根据排序的规则,PriorityOrdered的会排在前面,然后会再按照order升序,最后可以顺序是:Condtion3->Condtion2->Condtion1

Spring Boot中常用的一些注解

Spring Boot中大量使用了这些注解,常见的注解如下:

  1. @ConditionalOnBean:当容器中有指定Bean的条件下进行实例化。
  2. @ConditionalOnMissingBean:当容器里没有指定Bean的条件下进行实例化。
  3. @ConditionalOnClass:当classpath类路径下有指定类的条件下进行实例化。
  4. @ConditionalOnMissingClass:当类路径下没有指定类的条件下进行实例化。
  5. @ConditionalOnWebApplication:当项目是一个Web项目时进行实例化。
  6. @ConditionalOnNotWebApplication:当项目不是一个Web项目时进行实例化。
  7. @ConditionalOnProperty:当指定的属性有指定的值时进行实例化。
  8. @ConditionalOnExpression:基于SpEL表达式的条件判断。
  9. @ConditionalOnJava:当JVM版本为指定的版本范围时触发实例化。
  10. @ConditionalOnResource:当类路径下有指定的资源时触发实例化。
  11. @ConditionalOnJndi:在JNDI存在的条件下触发实例化。
  12. @ConditionalOnSingleCandidate:当指定的Bean在容器中只有一个,或者有多个但是指定了首选的Bean时触发实例化。

比如在WEB模块的自动配置类WebMvcAutoConfiguration下有这样一段代码:

    @Bean
@ConditionalOnMissingBean
public InternalResourceViewResolver defaultViewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix(this.mvcProperties.getView().getPrefix());
resolver.setSuffix(this.mvcProperties.getView().getSuffix());
return resolver;
}

常见的@Bean@ConditionalOnMissingBean注解结合使用,意思是当容器中没有InternalResourceViewResolver这种类型的Bean才会注入。这样写有什么好处呢?好处很明显,可以让开发者自定义需要的视图解析器,如果没有自定义,则使用默认的,这就是Spring Boot为自定义配置提供的便利。

总结

@Conditional注解在Spring Boot中演变的注解很多,需要着重了解,特别是后期框架整合的时候会大量涉及。

这类注解都不知道,还好意思说会Spring Boot ?的更多相关文章

  1. 【hibernate/JPA】注解方式实现 复合主键【spring boot】

    1>hibernate/JPA实现复合主键的思路:是将所有的主键属性封装在一个主键类中,提供给需要复合主键的实体类使用. 2>主键类的几点要求: . 使用复合主键的实体类必须实现Seria ...

  2. 扩展、接管MVC都不会,还说会Spring Boot?

    持续原创输出,点击上方蓝字关注我 目录 前言 Spring Boot 版本 如何扩展MVC? 如何自定义一个拦截器? 什么都不配置为什么依然能运行MVC相关的功能? 如何全面接管MVC?[不推荐] 为 ...

  3. 扩展、接管MVC都不会,还会用Spring Boot?

    持续原创输出,点击上方蓝字关注我 目录 前言 Spring Boot 版本 如何扩展MVC? 如何自定义一个拦截器? 什么都不配置为什么依然能运行MVC相关的功能? 如何全面接管MVC?[不推荐] 为 ...

  4. Spring Boot 最核心的 25 个注解,都是干货!

    学习和应用 Spring Boot 有一些时间了,你们对 Spring Boot 注解了解有多少呢?今天栈长我给大家整理了 Spring Boot 最核心的 25 个注解,都是干货! 你所需具备的基础 ...

  5. Spring Boot的27个注解【核心】

    导读[约定大于配置] Spring Boot方式的项目开发已经逐步成为Java应用开发领域的主流框架,它不仅可以方便地创建生产级的Spring应用程序,还能轻松地通过一些注解配置与目前比较火热的微服务 ...

  6. 大清朝早亡了,还没有入门 Spring Boot?

    由于读者的数量越来越多,难免会被问到一些我自己都觉得不好意思的问题,比如说前几天小王就问我:"二哥,快教教我,怎么通过 Spring Boot 创建一个 Hello World 项目啊?&q ...

  7. Spring Boot入门系列(十八)整合mybatis,使用注解的方式实现增删改查

    之前介绍了Spring Boot 整合mybatis 使用xml配置的方式实现增删改查,还介绍了自定义mapper 实现复杂多表关联查询.虽然目前 mybatis 使用xml 配置的方式 已经极大减轻 ...

  8. 【spring cloud】子模块module -->导入一个新的spring boot项目作为spring cloud的一个子模块微服务,怎么做/或者 每次导入一个新的spring boot项目,IDEA不识别子module,启动类无法启动/右下角没有蓝色图标

    如题:导入一个新的spring boot项目作为spring cloud的一个子模块微服务,怎么做 或者说每次导入一个新的spring boot项目,IDEA不识别,启动类无法启动,怎么解决 下面分别 ...

  9. spring boot注解

    一.注解(annotations)列表 @SpringBootApplication:包含了@ComponentScan.@Configuration和@EnableAutoConfiguration ...

随机推荐

  1. 11 vue 自定义全局方法

    //global.js// 定义vue 全局方   // 定义vue 全局方法 建议自定义的全局方法加_ 以示区分 export default {   install(Vue, options =  ...

  2. Activiti工作流概述

    本来打算看OCR的但是我手里有的资源是讲的PY的,涉及到CNN和RNN看得有的不太明白,捂脸,所以看看工作流吧,反正也都不会,干啥啥不会 工作流的概念 工作流的概念应该都差不多了解要不也不会搜索这个标 ...

  3. 电子邮箱有哪些隐藏技能,读懂了效率提升N倍!

    很多人将邮箱作为常见的通讯工具,然而,大部分职场人只了解其五分之一的功能.电子邮箱还有很多隐藏技能,身为商务精英的你,必须往下看看哦!今天跟随TOM邮箱小编导,来挖掘下邮箱的潜藏技能吧~ 作为经常外出 ...

  4. Hexo博客迁移

    Hexo用户指南 - 博客迁移 GitHub+Hexo搭建博客的过程比较平滑,但是它的配置却非常耗时,一旦电脑出现问题或者需要在另外一台电脑上写博客,那么Hexo博客的迁移非常就让人头疼.下面参考其他 ...

  5. C/C++ 实现PE文件特征码识别

    PE文件就是我们常说的EXE可执行文件,针对文件特征的识别可以清晰的知道该程序是使用何种编程语言实现的,前提是要有特征库,PE特征识别有多种形式,第一种是静态识别,此方法就是只针对磁盘中文件的特征码字 ...

  6. 一、loadrunner脚本录制及回放

    录制及回放的注意点: 1.测试系统教复杂时,正确的划分action,对监控的每一个业务模型和操作,起到重要作用 2.录制完成后,先进行编译(改动脚本之后检查下有没有语法错误):工具栏Vuser下有一个 ...

  7. python调用接口——requests模块

    前提:安装pip install requests 导入import requests 1.get请求   result=requests.get(url,d).json()  或  .text 2. ...

  8. Kubernetes中的存储(六)

    一.ConfigMap 1,介绍 ConfigMap 功能在 Kuberbetes 1.2 版本中引入,许多应用程序会从配置文件.命令行参数或环境变量中读取配置信息.ConfigMap API 给我们 ...

  9. EF CodeFirst学习

    EF CodeFirst 真是时光如梦,岁月如歌,自己一不留神就被后浪拍在了沙滩上,就连我要学的这个东西也是12年就在博客园很火了. EF有三种开发模式,DatabaseFirst, ModelFir ...

  10. ServletContex对象学习

    问题: 不同的用户使用相同的数据 解决: ServletContext对象 特点: 服务器创建 用户共享 作用域: 整个项目内 生命周期: 服务器启动到服务器关闭 使用: 1.获取SercvletCo ...