条件注解是
Spring4
提供的一种bean加载特性,主要用于控制配置类和bean初始化条件。在springBoot,springCloud
一系列框架底层源码中,条件注解的使用到处可见。
不少人在使用@ConditionalOnBean
注解时会遇到不生效的情况,依赖的 bean 明明已经配置了,但就是不生效。到底@ConditionalOnBean
和bean加载的顺序有没有关系呢?跟着源码,一探究竟。
问题演示:
@Configuration public class Configuration1 { @Bean @ConditionalOnBean(Bean2.class) public Bean1 bean1() { return new Bean1(); } } 复制代码
@Configuration public class Configuration2 { @Bean public Bean2 bean2(){ return new Bean2(); } } 复制代码
结果: @ConditionalOnBean(Bean2.class)
返回false。 命名定义了bean2
,bean1
却未加载。
源码分析
首先要明确一点,条件注解的解析一定发生在spring ioc的bean definition
阶段,因为 spring bean初始化的前提条件就是有对应的bean definition
,条件注解正是通过判断bean definition
来控制bean能否实例化。
对上述示例进行源码调试。
从 bean definition解析的入口开始:ConfigurationClassPostProcessor
@Override public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) { int registryId = System.identityHashCode(registry); if (this.registriesPostProcessed.contains(registryId)) { throw new IllegalStateException( "postProcessBeanDefinitionRegistry already called on this post-processor against " + registry); } if (this.factoriesPostProcessed.contains(registryId)) { throw new IllegalStateException( "postProcessBeanFactory already called on this post-processor against " + registry); } this.registriesPostProcessed.add(registryId); // 解析bean definition入口 processConfigBeanDefinitions(registry); } 复制代码
跟进processConfigBeanDefinitions
方法:
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) { //省略不必要的代码... //解析候选bean,先获取所有的配置类,也就是@Configuration标注的类 parser.parse(candidates); parser.validate(); //配置类存入集合 Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses()); configClasses.removeAll(alreadyParsed); // Read the model and create bean definitions based on its content if (this.reader == null) { this.reader = new ConfigurationClassBeanDefinitionReader( registry, this.sourceExtractor, this.resourceLoader, this.environment, this.importBeanNameGenerator, parser.getImportRegistry()); } //开始解析配置类,也就是条件注解解析的入口 this.reader.loadBeanDefinitions(configClasses); alreadyParsed.addAll(configClasses); //... } 复制代码
跟进条件注解解析入口loadBeanDefinitions
,开始循环解析配置类。这里是所有自定义的配置类和自动装配的配置类,如下:
在解析方法loadBeanDefinitionsForConfigurationClass()
中,会获得配置类中定义bean的所有方法, 并调用loadBeanDefinitionsForBeanMethod()
方法来进行循环解析,解析时会执行如下校验方法,也正是条件注解的入口:
public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable ConfigurationPhase phase) { //判断是否有条件注解,否则直接返回 if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) { return false; } if (phase == null) { if (metadata instanceof AnnotationMetadata && ConfigurationClassUtils.isConfigurationCandidate((AnnotationMetadata) metadata)) { return shouldSkip(metadata, ConfigurationPhase.PARSE_CONFIGURATION); } return shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN); } //获取当前定义bean的方法上,所有的条件注解 List<Condition> conditions = new ArrayList<>(); for (String[] conditionClasses : getConditionClasses(metadata)) { for (String conditionClass : conditionClasses) { Condition condition = getCondition(conditionClass, this.context.getClassLoader()); conditions.add(condition); } } //根据Order来进行排序 AnnotationAwareOrderComparator.sort(conditions); //遍历条件注解,开始执行条件注解的流程 for (Condition condition : conditions) { ConfigurationPhase requiredPhase = null; if (condition instanceof ConfigurationCondition) { requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase(); } //这里执行条件注解的 condition.matches 方法来进行匹配,返回布尔值 if ((requiredPhase == null || requiredPhase == phase) && !condition.matches(this.context, metadata)) { return true; } } return false; } 复制代码
继续跟进条件注解的匹配方法:
这里开始解析示例代码中bean1
的配置:
@Bean @ConditionalOnBean(Bean2.class) public Bean1 bean1() { return new Bean1(); } 复制代码
上述getMatchOutcome
方法中,参数metadata
是要解析的目标bean,也就是bean1
。条件注解依赖的bean
被封装成了BeanSearchSpec
,从名字可以看出是要寻找的对象,这是一个静态内部类,构造方法如下:
BeanSearchSpec(ConditionContext context, AnnotatedTypeMetadata metadata, Class<?> annotationType) { this.annotationType = annotationType; //读取 metadata中的设置的value MultiValueMap<String, Object> attributes = metadata .getAllAnnotationAttributes(annotationType.getName(), true); //设置各参数,根据这些参数进行寻找目标类 collect(attributes, "name", this.names); collect(attributes, "value", this.types); collect(attributes, "type", this.types); collect(attributes, "annotation", this.annotations); collect(attributes, "ignored", this.ignoredTypes); collect(attributes, "ignoredType", this.ignoredTypes); this.strategy = (SearchStrategy) metadata .getAnnotationAttributes(annotationType.getName()).get("search"); BeanTypeDeductionException deductionException = null; try { if (this.types.isEmpty() && this.names.isEmpty()) { addDeducedBeanType(context, metadata, this.types); } } catch (BeanTypeDeductionException ex) { deductionException = ex; } validate(deductionException); } 复制代码
继续跟进搜索bean的方法:
MatchResult matchResult = getMatchingBeans(context, spec); 复制代码
private MatchResult getMatchingBeans(ConditionContext context, BeanSearchSpec beans) { ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); if (beans.getStrategy() == SearchStrategy.ANCESTORS) { BeanFactory parent = beanFactory.getParentBeanFactory(); Assert.isInstanceOf(ConfigurableListableBeanFactory.class, parent, "Unable to use SearchStrategy.PARENTS"); beanFactory = (ConfigurableListableBeanFactory) parent; } MatchResult matchResult = new MatchResult(); boolean considerHierarchy = beans.getStrategy() != SearchStrategy.CURRENT; List<String> beansIgnoredByType = getNamesOfBeansIgnoredByType( beans.getIgnoredTypes(), beanFactory, context, considerHierarchy); //因为实例代码中设置的是类型,所以这里会遍历类型,根据type获取目标bean是否存在 for (String type : beans.getTypes()) { Collection<String> typeMatches = getBeanNamesForType(beanFactory, type, context.getClassLoader(), considerHierarchy); typeMatches.removeAll(beansIgnoredByType); if (typeMatches.isEmpty()) { matchResult.recordUnmatchedType(type); } else { matchResult.recordMatchedType(type, typeMatches); } } //根据注解寻找 for (String annotation : beans.getAnnotations()) { List<String> annotationMatches = Arrays .asList(getBeanNamesForAnnotation(beanFactory, annotation, context.getClassLoader(), considerHierarchy)); annotationMatches.removeAll(beansIgnoredByType); if (annotationMatches.isEmpty()) { matchResult.recordUnmatchedAnnotation(annotation); } else { matchResult.recordMatchedAnnotation(annotation, annotationMatches); } } //根据设置的name进行寻找 for (String beanName : beans.getNames()) { if (!beansIgnoredByType.contains(beanName) && containsBean(beanFactory, beanName, considerHierarchy)) { matchResult.recordMatchedName(beanName); } else { matchResult.recordUnmatchedName(beanName); } } return matchResult; } 复制代码
getBeanNamesForType()
方法最终会委托给BeanTypeRegistry
类的getNamesForType
方法来获取对应的指定类型的bean name:
Set<String> getNamesForType(Class<?> type) { //同步spring容器中的bean updateTypesIfNecessary(); //返回指定类型的bean return this.beanTypes.entrySet().stream() .filter((entry) -> entry.getValue() != null && type.isAssignableFrom(entry.getValue())) .map(Map.Entry::getKey) .collect(Collectors.toCollection(LinkedHashSet::new)); } 复制代码
重点来了。 上述方法中的第一步便是同步bean,也就是获取此时 spring 容器中的所有 beanDifinition。只有这样,条件注解的判断才有意义。
我们跟进updateTypesIfNecessary()
:
private void updateTypesIfNecessary() { //这里lastBeanDefinitionCount 代表已经同步的数量,如果和容器中的数量不相等,才开始同步。 //否则,获取beanFactory迭代器,开始同步。 if (this.lastBeanDefinitionCount != this.beanFactory.getBeanDefinitionCount()) { Iterator<String> names = this.beanFactory.getBeanNamesIterator(); while (names.hasNext()) { String name = names.next(); if (!this.beanTypes.containsKey(name)) { addBeanType(name); } } //同步完之后,更新已同步的beanDefinition数量。 this.lastBeanDefinitionCount = this.beanFactory.getBeanDefinitionCount(); } } 复制代码
离答案只差一步了,就是看一下从beanFactory
中迭代的是哪些beanDefinition
?
跟进beanFactory.getBeanNamesIterator();
方法:
@Override public Iterator<String> getBeanNamesIterator() { CompositeIterator<String> iterator = new CompositeIterator<>(); iterator.add(this.beanDefinitionNames.iterator()); iterator.add(this.manualSingletonNames.iterator()); return iterator; } 复制代码
分别来看:
beanDefinitionNames
就是存储一些自动解析和装配的bean,我们的启动类、配置类、controller、service
等。manualSingletonNames
,从名字可以看出,手工单例名称。什么意思呢?在spring ioc
的过程中,会手动触发一些bean的注册。比如在springboot
启动过程中,会显示的注册一些配置 bean,如:springBootBanner,systemEnvironment,systemProperties
等。
我们来分析一下上面示例bean1
为何没有实例化?
在spring ioc
的过程中,优先解析@Component,@Service,@Controller
注解的类。其次解析配置类,也就是@Configuration
标注的类。最后开始解析配置类中定义的bean
。 示例代码中bean1
是定义在配置类中的,当执行到配置类解析的时候,@Component,@Service,@Controller ,@Configuration
标注的类已经全部被解析,所以这些BeanDifinition
已经被同步。 但是bean1
的条件注解依赖的是bean2
,bean2
是被定义的配置类中的,因为两个Bean
都是配置类中Bean
,所以此时配置类的解析无法保证先后顺序,就会出现不生效的情况。
同样的道理,如果依赖的是FeignClient
,也有可能会出现不生效的情况。因为FeignClient
最终还是由配置类触发,解析的先后顺序也不能保证。
解决
有两种方式:
- 项目中条件注解依赖的类,大多会交给
spring
容器管理,所以如果要在配置中Bean
通过@ConditionalOnBean
依赖配置中的Bean
时,完全可以用@ConditionalOnClass(Bean2.class)
来代替。 - 如果一定要区分两个配置类的先后顺序,可以将这两个类交与
EnableAutoConfiguration
管理和触发。也就是定义在META-INF\spring.factories
中声明是配置类,然后通过@AutoConfigureBefore、AutoConfigureAfter、AutoConfigureOrder
控制先后顺序。因为这三个注解只对自动配置类生效。
总结
在配置类中定义Bean
,如果使用@ConditionalOnBean
依赖的也是配置类中Bean
,则执行结果不可控,和配置类加载顺序有关。