前言
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注解的实现原理其实大致已经理清了,总的来说就是
SpringBootCondition
抽象父类实现org.springframework.context.annotation.Condition
接口OnBeanCondition
这个具体子类则根据BeanFactory查询Spring IOC中是否存在某个类型的Java Bean- 最终若存在某个类型的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》专栏,感兴趣的话还请关注点赞收藏.