springboot源码分析系列(三)--@EnableAutoConfiguration自动配置加载过程

智数寻梦
• 阅读 4492

为什么需要自动化配置

  在常规的spring应用程序中,充斥着大量的配置文件,我们需要手动去配置这些文件,如配置组件扫描、视图解析器、http编码等等。常规的配置让开发人员将更多的经历耗费在了配置文件上。而这些配置都是一些固定模式的配置方式,甚至很多都是模板代码。那既然是这样一种情况,有没有一种可能性,让spring自动完成这些模板配置工作呢?答案是肯定的,这就是SpringBoot AutoConfiguration产生的初衷。将开发人员从繁重的配置工作中解放出来,把这些繁琐的配置交由SpringBoot完成,如果我们需要自己配置参数,只需要覆盖自动配置的参数即可。

SpringBoot自动化配置的核心原理

  在之前的文章中,我们看过了SpringBoot的核心注解@SpringBootApplication注解的源码。其中有三个注解:@SpringBootConfiguration,@EnableAutoConfiguration,@ComponentScan。以前我们需要配置的东西,SpringBoot帮我们自动配置,@EnableAutoConfiguration告诉SpringBoot开启自动配置功能,这样自动配置才能生效。
  下面我们来分析一下@EnableAutoConfiguration这个注解的加载过程

Target(ElementType.TYPE)
Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
    /**
     * Exclude specific auto-configuration classes such that they will never be applied.
     * @return the classes to exclude
     */
    Class<?>[] exclude() default {};
    /**
     * Exclude specific auto-configuration class names such that they will never be
     * applied.
     * @return the class names to exclude
     * @since 1.3.0
     */
    String[] excludeName() default {};
}

  由源码可知,@EnableAutoConfiguration是一个组合注解,由@AutoConfigurationPackage,@Import(AutoConfigurationImportSelector.class)这两个注解组成。

@AutoConfigurationPackage

  @AutoConfigurationPackage的主要作用是自动配置包

/**
 * Indicates that the package containing the annotated class should be registered with
 * {@link AutoConfigurationPackages}.
 *
 * @author Phillip Webb
 * @since 1.3.0
 * @see AutoConfigurationPackages
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {

}

@Import(AutoConfigurationImportSelector.class)

  Spring底层注解@Import,给容器中导入一个组件;导入的组件由AutoConfigurationPackages.Registrar.class
将主配置类(@SpringBootApplication标注的类)的所在包以及下面所有子包里面的所有组件扫描到Spring容器。
  AutoConfigurationImportSelector的作用是导入哪些组件的选择器。将所有需要导入的组件以全类名的方式返回,这些组件就会被添加到容器中;也会给容器导入非常多的自动配置类(xxxAutoConfiguration),就是给容器中导入这个场景需要的所有组件,并配置好这些组件。
  有了自动配置类,免去了我们手动编写配置注入功能组件等的工作
具体的工作流程如下:
springboot源码分析系列(三)--@EnableAutoConfiguration自动配置加载过程

@EnableAutoConfiguration加载过程

  自动配置主要由AutoConfigurationImportSelector实现的,我们主要从这个类开始讲起。AutoConfigurationImportSelector是@EnableAutoConfiguration“@Import”的DeferredImportSelector实现类,由于DeferredImportSelector作为ImportSelector的子接口,所以组件自动配置逻辑均在selectImports(AnnotationMetadata)方法中实现

  自动配置加载过程主要分为以下几个步骤:

  • 1.判断是否开启自动配置
  • 2.从META-INF/spring-autoconfigure-metadata.properties文件中载入属性配置
  • 3.获取所有的配置列表
public String[] selectImports(AnnotationMetadata annotationMetadata) {
    //1.是否开启自动配置,默认开启
    if (!isEnabled(annotationMetadata)) {
        return NO_IMPORTS;
    }
    //2.从META-INF/spring-autoconfigure-metadata.properties文件中载入属性配置(有一些有默认值),获取注解信息
    AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
            .loadMetadata(this.beanClassLoader);
    //3.获取所有的配置列表
    AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata,
            annotationMetadata);
    return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}

1.是否开启自动配置,默认开启

protected boolean isEnabled(AnnotationMetadata metadata) {
    if (getClass() == AutoConfigurationImportSelector.class) {
        return getEnvironment().getProperty(EnableAutoConfiguration.ENABLED_OVERRIDE_PROPERTY, Boolean.class, true);
    }
    return true;
}

2.从META-INF/spring-autoconfigure-metadata.properties文件中载入属性配置

//文件为需要加载的配置类的类路径
protected static final String PATH = "META-INF/" + "spring-autoconfigure-metadata.properties";

public static AutoConfigurationMetadata loadMetadata(ClassLoader classLoader) {
    return loadMetadata(classLoader, PATH);
}

static AutoConfigurationMetadata loadMetadata(ClassLoader classLoader, String path) {
    try {
        //1.读取spring-boot-autoconfigure-2.1.6.RELEASE.jar包中pring-autoconfigure-metadata.properties的信息生成urls枚举对象
        Enumeration<URL> urls = (classLoader != null) ? classLoader.getResources(path)
                : ClassLoader.getSystemResources(path);
        Properties properties = new Properties();
        //2.解析urls枚举对象中的信息封装成properties对象并加载
        while (urls.hasMoreElements()) {
            properties.putAll(PropertiesLoaderUtils.loadProperties(new UrlResource(urls.nextElement())));
        }
        //3.根据封装好的properties对象生成AutoConfigurationMetadata对象返回
        return loadMetadata(properties);
    }
    catch (IOException ex) {
        throw new IllegalArgumentException("Unable to load @ConditionalOnClass location [" + path + "]", ex);
    }
}

3.获取所有的配置列表

/**
 * Return the {@link AutoConfigurationEntry} based on the {@link AnnotationMetadata}
 * of the importing {@link Configuration @Configuration} class.
 * @param autoConfigurationMetadata the auto-configuration metadata
 * @param annotationMetadata the annotation metadata of the configuration class
 * @return the auto-configurations that should be imported
 */
protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,
        AnnotationMetadata annotationMetadata) {
    if (!isEnabled(annotationMetadata)) {
        return EMPTY_ENTRY;
    }
    //1.将注解元信息封装成注解属性对象
    AnnotationAttributes attributes = getAttributes(annotationMetadata);
    //2.获取到配置类的全路径字符串集合
    List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
    configurations = removeDuplicates(configurations);
    //需要排除的自动装配类(springboot的主类上 @SpringBootApplication(exclude = {com.demo.XXX.class})指定的排除的自动装配类)
    Set<String> exclusions = getExclusions(annotationMetadata, attributes);
    checkExcludedClasses(configurations, exclusions);
    //将需要排除的类从 configurations remove掉
    configurations.removeAll(exclusions);
    //过滤掉不需要装配的类。过滤的逻辑有很多,比如我们常用的@ConditionXXX注解
    configurations = filter(configurations, autoConfigurationMetadata);
    fireAutoConfigurationImportEvents(configurations, exclusions);
    return new AutoConfigurationEntry(configurations, exclusions);
}

3.1将注解元信息封装成注解属性对象

/**
 * Return the appropriate {@link AnnotationAttributes} from the
 * {@link AnnotationMetadata}. By default this method will return attributes for
 * {@link #getAnnotationClass()}.
 * @param metadata the annotation metadata
 * @return annotation attributes
 */
protected AnnotationAttributes getAttributes(AnnotationMetadata metadata) {
    String name = getAnnotationClass().getName();
    AnnotationAttributes attributes = AnnotationAttributes.fromMap(metadata.getAnnotationAttributes(name, true));
    Assert.notNull(attributes, () -> "No auto-configuration attributes found. Is " + metadata.getClassName()
            + " annotated with " + ClassUtils.getShortName(name) + "?");
    return attributes;
}

3.2获取到配置类的全路径字符串集合

  getCandidateConfigurations(annotationMetadata, attributes);这个方法中有一个重要方法loadFactoryNames,这个方法是让SpringFactoryLoader去加载一些组件的名字

/**
 * Return the auto-configuration class names that should be considered. By default
 * this method will load candidates using {@link SpringFactoriesLoader} with
 * {@link #getSpringFactoriesLoaderFactoryClass()}.
 * @param metadata the source metadata
 * @param attributes the {@link #getAttributes(AnnotationMetadata) annotation
 * attributes}
 * @return a list of candidate configurations
 */
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
    /**
     * 这个方法需要传入两个参数getSpringFactoriesLoaderFactoryClass()和getBeanClassLoader()
     * getSpringFactoriesLoaderFactoryClass()这个方法返回的是EnableAutoConfiguration.class
     * getBeanClassLoader()这个方法返回的是beanClassLoader(类加载器)
     */
    List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
            getBeanClassLoader());
    Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
            + "are using a custom packaging, make sure that file is correct.");
    return configurations;
}

/**
 * Return the class used by {@link SpringFactoriesLoader} to load configuration
 * candidates.
 * @return the factory class
 */
protected Class<?> getSpringFactoriesLoaderFactoryClass() {
    return EnableAutoConfiguration.class;
}

protected ClassLoader getBeanClassLoader() {
    return this.beanClassLoader;
}

  下面来看下loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),

        getBeanClassLoader());这个方法
public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
    //factoryClassName为org.springframework.boot.autoconfigure.EnableAutoConfiguration
    String factoryClassName = factoryClass.getName();
    //该方法返回的是所有spring.factories文件中key为org.springframework.boot.autoconfigure.EnableAutoConfiguration的类路径
    return (List)loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
}

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
    MultiValueMap<String, String> result = (MultiValueMap)cache.get(classLoader);
    if(result != null) {
        return result;
    } else {
        try {
            //如果类加载器不为null,则加载类路径下spring.factories文件,将其中设置的配置类的全路径信息封装 为Enumeration类对象
            Enumeration<URL> urls = classLoader != null?classLoader.getResources("META-INF/spring.factories"):ClassLoader.getSystemResources("META-INF/spring.factories");
            LinkedMultiValueMap result = new LinkedMultiValueMap();

            //循环Enumeration类对象,根据相应的节点信息生成Properties对象,通过传入的键获取值,在将值切割为一个个小的字符串转化为Array,方法result集合中
            while(urls.hasMoreElements()) {
                URL url = (URL)urls.nextElement();
                UrlResource resource = new UrlResource(url);
                //读取文件内容,properties类似于HashMap,包含了属性的key和value
                Properties properties = PropertiesLoaderUtils.loadProperties(resource);
                Iterator var6 = properties.entrySet().iterator();

                //属性文件中可以用','分割多个value
                while(var6.hasNext()) {
                    Entry<?, ?> entry = (Entry)var6.next();
                    String factoryClassName = ((String)entry.getKey()).trim();
                    String[] var9 = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
                    int var10 = var9.length;

                    for(int var11 = 0; var11 < var10; ++var11) {
                        String factoryName = var9[var11];
                        result.add(factoryClassName, factoryName.trim());
                    }
                }
            }

            cache.put(classLoader, result);
            return result;
        } catch (IOException var13) {
            throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var13);
        }
    }
}

总结

  springboot底层实现自动配置的步骤:

  • 1.springboot应用启动
  • 2.@SpringBootApplication起作用
  • 3.@EnableAutoConfiguration
  • 4.@AutoConfigurationPackage:这个组合注解主要是@Import(AutoConfigurationPackages.Registrar.class),它通过将Registrar类导入到容器中,而Registrar类作用是扫描主配置类同级目录以及子包,并将相应的组件导入到springboot创建管理的容器中
  • 5.@Import(AutoConfigurationImportSelector.class):它通过将AutoConfigurationImportSelector类导入到容器中,AutoConfigurationImportSelector类作用是通过selectImports方法实现将配置类信息交给SpringFactory加载器进行一系列的容器创建过程,具体实现可查看上面的源码
点赞
收藏
评论区
推荐文章
捉虫大师 捉虫大师
4年前
nacos配置中心模块详解
本文已收录https://github.com/lkxiaolou/lkxiaolou欢迎star。配置中心业务上的配置,功能开关,服务治理上对弱依赖的降级,甚至数据库的密码等,都可能用到动态配置中心。在没有专门的配置中心组件时,我们使用硬编码、或配置文件、或数据库、缓存等方式来解决问题。硬编码修改配置时需要重新编译打包,配置文件需要重启应用,数据库受
Easter79 Easter79
3年前
springcloud的配置文件的读取顺序
SpringBoot默认支持properties和YAML两种格式的配置文件。前者格式简单,但是只支持键值对。如果需要表达列表,最好使用YAML格式。SpringBoot支持自动加载约定名称的配置文件,例如application.yml。如果是自定义名称的配置文件,就要另找方法了。可惜的是,不像前者有@PropertySource这样方便的加载方式,
Easter79 Easter79
3年前
spring注解
随着越来越多地使用Springboot敏捷开发,更多地使用注解配置Spring,而不是Spring的applicationContext.xml文件。Configuration注解:Spring解析为配置类,相当于spring配置文件Bean注解:容器注册Bean组件,默认id为方法名@Configurat
Stella981 Stella981
3年前
MyBatis接口(Bean)与配置信息(Mapper)绑定
目的MyBatis的XML配置文件解析成JAVA类并在内存中存储,但是在程序运行时需要对应的类去调用,而相应的调用类还没有实例化,现在流行的都是使用Spring去管理需要的对象,Spring提供2种方式,分别为XML与注解。下面来分析调用类的实例化及与配置绑定。1XML方式<bean id"menuMapper" cl
Stella981 Stella981
3年前
Spring boot 配置文件,配置注解详解 (properties 和yml )
从其他框架来看我们都有自己的配置文件,hibernate有hbm,mybatis有properties,同样,Springboot也有全局配置文件。Springboot使用一个全局的配置文件,而且配置文件的名字是固定的。有两种application.propertiesapplication.yml spring
Stella981 Stella981
3年前
Linux脚本调用Java模板,将Properties文件放到CLASSPATH中的配置方式
为了方便管理配置文件,每次更改配置文件(例如:jdbc.properties,service.properties),我们不需要重新buildjar,所以将properties单独放到一个目录中,在liunx脚本调用java的过程中,我们将这些配置文件放到CLASSPATH中,并export。!/bin/bashLIBRA
Stella981 Stella981
3年前
SpringBoot 2.0 系列004
SpringBoot2.0系列004启动实战之配置文件配置文件配置文件加载流程很多文档包括官方文档说SB的默认配置文件是application开头的文件,那么是为什么呢?我们先看下流程图 !(https://gitee.c
Easter79 Easter79
3年前
Spring之导入和混合配置
  在典型的Spring应用中,我们可能会同时使用自动化和显式配置。即便你更喜欢通过JavaConfig实现显式配置,但有的时候XML却是最佳的方案。幸好在Spring中,这些配置方案都不是互斥的。你尽可以将JavaConfig的组件扫描和自动装配和/或XML配置混合在一起。实际上,就像在2.2.1小节中所看到的,我们至少需要有一点显式配置来启用组件扫描和自
Easter79 Easter79
3年前
Spring加载资源文件并静态化
说明:     在实际项目中有时候需要用到很多的配置文件,而这些配置文件我们又想按功能分文件加载。第一步     我们需要在Spring的配置文件中加入一段配置,配置如下<beanid"propertyBean"class"com.funo.util.WcityProperty"<propertyname
Stella981 Stella981
3年前
Kubernetes 学习1 Devops 核心要点和k8s架构概述
一、概述  1、我们以往在去实现安装部署应用程序时我们要去实现部署实现应用手动去做会非常麻烦,所以我们后来便有了工具,像ansible等等,这个工具其实就是一个应用编排工具。他能够安装,配置,服务启动,甚至能够让你按照所定义的Playbok完成对多种应用程序在实现有依赖关系时将我们手工需要配置的工作反应在ansible配置文件playbox中,让其按照p
美凌格栋栋酱 美凌格栋栋酱
5个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(