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

前言

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

@ConditionalOnMissingBean原理解析

@Conditional注解

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

实现Condition接口

接下来查看OnBeanCondition.class实现

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

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

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

OnBeanCondition

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

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

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

OnBeanCondition.getMatchOutcome()

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

OnBeanCondition.getMatchingBeans()

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

OnBeanCondition.getBeanNamesForType()

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

OnBeanCondition.collectBeanNamesForType()

跟踪到collectBeanNamesForType()方法

ListableBeanFactory.getBeanNamesForType()

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

DefaultListableBeanFactory.doGetBeanNamesForType()

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

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

AbstractBeanFactory.isTypeMatch()

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

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

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

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

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

再往上找寻可以找到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注解

自定义@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也是一样的

新建一个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被初始化

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

读源码的心得

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

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

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

结尾

本文章源自《Learn SpringBoot》专栏,感兴趣的话还请关注点赞收藏.

上一篇文章:《SpringBoot核心特性------教你如何扩展ApplicationContext

相关推荐
P.H. Infinity2 分钟前
【RabbitMQ】04-发送者可靠性
java·rabbitmq·java-rabbitmq
生命几十年3万天5 分钟前
java的threadlocal为何内存泄漏
java
caridle17 分钟前
教程:使用 InterBase Express 访问数据库(五):TIBTransaction
java·数据库·express
^velpro^22 分钟前
数据库连接池的创建
java·开发语言·数据库
苹果醋326 分钟前
Java8->Java19的初步探索
java·运维·spring boot·mysql·nginx
秋の花30 分钟前
【JAVA基础】Java集合基础
java·开发语言·windows
小松学前端33 分钟前
第六章 7.0 LinkList
java·开发语言·网络
Wx-bishekaifayuan40 分钟前
django电商易购系统-计算机设计毕业源码61059
java·spring boot·spring·spring cloud·django·sqlite·guava
customer0844 分钟前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
全栈开发圈1 小时前
新书速览|Java网络爬虫精解与实践
java·开发语言·爬虫