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

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

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 应用程序开发提供集成的框架。

    946 引用 • 1460 回帖 • 1 关注
  • 代码
    467 引用 • 586 回帖 • 9 关注
  • 面试

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

    325 引用 • 1395 回帖

相关帖子

欢迎来到这里!

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

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

    过度依赖既有的可行路径