springboot 是怎么做到简化配置的?

本贴最后更新于 1729 天前,其中的信息可能已经沧海桑田

00 前言

惭愧地狠,前几天的一个面试问到 springboot 是怎么做到简化配置的,我就说了个事先约定,内部实现没有答上来。用 springboot 也用了一年多,从来没想着去看看 springboot 是怎么实现简化配置,让大家爱用这个玩意儿的。

然后搜了下,说是加载 jar 包下的 META-INF/spring.factories 文件,但是又有个面试官问我,这里面的配置代表了什么意思呢?

我又瞎说了一通。

今天就找了个下资料,学习了下,然后自己点开源码看了下,发现主脉络写的很清晰,并不是很难懂,就此写一篇文章,加深下自己的记忆吧。

也为我的两次面试哀悼。

可能我不是属于考试型的吧。唉。

面试还是要懂些原理性的东西。

有一个面试官说的好,人有两种能力,一种是面试时表现出的能力,一种是工作中解决实际问题的能力。

我的第一种能力太弱了,都是靠第二种能力撑着的,但是第二种能力面试时没法面试出来,所以我每次换工作时都挺痛苦的。

废话不多说了,开启今天的 springboot 自动化配置之旅吧。

01 EnableAutoConfiguration

springboot 的启动类上有个注解 SpringBootApplication,点开这个注解,可以看到它的内部实现,是由多个注解组成的。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {

}

在这其中,可以看到有个 EnableAutoConfiguration,就是负责开启我们的自动配置。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

    Class<?>[] exclude() default {};

    String[] excludeName() default {};
}

打开 EnableAutoConfiguration,可以看到里面是 import 了 AutoConfigurationImportSelector 这样一个类。

继续追踪这个类,AutoConfigurationImportSelector 类里主要看一个 selectImports 方法,这个方法的调用链如下:

selectImports
    ->getAutoConfigurationEntry 
        -> getCandidateConfigurations
            ->SpringFactoriesLoader.loadFactoryNames

SpringFactoriesLoader.loadFactoryNames 方法如下:

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;
}

可以看到断言中说到是去 META-INF/spring.factories 这个文件下去寻找有没有自动配置类。

我们再点开 SpringFactoriesLoader.loadFactoryNames 这个方法确认下。

public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
    String factoryTypeName = factoryType.getName();
    return (List)loadSpringFactories(classLoader).getOrDefault(factoryTypeName, 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 {
            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);
        }
    }
}

可以看到确实调用了 loadSpringFactories 这个方法,loadSpringFactories 方法中开头的这段代码

Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");

即告诉我们,它是要找到所有 META-INF/spring.factories 下的文件,然后加载进来,使用 PropertiesLoaderUtils.loadProperties 方法读取其中的文件配置。

02 spring.factories

哪些 jar 包下有 META-INF/spring.factories 文件呢?一般是在 springboot 的核心类及 XXX-spring-boot-autoconfigure 或 spring-boot-autoconfigure-XXX 这样的 jar 包中。

本次我们只说 EnableAutoConfiguration,打开 spring-boot-autoconfigure-2.2.1.RELEASE.jar,这个就是 springboot 中最重要的自动配置包。

打开下面的 META-INF/spring.factories 文件,可以发现这个文件也是也是一组一组的 key=value 的形式,与我们常用的 properties 文件没有什么区别。

截取其中部分内容如下:

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
……

这里 EnableAutoConfiguration 这个 key 对应的 value 很长,以逗号分割。

可以看到这里的 value 其实都是一个个类,那么它们是在哪里呢?

再返回上一层看看 spring-boot-autoconfigure-2.2.1.RELEASE.jar 下的 org.springframework.boot.autoconfigure 代码包。

这里,有各种各样事先写好的配置类。

我们思考下,为什么在 application.properties 中写上诸如 spring.datasource.url=XXX 的配置就能加载 jdbc 了?

03 application.properties

下面是我们在 application.properties 常用的加载 jdbc 的方式:

spring.datasource.url=jdbc:oracle:thin:@192.168.1.7:1521:orcl
spring.datasource.username=yaomaomao
spring.datasource.password=Iyaoshen369
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driverClassName=oracle.jdbc.driver.OracleDriver

这里配置的意思大家都应该明白,但是为什么它能起作用呢?

我们先从 spring.factories 文件中 org.springframework.boot.autoconfigure.EnableAutoConfiguration 这个 key 中找到 jdbc 相关的 value,如下,找到了这些:

……
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.JndiDataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration,\
……

打开 org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration 这个类,发现这样的代码

@Configuration(
    proxyBeanMethods = false
)
@ConditionalOnClass({DataSource.class, EmbeddedDatabaseType.class})
@EnableConfigurationProperties({DataSourceProperties.class})
@Import({DataSourcePoolMetadataProvidersConfiguration.class, DataSourceInitializationConfiguration.class})
public class DataSourceAutoConfiguration {
    public DataSourceAutoConfiguration() {
    }
}

主要看这段注解

@EnableConfigurationProperties({DataSourceProperties.class})

打开 DataSourceProperties 这个类,发现如下内容

@ConfigurationProperties(
    prefix = "spring.datasource"
)
public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {
    private ClassLoader classLoader;
    private String name;
    private boolean generateUniqueName;
    private Class<? extends DataSource> type;
    private String driverClassName;
    private String url;
    private String username;
    private String password;
    private String jndiName;
    ...
}

至此,我们基本上是明白了 @EnableAutoConfiguration 是怎么起作用的,又是怎么与 application.properties 关联的。

04 总结

最后,在面试时,我们能不能一句话总结下,springboot 是怎么做到简化配置的?

答:主要是 @EnableAutoConfiguration 这个注解起的作用,这个注解是间接隐藏在 springboot 的启动类注解 @SpringBootApplication 中。

通过这个注解,SpringApplication.run(...)的内部就会执行 selectImports()方法,寻找 META-INF/spring.factories 文件,读取里面的文件配置,将事先已经写好的自动配置类有选择地加载到 Spring 容器中,并且能按照约定的写法在 application.properties 中配置参数或开关。

  • Spring

    Spring 是一个开源框架,是于 2003 年兴起的一个轻量级的 Java 开发框架,由 Rod Johnson 在其著作《Expert One-On-One J2EE Development and Design》中阐述的部分理念和原型衍生而来。它是为了解决企业应用开发的复杂性而创建的。框架的主要优势之一就是其分层架构,分层架构允许使用者选择使用哪一个组件,同时为 JavaEE 应用程序开发提供集成的框架。

    944 引用 • 1459 回帖 • 17 关注
  • 代码
    466 引用 • 631 回帖 • 9 关注
  • 面试

    面试造航母,上班拧螺丝。多面试,少加班。

    325 引用 • 1395 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • someone

    过度依赖既有的可行路径