Spring之@Conditional源码解析

1.常见@Conditional注解

  • @ConditionalOnClass : 当前环境中存在某个class,bean才会被加载
  • @ConditionalOnBean :Spring中存在某个bean,当前bean才会被加载 (类似于@DependsOn的用法)
  • @ConditionalOnMissingBean :Spring中不存在某个bean,当前bean才会被加载
  • @ConditionalOnProperty :当前环境中存在某个property,bean才会被加载

2.举例演示@ConditionalOnBean注解作用

代码准备

1.创建实体类ComponentA
package com.test.conditional.component;

import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.stereotype.Component;

@Component
@ConditionalOnBean(type ="com.test.conditional.component.ComponentB")
public class ComponentA {
}
2.创建配置类AppConfig
package com.test.conditional.config;

import org.springframework.context.annotation.ComponentScan;

@ComponentScan("com.test.conditional")
public class AppConfig {
}
3.创建启动类
package com.test.conditional;

import com.test.conditional.config.AppConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Main {

    public static void main(String[] args) {

        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

        Object bean = context.getBean("componentA");
        System.out.println(bean);
    }
}
4.运行main方法,查看运行结果

BeanFactory中不存在名称为componentA的bean

5.创建实体类ComponentB,满足@Conditional条件
package com.test.conditional.component;

import org.springframework.stereotype.Component;

@Component
public class ComponentB {
}
 6.再次运行main方法,查看运行结果

BeanFactory中存在名称为componentA的bean

3.源码解析

查看枚举类ConfigurationCondition

枚举

  • PARSE_CONFIGURATION
  • REGISTER_BEAN

通过这个枚举,我们可以推断Spring在解析@Conditional注解的过程中,分成两个阶段(@Conditional注解可以指定生效阶段)

枚举类ConfigurationPhase在Spring源码中的相关应用(Spring解析顺序)

PARSE_CONFIGURATION解析阶段
ConfigurationClassParser#processConfigurationClass

REGISTER_BEAN解析阶段
ConfigurationClassParser#doProcessConfigurationClass

ConfigurationClassBeanDefinitionReader#loadBeanDefinitionsForConfigurationClass 

在上文中我们演示了@ConditionalOnBean注解的作用:如果相关bean存在,则正确实例化bean,如果相关bean不存在,则会抛出NoSuchBeanDefinitionException 。

为什么会抛出这个异常?而不是类似BeanNotFoundException这样的异常,其实答案就在上图中(我重新截了一下图,将关键代码框出来)

如果BeanFactory中不存在相关bean,则trackedConditionEvaluator.shouldSkip方法返回true,继而会删除相关BeanDefinition,所以会抛出NoSuchBeanDefinitionException异常

小结

根据上述截图及相关分析,@Conditional注解大概会有下列四个阶段

  • step1:如果存在一个在PARSE_CONFIGURATION阶段生效的@Conditional注解,且未匹配上,配置类不解析
  • step2:如果存在一个在REGISTER_BEAN阶段生效的@Conditional注解,如果存在@ComponentScan注解,且未匹配上,则@ComponentScan注解不生效
  • step3:如果存在一个在REGISTER_BEAN阶段生效的@Conditional注解,且未匹配上,则bean相关的建模对象BeanDefinition,会从BeanFactory中移除,即后期BeanFactory中也不会存在相关bean
  • step4:如果存在一个在REGISTER_BEAN阶段生效的@Conditional注解,@Bean标注的方法上存在@Conditional注解,则也需要满足具体@Conditional注解的matches方法

查看ConditionEvaluator#shouldSkip方法

关注点一

我们未指定当前解析阶段,如果metadata为AnnotationMetadata,且metadata为配置类,则当前解析阶段为PARSE_CONFIGURATION,否则当前阶段为REGISTER_BEAN

关注点二

如果condition为ConfigurationCondition的子类,则获取其生效阶段。如果不是ConfigurationCondition的子类或者getConfigurationPhase方法返回null,即所有阶段都生效。如果解析阶段和生效阶段一致,则执行具体condition的mathches方法,如果未匹配上则直接不解析或者将解析生成的BeanDefinition从BeanFactory中移除。

4.分析@ConditionalOnBean注解解析流程

1.@ConditionalOnBean注解的生效阶段是REGISTER_BEAN

2.OnBeanCondition未重写matches方法,主要使用其祖父类SpringBootCondition的matches方法

 3.SpringBootCondition的matches方法调用OnBeanCondition的getMatchOutcome方法

在上述的前置条件下 分析解析流程

  1. 解析AppConfig,其类上存在@ComponentScan注解,执行扫描逻辑
  2. 类ComponentB在注解@ComponentScan的扫描路径上,所以被解析成BeanDefinition
  3. 当执行到ConfigurationClassBeanDefinitionReader#loadBeanDefinitionsForConfigurationClass方法,会判断类ComponentB相关的BeanDefinition需不需要skip,由于该类上存在@ConditionalOnBean注解且未满足条件(解析阶段和生效阶段一致,会调用matches方法),所以从BeanFactory中移除相关BeanDefinition
  4. 调用getBean方法 抛出NoSuchBeanDefinitionException

5.综合案例演示@Conditional注解的作用

前置代码使用上文中的

1.自定义两个分别在PARSE_CONFIGURATION和REGISTER_BEAN阶段生效的@Conditional注解

package com.test.conditional.annotation;

import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.ConfigurationCondition;
import org.springframework.core.type.AnnotatedTypeMetadata;

public class ParseConfigurationCondition implements ConfigurationCondition {

    @Override
    public ConfigurationPhase getConfigurationPhase() {
        return ConfigurationPhase.PARSE_CONFIGURATION;
    }

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        return false;
    }
}
package com.test.conditional.annotation;

import org.springframework.context.annotation.Conditional;

import java.lang.annotation.*;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(ParseConfigurationCondition.class)
public @interface ParseConfigurationConditional {

}
package com.test.conditional.annotation;

import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.ConfigurationCondition;
import org.springframework.core.type.AnnotatedTypeMetadata;

public class RegisterBeanCondition implements ConfigurationCondition {

    @Override
    public ConfigurationPhase getConfigurationPhase() {
        return ConfigurationPhase.REGISTER_BEAN;
    }

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        return true;
    }
}
package com.test.conditional.annotation;

import org.springframework.context.annotation.Conditional;

import java.lang.annotation.*;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(RegisterBeanCondition.class)
public @interface RegisterBeanConditional {

}

2.新增ScannerImport类 在REGISTER_BEAN阶段生效的@Conditional注解作用在类上

package com.test.conditional.imports;

import com.test.conditional.annotation.RegisterBeanConditional;
import org.springframework.context.annotation.ComponentScan;

@ComponentScan("com.test.conditional.component")
@RegisterBeanConditional
public class ScannerImport {
}

3.新增DemoComponent类

package com.test.conditional.component;

import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConditionalOnBean(type ="com.test.conditional.component.ComponentC")
public class DemoComponent {

    @Bean
    public Demo demo() {
        return new Demo();
    }

    static class Demo {

    }
}

4.修改配置类 在PARSE_CONFIGURATION阶段生效的@Conditional注解作用在类上

package com.test.conditional.config;

import com.test.conditional.annotation.ParseConfigurationConditional;
import com.test.conditional.imports.ScannerImport;
import org.springframework.context.annotation.Import;


@ParseConfigurationConditional
@Import(ScannerImport.class)
public class AppConfig {

}

5.修改启动类

package com.test.conditional;

import com.test.conditional.config.AppConfig;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;


public class Main {

    public static void main(String[] args) {

        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        // AnnotationConfigApplicationContext有参构造方法会将传入的class解析成BeanDefinition
        // 但是该class如果存在@Conditional相关注解 可能被忽略 为了避免这个因素影响 手动注册一个BeanDefinition
        // 有兴趣的小伙伴可以阅读源码AnnotatedBeanDefinitionReader#doRegisterBean
        GenericBeanDefinition genericBeanDefinition = new GenericBeanDefinition();
        genericBeanDefinition.setBeanClass(AppConfig.class);
        context.registerBeanDefinition("appConfig", genericBeanDefinition);

        context.refresh();

        try {
            Object bean1 = context.getBean("appConfig");
            System.out.println(bean1);

            try {
                Object bean2 = context.getBean("componentB");
                System.out.println(bean2);
            } catch (BeansException e2) {
                System.out.println(e2.getMessage());
            }

        } catch (BeansException e1) {
            System.out.println(e1.getMessage());
            try {
                Object bean2 = context.getBean("componentB");
                System.out.println(bean2);
            } catch (BeansException e2) {
                System.out.println(e2.getMessage());
            }
        } finally {
            try {
                Object bean3 = context.getBean("demo");
                System.out.println(bean3);
            } catch (BeansException e3) {
                System.out.println(e3.getMessage());
            }
        }
    }
}

6.Spring解析流程

  1. 解析AppConfig:其类上存在@Import注解,将@Import注解注入的类当成配置类解析
  2. 解析ScannerImport :其类上存在@ComponentScan注解,会扫描指定路径,将路径包含@Component注解的类 扫描解析成BeanDefinition 后期实例化成bean
  3. DemoComponent在上述的扫描路径中 类和标注了@Bean注解的方法 最终都会解析成bean

7.工程文件结构

8.启动main方法 查看运行结果

分析原因

  1. AppConfig存在PARSE_CONFIGURATION阶段生效且未匹配上的@Conditional注解,所以不解析@Import注解
  2. 由于不解析@Import注解,ScannerImport类上的@ComponentScan注解不生效,所以名称为componentA,componentB,demoComponent,demo的几个bean都不存在
  3. 由于类AppConfig不存在REGISTER_BEAN阶段生效且未匹配上的@Conditional注解,不移除相关BeanDefinition ,所以存在名称为appConfig的bean

9.将ParseConfigurationCondition的matches方法改成返回true,重新运行main方法

分析原因

  1. AppConfig存在PARSE_CONFIGURATION阶段生效且匹配上的@Conditional注解,所以解析@Import注解
  2. ScannerImport类上的@ComponentScan注解生效,所以名称为componentA,componentB存在,由于DemoComponent类上存在@ConditionalOnBean注解,且未匹配上,所有不存在名称为demoComponent的bean,由于名称为demo的bean依赖名称为demoComponent的bean,所以名称为demo的bean也不存在
  3. 由于类AppConfig不存在REGISTER_BEAN阶段生效且未匹配上的@Conditional注解,不移除相关BeanDefinition ,所以存在名称为appConfig的bean

10.添加类ComponnetC,重新运行main方法

package com.test.conditional.component;

import org.springframework.stereotype.Component;

@Component
public class ComponentC {

}

 Spring满足@Conditional注解在生效阶段所需要的条件 所以所有bean都能正确创建

6.总结

Spring对@Conditional注解的处理分成两个阶段

@Conditional注解可以指定在那个阶段生效或者所有阶段都生效

能否匹配上@Conditional注解,可以查阅具体@Conditional注解注入类的mathces方法(子类没有重新就查找其父类的matches方法)

  • 18
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值