Springboot-2.3.4自动配置原理

Springboot的自动配置是springboot的精髓,最近学习了相关原理,特在此记录一下

我们在使用springboot的时候,部分属性可以进行yml或properties文件的配置,比如server.port=8080等等,部分属性已经被自动配置了。我们如何知道哪些属性(比如server.port)是可以配置的呢?配置怎么写呢?自动配置的原理是什么?

接下来就对自动配置原理进行一定介绍

首先,springboot在启动时加载主配置类,开启了自动配置功能
@SpringBootApplication里有@EnableAutoConfiguration,点进去,有:
在这里插入图片描述

  • @AutoConfigurationPackage的作用是将包含该注解的类所在的包看作实现自动配置功能的包,如这里的主配置类,就实现了自动配置功能
  • 接下来详细介绍@Import({AutoConfigurationImportSelector.class})

@Import({AutoConfigurationImportSelector.class})主要作用:利用AutoConfigurationImportSelector这个选择器,给spring容器导入一些组件,导入哪些组件呢?点进去,可以看到selectImports方法:

public String[] selectImports(AnnotationMetadata annotationMetadata) {
    
    
    if (!this.isEnabled(annotationMetadata)) {
    
    
        return NO_IMPORTS;
    } else {
    
    
        AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);
        return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
    }
}

可以看到,将要被导入的AutoConfigurationEntry组件,即自动配置组件,是由getAutoConfigurationEntry方法得到的,因此点进else里面的getAutoConfigurationEntry方法:

protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
    
    
    if (!this.isEnabled(annotationMetadata)) {
    
    
        return EMPTY_ENTRY;
    } else {
    
    
        AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
        List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
        configurations = this.removeDuplicates(configurations);
        Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
        this.checkExcludedClasses(configurations, exclusions);
        configurations.removeAll(exclusions);
        configurations = this.getConfigurationClassFilter().filter(configurations);
        this.fireAutoConfigurationImportEvents(configurations, exclusions);
        return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
    }
}

注意看和配置有关的configurations对象,List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);方法,字面意思是“获取候选的配置”,也就是说,configurations对象是由getCandidateConfigurations方法获得的,点进去有:

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
    
    
    List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.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;
}

注意里面的SpringFactoriesLoader.loadFactoryNames方法,这里的意思是,loadFactoryNames通过传进去getSpringFactoriesLoaderFactoryClass()这个参数,得到了跟配置有关的configurations对象,那么传入的是什么参数呢?点进getSpringFactoriesLoaderFactoryClass()方法,可以看到:
可以看到是由getSpringFactoriesLoaderFactoryClass()这个方法传进去了一个类对象,点进去:

protected Class<?> getSpringFactoriesLoaderFactoryClass() {
    
    
    return EnableAutoConfiguration.class;
}

可以看到,传进来的是一个叫EnableAutoConfiguration的类,记住这一点,我们接着往下走。点进loadFactoryNames方法 (这个方法很重要)

public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
    
    
    String factoryTypeName = factoryType.getName();
    return (List)loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}

loadFactoryNames通过传入的EnableAutoConfiguration的类,并将类名和类加载器传入loadSpringFactories这个方法,通过loadSpringFactories这个方法,获得了一个值并转换成List,这个值是什么呢?(后面我们可以看到这是一个map

这里先简单解释一下getOrDefault,第一个参数是key,这里是factoryTypeName;第二个参数是defaultValue,这里是Collections.emptyList()。方法的意思是:如果包含key,则返回它的值,否则返回默认值。这里对应的意思就是,将EnableAutoConfiguration的类名作为key,若包含该key则返回相应的值,若不包含则返回Collections.emptyList(),也就是一个空值

我们点进loadSpringFactories

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 {
    
    
            Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");
            LinkedMultiValueMap result = new LinkedMultiValueMap();

            while(urls.hasMoreElements()) {
    
    
                URL url = (URL)urls.nextElement();
                UrlResource resource = new UrlResource(url);
                Properties properties = PropertiesLoaderUtils.loadProperties(resource);
                Iterator var6 = properties.entrySet().iterator();

                while(var6.hasNext()) {
    
    
                    Entry<?, ?> entry = (Entry)var6.next();
                    String factoryTypeName = ((String)entry.getKey()).trim();
                    String[] var9 = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
                    int var10 = var9.length;

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

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

可以看到在try代码块的第一句里面,有classLoader.getResources("META-INF/spring.factories"),即得到名为"META-INF/spring.factories"的资源,也就是扫描所有jar包中的"META-INF/spring.factories"文件,那么为什么要扫描这些文件呢?

Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");这一句可以看出,扫描得到的结果,存放到了urls这个变量中,这个变量的作用是什么呢? 继续往下看

在try语句块的while循环中有这样一段:

URL url = (URL)urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
Iterator var6 = properties.entrySet().iterator();

也就是遍历urls,即遍历"META-INF/spring.factories"扫描到的结果,并存放到properties对象当中,然后将properties对象的键值对存放到var6这个迭代器中,接着往下看,在对var6的遍历中,有:

Entry<?, ?> entry = (Entry)var6.next();
String factoryTypeName = ((String)entry.getKey()).trim();
String[] var9 = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
int var10 = var9.length;

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

可以看到,遍历var6,将其中每一个键值对的key,用factoryTypeName存起来,将每一个键值对的value,存到factoryImplementationName中,并将(factoryTypeName, factoryImplementationName)作为一个键值对,存到result当中,result又是什么呢?我们回过头去看第一行

MultiValueMap<String, String> result = (MultiValueMap)cache.get(classLoader);

也就是说,result是一个map结构,loadSpringFactories方法的返回值就是这个result,也就是说,loadSpringFactories方法的返回值,是由扫描所有jar包下的"META-INF/spring.factories"文件得到properties对象,再存进(factoryTypeName, factoryImplementationName)这若干个键值对,最后存进result并返回

重点来了! 这时,想起之前的getOrDefault了吗?loadSpringFactories方法的返回值,是由扫描所有jar包下的"META-INF/spring.factories"文件而得到的全部的键值对,而getOrDefault(Object key, V defaultValue)只选择包含key的值返回,其余则返回空值,在这里,key就是EnableAutoConfiguration的类名

此刻,逻辑就捋通了,整个过程进行抽象起来就是:读取所有jar包的"META-INF/spring.factories"并生成键值对→取出其中keyEnableAutoConfiguration类名的键值对,转换成一个list返回→根据这些list的信息,将相关的类加载到spring容器当中去

那么,具体加载的是哪些类呢?我们不妨点进"META-INF/spring.factories"看一看
在这里插入图片描述
可以看到,以EnableAutoConfiguration类名为key,有很多个类(篇幅原因只截取了一部分),这些类都将在主配置类加载的时候,自动被导入spring容器中,名字统一为xxxAutoConfiguration
在这里插入图片描述
每一个这种类,都实现了自动配置功能,这里以HttpEncodingAutoConfiguration为例,解释自动配置原理。

@Configuration(
    proxyBeanMethods = false
)
@EnableConfigurationProperties({
    
    ServerProperties.class})
@ConditionalOnWebApplication(
    type = Type.SERVLET
)
@ConditionalOnClass({
    
    CharacterEncodingFilter.class})
@ConditionalOnProperty(
    prefix = "server.servlet.encoding",
    value = {
    
    "enabled"},
    matchIfMissing = true
)
public class HttpEncodingAutoConfiguration {
    
    
    private final Encoding properties;

    public HttpEncodingAutoConfiguration(ServerProperties properties) {
    
    
        this.properties = properties.getServlet().getEncoding();
    }

    @Bean
    @ConditionalOnMissingBean
    public CharacterEncodingFilter characterEncodingFilter() {
    
    
        CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
        filter.setEncoding(this.properties.getCharset().name());
        filter.setForceRequestEncoding(this.properties.shouldForce(org.springframework.boot.web.servlet.server.Encoding.Type.REQUEST));
        filter.setForceResponseEncoding(this.properties.shouldForce(org.springframework.boot.web.servlet.server.Encoding.Type.RESPONSE));
        return filter;
    }

    @Bean
    public HttpEncodingAutoConfiguration.LocaleCharsetMappingsCustomizer localeCharsetMappingsCustomizer() {
    
    
        return new HttpEncodingAutoConfiguration.LocaleCharsetMappingsCustomizer(this.properties);
    }

    static class LocaleCharsetMappingsCustomizer implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>, Ordered {
    
    
        private final Encoding properties;

        LocaleCharsetMappingsCustomizer(Encoding properties) {
    
    
            this.properties = properties;
        }

        public void customize(ConfigurableServletWebServerFactory factory) {
    
    
            if (this.properties.getMapping() != null) {
    
    
                factory.setLocaleCharsetMappings(this.properties.getMapping());
            }

        }

        public int getOrder() {
    
    
            return 0;
        }
    }
}

这里有很多注解,我们来逐一攻破:

  • @Configuration
    表示这是一个配置类
  • @EnableConfigurationProperties({ServerProperties.class})
  1. 开启指定类的ConfigurationProperties功能:
    在这里插入图片描述
    如图,该类上支持了@ConfigurationProperties注解,自动从配置文件(yml或properties)中取出前缀为“server”的配置项的值,并与类中的属性进行绑定,如图中的server.portserver.address等等。springboot中,所有能在配置文件(yml或properties)中配置的项,都封装在xxxProperties中,如这里的ServerProperties
    在这里插入图片描述
    由此,可得出它的第二个功能:
  2. 将配置文件中的值和xxxProperties中的属性绑定起来
  • @ConditionalOnWebApplication
    条件判断注解,这里是判断是不是web应用;如果是,则当前配置类生效
  • @ConditionalOnClass({CharacterEncodingFilter.class})
    判断当前项目有没有这个类,这里是CharacterEncodingFilter
  • @ConditionalOnProperty(prefix = "server.servlet.encoding", value = {"enabled"}, matchIfMissing = true))
    判断配置文件中有没有"server.servlet.encoding"这个前缀,若没有,则"server.servlet.encoding.enabled=true"

这些@Conditional*注解,都是为了确定这个配置类xxxAutoConfiguration是否能生效,如果生效,则通过@Bean标签向spring容器添加组件,这里举一个例子:

@Bean
@ConditionalOnMissingBean
public CharacterEncodingFilter characterEncodingFilter() {
    
    
    CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
    filter.setEncoding(this.properties.getCharset().name());
    filter.setForceRequestEncoding(this.properties.shouldForce(org.springframework.boot.web.servlet.server.Encoding.Type.REQUEST));
    filter.setForceResponseEncoding(this.properties.shouldForce(org.springframework.boot.web.servlet.server.Encoding.Type.RESPONSE));
    return filter;
}

配置类若生效,则向spring加入CharacterEncodingFilter组件,注意这几句:

filter.setEncoding(this.properties.getCharset().name());
filter.setForceRequestEncoding(this.properties.shouldForce(org.springframework.boot.web.servlet.server.Encoding.Type.REQUEST));
filter.setForceResponseEncoding(this.properties.shouldForce(org.springframework.boot.web.servlet.server.Encoding.Type.RESPONSE));

说明组件的有些属性需要从this.properties中获取,这里的properties是该类的一个属性
在这里插入图片描述
注意看此处,this.properties是从ServerProperties获取的,而ServerProperties的属性值是与配置文件绑定的,因此这里的this.properties的值也是由配置文件得来的。我们可以通过找这些xxxProperties类,来知道配置文件中可以怎么写

总结起来,最主要的功能:配置类通过@EnableConfigurationProperties({xxxProperties.class})将配置文件与xxxProperties类绑定,又通过配置类内的属性propertiesxxxProperties绑定,再用这个properties去控制组件的属性值,从而能够通过修改配置文件来设置组件的属性值

在这里插入图片描述

(如图,在以前的版本中是直接在构造方法中等于,新的2.3.4版本多绕了几个步骤,不过原理一致)

======================================
总结起来,springboot的精髓:

1)SpringBoot启动会加载大量的自动配置类

2)我们看我们需要的功能有没有SpringBoot默认写好的自动配置类

3)我们再来看这个自动配置类中到底配置了哪些组件;(只要我们要用的组件有,我们就不需要再来配置了)

4)给容器中自动配置类添加组件的时候,会从properties类中获取某些属性。我们就可以在配置文件中指定这些属性的值

*​ 5)默认的配置信息存在 包名字以xxx-autoconfigure-xxx.jar 形式结尾的jar包中,springboot的默认配置文件的名字为:/META-INF/spring-configuration-metadata.json
在这里插入图片描述
如8080端口:
在这里插入图片描述

=====================================
自动配置具体流程(抽象):

​ 1)扫描所有jar包下的/META-INF/spring-properties,取出key为EnableAutoConfiguration的配置类注入到spring容器

2)在确定能够生效后,这些配置类给容器中添加各种功能组件

3)功能组件实现自动配置功能,SpringBoot底层会实现默认配置

4)添加组件的时候,会从properties类中获取某些属性。可以在配置文件中指定这些属性的值