深入理解SpringBoot之装配条件

分类:后台开发 热度:

  我们知道自动装配是SpringBoot微服务化的核心,它会把META-INF/spring.factoires里配置的EnableAutoConfiguration注册到IOC容器里。但是,请大家考虑一个问题,根据需求我们要配置一个tomcat的内嵌容器,可是当前的运行环境里都没有servlet的相关API或者说当前的ApplicationContext不是一个WebApplicationContext,如果这样的话,那么创建tomcat的内嵌容器还有什么意义上呢?如果根据需求我们想自动装配一个Mybatis的SqlSessionFactory,可是运行环境里连DataSource都没有,恐怕要自动装配Mybatis的愿望也会落空吧!针对这种问题,SpringBoot早都考虑到了,下面我们来看看SpringBoot是怎么解决的。

一、关于@Conditional

  conditional中文的意思为条件,其本身是Springframework提供的核心注解,通常情况下该注解可以加在类上或者方法上与@Configuration或者@Bean配合使用,当和@Configuration配合使用时,那么该类下所有@Bean方法 或者@Import 或者 @ComponentScan都会受到其配置条件的影响,我们先看一下其源码:

 View Code

在这里文档注释提醒我们去看Condition接口:

 View Code

该接口就有一个方法:matches方法。它定义了最基本的匹配规则,该方法传入两个参数一个是ConditionContext ,该接口定义了若干个方法来获取spring核心接口的方法:

 View Code

在这里我们能获取到BeanFactory,ResourceLoader,Enviroment等。而另外一个参数是AnnotatedTypeMetadata接口,该接口主要获取该类上标记的注解。在这里我先写一个简单的例子,来试验一下:

MyTestConditional:

复制代码
package com.hzgj.lyrk.autoconfigure;

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

import java.util.Map;

public class MyTestConditional implements ConfigurationCondition {
    @Override
    public ConfigurationPhase getConfigurationPhase() {
        return ConfigurationPhase.REGISTER_BEAN;
    }

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        Map<String, Object> map = metadata.getAnnotationAttributes("org.springframework.context.annotation.Description");
        System.out.println(map);
        return false;
    }
}
复制代码

  这个是自定义的Conditional,该类实现了ConfigurationCondition接口,该接口继承了Condition,只不过它多添加了一个用于设置解析Condition阶段的方法,在这里有两个阶段进行解析:

  1)PARSE_CONFIGURATION:会在解析@Configuration时进行condition的解析

  2)REGISTER_BEAN:会在注册Bean的时候进行condition的解析

ServerAutoConfiguration:

复制代码
package com.hzgj.lyrk.autoconfigure;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Description;

@Configuration
public class ServerAutoConfiguration {

    @Configuration
    @Conditional(MyTestConditional.class)
    @Description(value = "student")
    public static class StudentAutoConfiguration {
        @Bean
        public Student student() {
            System.out.println("student create....");
            return new Student();
        }


    }

    @Configuration
    @Conditional(MyTestConditional.class)
    @Description(value = "teacher")
    public static class TeacherAutoConfiguration {
        @Bean
        public Teacher teacher() {
            System.out.println("teacher create.....");
            return new Teacher();
        }
    }
}
复制代码

 

此时由于自定义的Conditional的match方法返回值是false,因此不能注册@Bean配置的对象:

当值改为true时,则能注册@Bean配置的对象:

 

二、SpringBoot中对Conditional的扩展

  在SpringBoot中定义了一个SpringBootCondition类对Condition进行了扩展,该类源代码如下:

 View Code

  在这里,我们需要重写getMatchOutcome方法来进行,匹配结果的过滤,下面我们列举一下常见的Conditional:

2.1、Class Conditions

  常见的有ConditionalOnClass,ConditionalOnMissingClass

  ConditionalOnClass:表明当前classpath有对应指定的类型才去创建Bean,我们来看一下源代码:

 View Code

  根据注释我们去寻找一下:OnClassCondition这个类,我贴出部分代码:

复制代码
    @Override
    public ConditionOutcome getMatchOutcome(ConditionContext context,
            AnnotatedTypeMetadata metadata) {
        ClassLoader classLoader = context.getClassLoader();
        ConditionMessage matchMessage = ConditionMessage.empty();
        List<String> onClasses = getCandidates(metadata, ConditionalOnClass.class);
        if (onClasses != null) {
            List<String> missing = getMatches(onClasses, MatchType.MISSING, classLoader);
            if (!missing.isEmpty()) {
                return ConditionOutcome
                        .noMatch(ConditionMessage.forCondition(ConditionalOnClass.class)
                                .didNotFind("required class", "required classes")
                                .items(Style.QUOTE, missing));
            }
            matchMessage = matchMessage.andCondition(ConditionalOnClass.class)
                    .found("required class", "required classes").items(Style.QUOTE,
                            getMatches(onClasses, MatchType.PRESENT, classLoader));
        }
        List<String> onMissingClasses = getCandidates(metadata,
                ConditionalOnMissingClass.class);
        if (onMissingClasses != null) {
            List<String> present = getMatches(onMissingClasses, MatchType.PRESENT,
                    classLoader);
            if (!present.isEmpty()) {
                return ConditionOutcome.noMatch(
                        ConditionMessage.forCondition(ConditionalOnMissingClass.class)
                                .found("unwanted class", "unwanted classes")
                                .items(Style.QUOTE, present));
            }
            matchMessage = matchMessage.andCondition(ConditionalOnMissingClass.class)
                    .didNotFind("unwanted class", "unwanted classes").items(Style.QUOTE,
                            getMatches(onMissingClasses, MatchType.MISSING, classLoader));
        }
        return ConditionOutcome.match(matchMessage);
    }

复制代码

在这里我们关注一下getMatches方法:

复制代码
private List<String> getMatches(Collection<String> candidates, MatchType matchType,
            ClassLoader classLoader) {
        List<String> matches = new ArrayList<String>(candidates.size());
        for (String candidate : candidates) {
            if (matchType.matches(candidate, classLoader)) {
                matches.add(candidate);
            }
        }
        return matches;
    }


//.....

private enum MatchType {

        PRESENT {

            @Override
            public boolean matches(String className, ClassLoader classLoader) {
                return isPresent(className, classLoader);
            }

        },

        MISSING {

            @Override
            public boolean matches(String className, ClassLoader classLoader) {
                return !isPresent(className, classLoader);
            }

        };

        private static boolean isPresent(String className, ClassLoader classLoader) {
            if (classLoader == null) {
                classLoader = ClassUtils.getDefaultClassLoader();
            }
            try {
                forName(className, classLoader);
                return true;
            }
            catch (Throwable ex) {
                return false;
            }
        }

        private static Class<?> forName(String className, ClassLoader classLoader)
                throws ClassNotFoundException {
            if (classLoader != null) {
                return classLoader.loadClass(className);
            }
            return Class.forName(className);
        }
复制代码

  我们可以看到这里是通过ClassLoader或者Class.forName来加载类的

2.2、Bean Conditionals

  在这里常见的是ConditionalOnBean和ConditionalOnMissingBean,只有当BeanFactory里(不)包含指定的Bean时,才能通过匹配。注意:官网建议我们在AutoConfiguration里使用此注解,因为受到bean装配顺序影响,很有可能不能达到我们的预期效果。

复制代码
package com.hzgj.lyrk.autoconfigure;

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

@Configuration
public class ServerAutoConfiguration {

    @Configuration
    @ConditionalOnBean(Teacher.class)
    public static class StudentAutoConfiguration {
        @Bean
        public Student student() {
            System.out.println("student create....");
            return new Student();
        }


    }

    @Configuration
    public static class TeacherAutoConfiguration {
        @Bean
        public Teacher teacher() {
            System.out.println("teacher create.....");
            return new Teacher();
        }
    }
}
复制代码

比如说如上代码,运行后将得到如下结果:

我们可以看到此时Student并未创建。因为受其顺序影响当注册Student时,IOC容器里并没有Teacher,我在这里贴出OnBeanCondition的关键代码:

复制代码
@Override
    public ConditionOutcome getMatchOutcome(ConditionContext context,
            AnnotatedTypeMetadata metadata) {
        ConditionMessage matchMessage = ConditionMessage.empty();
        if (metadata.isAnnotated(ConditionalOnBean.class.getName())) {
            BeanSearchSpec spec = new BeanSearchSpec(context, metadata,
                    ConditionalOnBean.class);
            List<String> matching = getMatchingBeans(context, spec);
            if (matching.isEmpty()) {
                return ConditionOutcome.noMatch(
                        ConditionMessage.forCondition(ConditionalOnBean.class, spec)
                                .didNotFind("any beans").atAll());
            }
            matchMessage = matchMessage.andCondition(ConditionalOnBean.class, spec)
                    .found("bean", "beans").items(Style.QUOTE, matching);
        }
        if (metadata.isAnnotated(ConditionalOnSingleCandidate.class.getName())) {
            BeanSearchSpec spec = new SingleCandidateBeanSearchSpec(context, metadata,
                    ConditionalOnSingleCandidate.class);
            List<String> matching = getMatchingBeans(context, spec);
            if (matching.isEmpty()) {
                return ConditionOutcome.noMatch(ConditionMessage
                        .forCondition(ConditionalOnSingleCandidate.class, spec)
                        .didNotFind("any beans").atAll());
            }
            else if (!hasSingleAutowireCandidate(context.getBeanFactory(), matching,
                    spec.getStrategy() == SearchStrategy.ALL)) {
                return ConditionOutcome.noMatch(ConditionMessage
                        .forCondition(ConditionalOnSingleCandidate.class, spec)
                        .didNotFind("a primary bean from beans")
                        .items(Style.QUOTE, matching));
            }
            matchMessage = matchMessage
                    .andCondition(ConditionalOnSingleCandidate.class, spec)
                    .found("a primary bean from beans").items(Style.QUOTE, matching);
        }
        if (metadata.isAnnotated(ConditionalOnMissingBean.class.getName())) {
            BeanSearchSpec spec = new BeanSearchSpec(context, metadata,
                    ConditionalOnMissingBean.class);
            List<String> matching = getMatchingBeans(context, spec);
            if (!matching.isEmpty()) {
                return ConditionOutcome.noMatch(ConditionMessage
                        .forCondition(ConditionalOnMissingBean.class, spec)
                        .found("bean", "beans").items(Style.QUOTE, matching));
            }
            matchMessage = matchMessage.andCondition(ConditionalOnMissingBean.class, spec)
                    .didNotFind("any beans").atAll();
        }
        return ConditionOutcome.match(matchMessage);
    }

  @SuppressWarnings("deprecation")
    private List<String> getMatchingBeans(ConditionContext context,
            BeanSearchSpec beans) {
        ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
        if (beans.getStrategy() == SearchStrategy.PARENTS
                || beans.getStrategy() == SearchStrategy.ANCESTORS) {
            BeanFactory parent = beanFactory.getParentBeanFactory();
            Assert.isInstanceOf(ConfigurableListableBeanFactory.class, parent,
                    "Unable to use SearchStrategy.PARENTS");
            beanFactory = (ConfigurableListableBeanFactory) parent;
        }
        if (beanFactory == null) {
            return Collections.emptyList();
        }
        List<String> beanNames = new ArrayList<String>();
        boolean considerHierarchy = beans.getStrategy() != SearchStrategy.CURRENT;
        for (String type : beans.getTypes()) {
            beanNames.addAll(getBeanNamesForType(beanFactory, type,
                    context.getClassLoader(), considerHierarchy));
        }
        for (String ignoredType : beans.getIgnoredTypes()) {
            beanNames.removeAll(getBeanNamesForType(beanFactory, ignoredType,
                    context.getClassLoader(), considerHierarchy));
        }
        for (String annotation : beans.getAnnotations()) {
            beanNames.addAll(Arrays.asList(getBeanNamesForAnnotation(beanFactory,
                    annotation, context.getClassLoader(), considerHierarchy)));
        }
        for (String beanName : beans.getNames()) {
            if (containsBean(beanFactory, beanName, considerHierarchy)) {
                beanNames.add(beanName);
            }
        }
        return beanNames;
    }
复制代码

  在这里我们关注getMatchingBeans方法,此方法从当前的BeanFactory找所需要的Bean。由于BeanFactory层次化的关系,因此在ConditionalOn(Missing)Bean里有相关属性来配置寻找策略:

复制代码
/**
     * Strategy to decide if the application context hierarchy (parent contexts) should be
     * considered.
     * @return the search strategy
     */
    SearchStrategy search() default SearchStrategy.ALL;
复制代码
复制代码
/*
 * Copyright 2012-2018 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.boot.autoconfigure.condition;

/**
 * Some named search strategies for beans in the bean factory hierarchy.
 *
 * @author Dave Syer
 */
public enum SearchStrategy {

    /**
     * Search only the current context.
     */
    CURRENT,

    /**
     * Search all parents and ancestors, but not the current context.
     * @deprecated as of 1.5 in favor of {@link SearchStrategy#ANCESTORS}
     */
    @Deprecated
    PARENTS,

    /**
     * Search all ancestors, but not the current context.
     */
    ANCESTORS,

    /**
     * Search the entire hierarchy.
     */
    ALL

}
复制代码

 2.3、Property Conditions

  常见的注解为@ConditionalOnProperty,该注解会在Spring的Environment里面找对应的PropertySource,如果存在对应的属性值并且对应的值不为false时则匹配,我贴出关键代码部分:

复制代码
  @Override
    public ConditionOutcome getMatchOutcome(ConditionContext context,
            AnnotatedTypeMetadata metadata) {
  //.....

          for (AnnotationAttributes annotationAttributes : allAnnotationAttributes) {
            ConditionOutcome outcome = determineOutcome(annotationAttributes,
                    context.getEnvironment());
            (outcome.isMatch() ? match : noMatch).add(outcome.getConditionMessage());
        }
//.....

}

private static class Spec {

      // .....

      private void collectProperties(PropertyResolver resolver, List<String> missing,
                List<String> nonMatching) {
            if (this.relaxedNames) {
                resolver = new RelaxedPropertyResolver(resolver, this.prefix);
            }
            for (String name : this.names) {
                String key = (this.relaxedNames ? name : this.prefix + name);
                if (resolver.containsProperty(key)) {
                    if (!isMatch(resolver.getProperty(key), this.havingValue)) {
                        nonMatching.add(name);
                    }
                }
                else {
                    if (!this.matchIfMissing) {
                        missing.add(name);
                    }
                }
            }
        }

        private boolean isMatch(String value, String requiredValue) {
            if (StringUtils.hasLength(requiredValue)) {
                return requiredValue.equalsIgnoreCase(value);
            }
            return !"false".equalsIgnoreCase(value);
        }      
//.....
    
}    
复制代码

  在这里通过ConditionContext拿到当前的Environment对象,在通过PropertyResover获取其配置的值。isMatch方法表明如果值存在且不等于false的情况下条件才生效

 

2.4、Resource Conditions

  常见的注解为:@ConditionalOnResource,只有存在指定的资源文件时才生效,默认情况下是classpath,当然我们也可以指定其绝对路径,例如:file:/home/user/test.dat,源代码如下:

 View Code

在这里我们可以看到,它是用DefaultResourceLoader来加载资源文件的,它会根据路径前缀来判断根据classpath加载或是url加载,其相关代码如下:

复制代码
  @Override
    public Resource getResource(String location) {
        Assert.notNull(location, "Location must not be null");

        for (ProtocolResolver protocolResolver : this.protocolResolvers) {
            Resource resource = protocolResolver.resolve(location, this);
            if (resource != null) {
                return resource;
            }
        }

        if (location.startsWith("/")) {
            return getResourceByPath(location);
        }
        else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
            return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
        }
        else {
            try {
                // Try to parse the location as a URL...
                URL url = new URL(location);
                return new UrlResource(url);
            }
            catch (MalformedURLException ex) {
                // No URL -> resolve as resource path.
                return getResourceByPath(location);
            }
        }
    }
复制代码

2.5、WebApplication Conditions

  最常见的是@ConditionalOnWebApplication或者@ConditionalOnNotWebApplication,此注解的含义为:判断当前的ApplicationContext是否为WebApplicationContext。其关键代码如下:

复制代码
private ConditionOutcome isWebApplication(ConditionContext context,
            AnnotatedTypeMetadata metadata, boolean required) {
        ConditionMessage.Builder message = ConditionMessage.forCondition(
                ConditionalOnWebApplication.class, required ? "(required)" : "");
        if (!ClassUtils.isPresent(WEB_CONTEXT_CLASS, context.getClassLoader())) {
            return ConditionOutcome
                    .noMatch(message.didNotFind("web application classes").atAll());
        }
        if (context.getBeanFactory() != null) {
            String[] scopes = context.getBeanFactory().getRegisteredScopeNames();
            if (ObjectUtils.containsElement(scopes, "session")) {
                return ConditionOutcome.match(message.foundExactly("'session' scope"));
            }
        }
        if (context.getEnvironment() instanceof StandardServletEnvironment) {
            return ConditionOutcome
                    .match(message.foundExactly("StandardServletEnvironment"));
        }
        if (context.getResourceLoader() instanceof WebApplicationContext) {
            return ConditionOutcome.match(message.foundExactly("WebApplicationContext"));
        }
        return ConditionOutcome.noMatch(message.because("not a web application"));
    }
复制代码

在此我们可以看到它的判断依据有如下几个方面:

  1)是否为WebApplicationContext

  2) 是否包含session的scope

  3) 当前的Environment是否为StandardServletEnvironment

 

三、基于@Configuration注册Bean分析

  我们知道解析@Configuration的最主要的类是ConfigurationClassPostProcessor,这个类下有一个属性叫ConfigurationClassBeanDefinitionReader,与接口BeanDefiinitiaonReader类似,一看这个类我们就能联想到它的loadBeanDefinitions,其方法会调用loadBeanDefinitionsForConfigurationClass,我们来看一看这个方法:

复制代码
/**
     * Read a particular {@link ConfigurationClass}, registering bean definitions
     * for the class itself and all of its {@link Bean} methods.
     */
    private void loadBeanDefinitionsForConfigurationClass(ConfigurationClass configClass,
            TrackedConditionEvaluator trackedConditionEvaluator) {

        if (trackedConditionEvaluator.shouldSkip(configClass)) {
            String beanName = configClass.getBeanName();
            if (StringUtils.hasLength(beanName) && this.registry.containsBeanDefinition(beanName)) {
                this.registry.removeBeanDefinition(beanName);
            }
            this.importRegistry.removeImportingClass(configClass.getMetadata().getClassName());
            return;
        }

        if (configClass.isImported()) {
            registerBeanDefinitionForImportedConfigurationClass(configClass);
        }
        for (BeanMethod beanMethod : configClass.getBeanMethods()) {
            loadBeanDefinitionsForBeanMethod(beanMethod);
        }
        loadBeanDefinitionsFromImportedResources(configClass.getImportedResources());
        loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars());
    }
复制代码

在这里大家关注一下loadBeanDefinitionsForBeanMethod,此方法是加载@Bean注解所配置的Bean,我们来看一下其源码,我贴出关键部分:

复制代码
private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) {
        ConfigurationClass configClass = beanMethod.getConfigurationClass();
        MethodMetadata metadata = beanMethod.getMetadata();
        String methodName = metadata.getMethodName();

        // Do we need to mark the bean as skipped by its condition?
        if (this.conditionEvaluator.shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN)) {
            configClass.skippedBeanMethods.add(methodName);
            return;
        }
        if (configClass.skippedBeanMethods.contains(methodName)) {
            return;
        }


    
            //.....省略后续代码
}
复制代码

  在这里会调用conditionEvaluator的shouldSkip方法 如果为true,则return,那么shuoldSkip方法又是怎么样的呢?我们来追踪一下:

复制代码
/**
     * Determine if an item should be skipped based on {@code @Conditional} annotations.
     * @param metadata the meta data
     * @param phase the phase of the call
     * @return if the item should be skipped
     */
    public boolean shouldSkip(AnnotatedTypeMetadata metadata, 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);
        }

        List<Condition> conditions = new ArrayList<Condition>();
        for (String[] conditionClasses : getConditionClasses(metadata)) {
            for (String conditionClass : conditionClasses) {
                Condition condition = getCondition(conditionClass, this.context.getClassLoader());
                conditions.add(condition);
            }
        }

        AnnotationAwareOrderComparator.sort(conditions);

        for (Condition condition : conditions) {
            ConfigurationPhase requiredPhase = null;
            if (condition instanceof ConfigurationCondition) {
                requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase();
            }
            if (requiredPhase == null || requiredPhase == phase) {
                if (!condition.matches(this.context, metadata)) {
                    return true;
                }
            }
        }

        return false;
    }
复制代码

  那么至此,终于和先前的Condition所关联了。

四、总结

  SpringBoot通过扩展Conditional来设置装配Bean的条件,通过Condition接口的matches方法的返回值来判断是否向IOC容器里注册Bean

上一篇:调度算法的介绍及优缺点 下一篇:MySQL--各版本DDL 操作总结
猜你喜欢
热门排行
精彩图文
  • MySQL--各版本DDL 操作总结
    MySQL--各版本DDL 操作总结
    MySQL 5.5 DDL 在MySQL 5.5版本前,所有DDL操作都使用Copy Table的方式完成,操作过程中原表数据库不允许写入,只能读取,在MySQL 5.5版本中引入FIC(Fast index crea
  • 深入理解SpringBoot之装配条件
    深入理解SpringBoot之装配条件
    阅读目录 一、关于@Conditional 二、SpringBoot中对Conditional的扩展 三、基于@Configuration注册Bean分析 四、总结 我们知道自动装配是SpringBoot微服务化的核心,它会
  • 调度算法的介绍及优缺点
    调度算法的介绍及优缺点
    一、先来先服务(FCFS)/先进先出(FIFO)调度算法 (1)概念:按照作业/进程进入系统的先后次序进行调度,先进入系统者先调度;即启动等待时间最长的作业
  • 多级反馈队列调度算法(附Python3实现代码)
    多级反馈队列调度算法(附Python3实现代码)
    一、多级反馈队列调度算法 多级反馈队列调度算法是进程调度的一种算法,该调度算法可以不用事先知道各种进程所需的执行时间,还可以较好的满足各种
  • linux 64位centos下源码安装mysql服务教程
    linux 64位centos下源码安装mysql服务教程
    1.下载mysql源码包 打开地址https://dev.mysql.com/downloads/mysql/,选择下载MySQL Community Server源码包 分别选择Source Code和 Generic Linux (Architecture Independent)选项,最后选