锋盈数科-知识库 Logo
首页
软件开发
计算机基础
Hello Halo
新手必读
关于本知识库
登录 →
锋盈数科-知识库 Logo
首页 软件开发 计算机基础 Hello Halo 新手必读 关于本知识库
登录
  1. 首页
  2. 软件开发
  3. JAVA
  4. Spring Boot系列之条件注解

Spring Boot系列之条件注解

0
  • JAVA
  • 发布于 2024-08-14
  • 0 次阅读
黄健
黄健

本文由 简悦 SimpRead 转码, 原文地址 blog.csdn.net

概述

想要搞懂 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 {
	@Override
	protected 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.class和FreeMarkerConfigurationFactory.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注解所修饰,不会skip
    if (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 {
    @Override
    public 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;
    }
}

参考

标签: #软件开发 1171 #JAVA 991
相关文章

Spring 实现 3 种异步接口 2024-10-18 09:07

大家好,我是苏三~ 如何处理比较耗时的接口? 这题我熟,直接上异步接口,使用 Callable、WebAsyncTask 和 DeferredResult、CompletableFuture等均可实现。 但这些方法有局限性,处理结果仅返回单个值。在某些场景下,如果需要接口异步处理的同时,还持续不断地

重学SpringBoot3-集成Redis(五)之布隆过滤器 2024-10-08 11:24

更多SpringBoot3内容请关注我的专栏:《SpringBoot3》 期待您的点赞👍收藏⭐评论✍ 重学SpringBoot3-集成Redis(五)之布隆过滤器 1. 什么是布隆过滤器? * 基本概念 适用场景 2. 使用 Redis 实现布隆过滤器 * 项目依赖 Redis 配置

SpringBoot整合异步任务执行 2024-10-08 11:24

同步任务: 同步任务是在单线程中按顺序执行,每次只有一个任务在执行,不会引发线程安全和数据一致性等 并发问题 同步任务需要等待任务执行完成后才能执行下一个任务,无法同时处理多个任务,响应慢,影响用 户体验 异步任务: 异步任务是在多线程中同时执行,多个任务可以并发执行,同时处理多个请求,响应快,资源

springboot kafka多数据源,通过配置动态加载发送者和消费者 2024-10-08 11:24

前言 最近做项目,需要支持kafka多数据源,实际上我们也可以通过代码固定写死多套kafka集群逻辑,但是如果需要不修改代码扩展呢,因为kafka本身不处理额外逻辑,只是起到削峰,和数据的传递,那么就需要对架构做一定的设计了。 准备test kafka本身非常容易上手,如果我们需要单元测试,引入ja

SpringBoot 集成 Redis 2024-10-08 11:24

一:SpringBoot 集成 Redis ①Redis是一个 NoSQL(not only)数据库, 常作用缓存 Cache 使用。 ②Redis是一个中间件、是一个独立的服务器;常用的数据类型: string , hash ,set ,zset , list ③通过Redis客户端可以使用多种语

SpringBoot整合QQ邮箱 2024-10-08 11:24

SpringBoot可以通过导入依赖的方式集成多种技术,这当然少不了我们常用的邮箱,现在本章演示SpringBoot整合QQ邮箱发送邮件…. 下面按步骤进行: 1.获取QQ邮箱授权码 1.1 登录QQ邮箱 1.2 开启SMTP服务 找到下图中的SMTP服务区域,如果当前账号未开启的话自己手动开启。

目录

IT 外包服务商

  • 意见投递
  • zyf6619

软件开发应用

主菜单

  • 首页
  • 软件开发
  • 计算机基础
  • Hello Halo
  • 新手必读
  • 关于本知识库
Copyright © 2024 your company All Rights Reserved. Powered by Halo.