SpringBoot核心特性——教你如何自定义@Conditional...条件装配

3552 篇文章 111 订阅

前言

Spring提供了众多的@Conditional注解(@ConditionalOnBean、@ConditionalOnProperty、@ConditionalOnMissingBean...),这些注解可以让我们非常方便地根据不同条件灵活决定Java Bean是否要被Spring IOC容器加载,接下来便通过浅读@ConditionalOnMissingBean原理实现一个自定义的@Conditional注解

@ConditionalOnMissingBean原理解析

@Conditional注解

查看@ConditionalOnMissingBean源码,可以看到它除了定义了一些属性外,还继承了@Conditional注解

image.png

实现Condition接口

接下来查看OnBeanCondition.class实现

image.png

再往上查看它所继承的父类,最终找到SpringBootCondition,可以看到这个类实现了Condition接口,那么不难猜出@Conditional实现的原理,应该就是实现Condition接口

image.png

Spring会根据Condition接口matches()方法返回值,判断当前这个被@Conditional注解修饰的Java Bean,是否要被Spring IOC容器加载

image.png

SpringBootCondition的matches()实现很简单,短短几行代码

image.png

OnBeanCondition

接下来看看OnBeanCondition是如何实现SpringBootCondition的getMatchOutcome()抽象方法的

Tips:为了断点跟踪源码,我事先在业务Service上打上了@ConditionalOnMissingBean注解

image.png

同时创建了RedisConfig,并且标记了@Component注解,所以RedisService不会被初始化,因为条件不满足

image.png

OnBeanCondition.getMatchOutcome()

可以看到首先读取出需要判断当前是否缺失的Java Bean是什么

image.png

OnBeanCondition.getMatchingBeans()

接下来查看getMatchingBeans()方法实现

image.png

OnBeanCondition.getBeanNamesForType()

接着顺藤摸瓜深入getBeanNamesForType()方法的实现

image.png

OnBeanCondition.collectBeanNamesForType()

跟踪到collectBeanNamesForType()方法

image.png

ListableBeanFactory.getBeanNamesForType()

可以看到调用了ListableBeanFactory的getBeanNamesForType()方法,而type就是我在@ConditionalOnMissingBean注解里传入的RedisConfig.class的全限定类名

image.png

DefaultListableBeanFactory.doGetBeanNamesForType()

跟踪到DefaultListableBeanFactory的doGetBeanNamesForType()方法,可以看到在循环遍历当前的BeanDefinition。

因为我事先在RedisConfig中加上了@Component注解,所以此时BeanDefinition是存在RedisConfig的。

image.png

AbstractBeanFactory.isTypeMatch()

最终是来到了isTypeMatch()方法,根据BeanDefintion的名称查询当前是否存在类型为RedisConfig的Java Bean

image.png

image.png

image.png

查询到的确存在类型为RedisConfig的Java Bean,最后终在DefaultListableBeanFactory的doGetBeanNamesForType()方法中,matchFound为true,result长度不为0并返回

image.png

最终在OnBeanCondition的getMatchOutcome()方法中,matchResult根据类型匹配到结果,返回ConditionOutcome.noMatch()

image.png

image.png

当前Spring IOC中存在类型为RedisConfig的Java Bean,返回false,表示当前Java Bean不满足装配条件,不予加载。

image.png

Tips:查询到存在类型为RedisConfig,反而要返回noMatch,是因为这是@ConditionalOnMissingBean的实现,当Spring IOC存在相应的Java Bean时,被标记该注解的JavaBean反而不应该被Spring初始化,所以当存在RedisConfig这个Java Bean时,要返回noMatch,也就是false。不存在RedisConfig则返回match,也就是true。

ConditionEvaluator.shouldSkip()

这时候反过来往上查看是谁调用了OnBeanCondition的matches()方法,最终找到ConditionEvaluator的shouldSkip()方法。

根据方法名和注释也很容易看出,根据@Conditional注解决定是否跳过某项Java Bean

image.png

image.png

再往上找寻可以找到ConfigurationClassBeanDefinitionReader、ConfigurationClassPostProcessor等,这个可以自行继续深挖完整的调用链路

总结

到这里其实@ConditionalOnMissingBean注解的实现原理其实大致已经理清了,总的来说就是

  1. SpringBootCondition抽象父类实现org.springframework.context.annotation.Condition接口
  2. OnBeanCondition这个具体子类则根据BeanFactory查询Spring IOC中是否存在某个类型的Java Bean
  3. 最终若存在某个类型的Java Bean,那么在ConditionEvaluator的shouldSkip()方法中,则决定了某个Java类不会被Spring IOC管理
一通百通

除了@ConditionalOnMissingBean,别的什么@ConditionalOnMissingClass、@OnClassCondition、@ConditionalOnProperty注解原理也是一样的,你会发现它们实际都继承了@Conditional注解

image.png

image.png

image.png

自定义@Conditional

接下来尝试自定义@ConditionalXXX实现。

新建一个@ConditionalOnRedisConfig注解,意义为当Spring IOC中存在类型为RedisConfig的Bean时,标记了该注解的Java类才会被Spring IOC接管并加载。

kotlin复制代码package geek.springboot.application.annotation;  
  
import geek.springboot.application.conditional.OnRedisConfig;  
import org.springframework.context.annotation.Conditional;  
  
import java.lang.annotation.*;  
  
/**  
* 若SpringIOC中存在RedisConfig,被该注解标记的Java类才被Spring IOC接管  
*/  
@Target({ElementType.TYPE, ElementType.METHOD})  
@Retention(RetentionPolicy.RUNTIME)  
@Documented  
@Conditional(OnRedisConfig.class)  
public @interface ConditionalOnRedisConfig {  
  
}

自定义条件判断实现类,代码如下:

kotlin复制代码package geek.springboot.application.conditional;  
  
import geek.springboot.application.configuration.RedisConfig;  
import lombok.extern.slf4j.Slf4j;  
import org.springframework.context.annotation.Condition;  
import org.springframework.context.annotation.ConditionContext;  
import org.springframework.context.annotation.ConfigurationCondition;  
import org.springframework.core.type.AnnotatedTypeMetadata;  
  
  
/**  
* 这里不直接实现Condition接口,因为需要更细粒度地在Spring注册Bean的时候,才进行条件判断,所以实现ConfigurationCondition接口  
* ConfigurationCondition接口也继承自Condition接口  
* {@link org.springframework.context.annotation.ConfigurationCondition}  
*/  
@Slf4j  
public class OnRedisConfig implements ConfigurationCondition {  
  
    @Override  
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {  
        // 获取BeanFactory,并调用它提供的方法来判断当前SpringIOC是否存在类型为RedisConfig的Bean  
        String[] beanNames = context.getBeanFactory().getBeanNamesForType(RedisConfig.class);  
        // 存在返回true,代表符合条件,否则标记了该注解的Java类不会被Spring IOC管理并初始化  
        if (beanNames.length > 0) {  
        return true;  
        }  
        return false;  
    }  
  
    /**  
    * 这里返回的值表示,当前条件判断需要在Bean注册阶段时才进行  
    *  
    * @return {@link org.springframework.context.annotation.ConfigurationCondition.ConfigurationPhase}  
    */  
    @Override  
    public ConfigurationPhase getConfigurationPhase() {  
        return ConfigurationPhase.REGISTER_BEAN;  
    }  
}

这里不直接实现Condition接口,而是实现ConfigurationCondition接口,OnBeanCondition也是一样的

image.png

新建一个RedisConfig,标记了@Component注解,代码如下:

java复制代码package geek.springboot.application.configuration;  
  
import lombok.Data;  
import org.springframework.stereotype.Component;  
  
  
@Data  
@Component  
public class RedisConfig {  
  
    private String ip = "127.0.0.1";  

    private Integer port = 6379;  


    @Override  
    public String toString() {  
        return "RedisConfig{" +  
        "ip='" + ip + '\'' +  
        ", port=" + port +  
        '}';  
    }  
  
}

新建一个RedisService,标记了@ConditionalOnRedisConfig注解,代码如下:

kotlin复制代码package geek.springboot.application.service;  
  
import geek.springboot.application.annotation.ConditionalOnRedisConfig;  
import geek.springboot.application.configuration.RedisConfig;  
import lombok.extern.slf4j.Slf4j;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.stereotype.Service;  
  
import javax.annotation.PostConstruct;  
  
@Slf4j  
@ConditionalOnRedisConfig  
@Service  
public class RedisService {  
  
    @Autowired  
    private RedisConfig redisConfig;  
  
    @PostConstruct  
    public void init() {  
        log.info("redis service start connect... config is {}", this.redisConfig);  
    }  
  
}

启动SpringApplicaiton,控制台输出如下,可以看到RedisService被初始化

image.png

接下来把RedisConfig的@Component注解给删除,重启SpringApplication,可以看到控制台不再有RedisService初始化时的输出打印

image.png

读源码的心得

SpringBoot其实底层很多都是依赖于SpringFramework实现的,所以深入SpringBoot就得非常熟悉SpringFramework。

看源码时主旨就是抓大放小,摸清调用链路,抓到核心思想即可。不用过于追求每个类每个变量每个方法的作用含义都要搞明白。

而且Spring一些方法命名语义化非常好,甚至都不用深入查看方法实现,光是看个方法名就能猜出大概是做什么的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值