一直以来都把Spring Boot看作一个黑盒子来使用,虽然也了解过一些原理,但还是朦朦胧胧的感觉。今天就来学习、记录下Spring Boot的自动配置原理。
在新建好一个Spring Boot项目后,会有如下经典启动类:
@SpringBootApplication
public class BlogApplication {
public static void main(String[] args) {
SpringApplication.run(BlogApplication.class, args);
}
}
自动配置的奥秘就隐藏在 @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 {
@EnableAutoConfiguration
又使用@Import
导入了AutoConfigurationImportSelector.class
,从命名就能看出来,这个类会选择导入需要自动配置的类,进而查看:
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
.loadMetadata(this.beanClassLoader);
// 这里获取需要自动配置的类
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata,
annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
这个类重写了顶级接口ImportSelector的 selectImports
方法来导入自动配置类,其中又调用了自身的getAutoConfigurationEntry
方法,进一步查看:
protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,
AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
AnnotationAttributes attributes = getAttributes(annotationMetadata);
// 获取需要自动配置的候选类
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
// 去重
configurations = removeDuplicates(configurations);
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = filter(configurations, autoConfigurationMetadata);
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
这个方法除了获取需要的配置类,还做了一些其他工作,比如去重、按照约定再排除一部分(比如有时候我们不需要加载某个功能,就可以在 @SpringBootApplication
中设置exclude
字段来排除)。这里着重看获取配置类,即查看getCandidateConfigurations
方法:
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
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;
}
protected Class<?> getSpringFactoriesLoaderFactoryClass() {
return EnableAutoConfiguration.class;
}
protected ClassLoader getBeanClassLoader() {
return this.beanClassLoader;
}
这里又调用了SpringFactoriesLoader.loadFactoryNames
来加载配置,并传入两个参数,一个是自动配置类的Class类型:EnableAutoConfiguration.class
,一个是本类的一个类加载器:this.beanClassLoader
。注意当前类AutoConfigurationImportSelector是在org.springframework.boot.autoconfigure
包下的。
查看SpringFactoriesLoader.loadFactoryNames
方法:
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
// 获取Class文件的全限定名,这里对于自动配置,即EnableAutoConfiguration.class.getName()
// 结果是:org.springframework.boot.autoconfigure.EnableAutoConfiguration
String factoryTypeName = factoryType.getName();
// 继续根据下边的私有方法,通过给定的类加载器加载
return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
// 先从缓存中取
MultiValueMap<String, String> result = cache.get(classLoader);
// 如果缓存已加载过,则直接返回结果
if (result != null) {
return result;
}
// 没加载过
try {
// 加载文件,获得URL集合
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap<>();
// 遍历URL集合
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
// 读取配置文件
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryTypeName = ((String) entry.getKey()).trim();
for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
result.add(factoryTypeName, factoryImplementationName.trim());
}
}
}
cache.put(classLoader, result);
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}
这里首先可以看到的是缓存的应用,这也是很多源码中的老套路了,把加载到的东西存起来,下次再加载的时候直接取就行了,很值得学习的一点。
此外,可以看出来,首次加载的时候,也即缓存中不存在的时候,它是通过类加载器去加载类路径下的FACTORIES_RESOURCE_LOCATION
指明的文件,这里它是一个 static final
的字符串,为"META-INF/spring.factories"
,也就是说,会去加载类路径下的META-INF文件夹下的spring.factories
配置文件。下边具体解析过程等看完配置文件之后再看,先去找找配置文件。
因为当前是在追溯自动配置,所以就查看自动配置包,也即前边所说的org.springframework.boot.autoconfigure
包。查看该包的内容:
果然,有个指定目录下的spring.factories
文件,查看该文件,这里有很多内容,当然也有想要的自动配置相关内容:
这里是以键值对的形式给出了配置,key是某个类的全限定名,value是很多需要自动配置的类的全限定名(类名都是xxxAutoConfiguration格式)的长串,彼此之间用逗号,
隔开(\
只表示换行而已,无实际用处)。可以看到很多熟悉的面孔,比如切面AOP的自动配置(AopAutoConfiguration
),缓存的自动配置(CacheAutoConfiguration
)。
现在回到之前的解析过程:
// 新建一个result,result是一个特殊的map,value是一个列表List。即Map>类型
// 可以查看LinkedMultiValueMap的定义,底层就是依托一个Map>
result = new LinkedMultiValueMap<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
// 获取键值对
for (Map.Entry<?, ?> entry : properties.entrySet()) {
// 获取key的名字,比如org.springframework.boot.autoconfigure.EnableAutoConfiguration
String factoryTypeName = ((String) entry.getKey()).trim();
// 这里是调用一个工具类方法来获取value,该工具类以 ,作为分隔符,把字符串分割为字符串数组
for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
result.add(factoryTypeName, factoryImplementationName.trim());
}
}
}
// 结果放入缓存,以类加载器为key,result为value
cache.put(classLoader, result);
return result;
现在看这个解析过程就比较清晰了,它是把原始文件中的键值对做了修改,原始文件中的value是以,
分割的字符串,这里通过构造一个result,将value改为一个List。
再进一步,看看把这些自动配置类加载进去后会做什么,以典型的ServletWebServerFactoryAutoConfiguration类为例,查看它的定义:
// 表明当前是个配置类
@Configuration(proxyBeanMethods = false)
// 指明自动配置类顺序
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
// 条件判断,只有在指定类加载后,才加载这个配置
@ConditionalOnClass(ServletRequest.class)
// web环境下才加载这个配置
@ConditionalOnWebApplication(type = Type.SERVLET)
// 绑定配置文件
@EnableConfigurationProperties(ServerProperties.class)
// 导入其它配置
@Import({ ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,
ServletWebServerFactoryConfiguration.EmbeddedTomcat.class,
ServletWebServerFactoryConfiguration.EmbeddedJetty.class,
ServletWebServerFactoryConfiguration.EmbeddedUndertow.class })
public class ServletWebServerFactoryAutoConfiguration {
// 添加ServletWebServerFactoryCustomizer组件
@Bean
public ServletWebServerFactoryCustomizer servletWebServerFactoryCustomizer(ServerProperties serverProperties) {
return new ServletWebServerFactoryCustomizer(serverProperties);
}
@Bean
@ConditionalOnClass(name = "org.apache.catalina.startup.Tomcat")
public TomcatServletWebServerFactoryCustomizer tomcatServletWebServerFactoryCustomizer(
ServerProperties serverProperties) {
return new TomcatServletWebServerFactoryCustomizer(serverProperties);
}
...
这里可以看到它对配置做了一些要求,比如加载顺序、加载条件等,并且还有几个标记了@Bean
注解的方法。在配置类中,这些方法会给spring容器中添加对应的组件,添加进去以后我们就能获取、使用这些组件了。
此外,这个配置类还绑定一个对应的配置文件 @EnableConfigurationProperties(ServerProperties.class)
,它可以根据这个绑定的配置文件做个性化的设置。查看这个配置文件类定义:
@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
public class ServerProperties {
/**
* Server HTTP port.
*/
private Integer port;
/**
* Network address to which the server should bind.
*/
private InetAddress address;
这里就和我们日常使用的比较近了。@ConfigurationProperties
标注当前类是个配置文件,它绑定前缀为“server”的配置(prefix = "server"
),下边的私有字段就是可配置的属性。比如port
,这就对应着我们经常在配置文件中写的server.port=8080
,指明绑定的端口号。上边的配置类中:
@Bean
@ConditionalOnClass(name = "org.apache.catalina.startup.Tomcat")
public TomcatServletWebServerFactoryCustomizer tomcatServletWebServerFactoryCustomizer(
ServerProperties serverProperties) {
return new TomcatServletWebServerFactoryCustomizer(serverProperties);
}
就把这个配置文件的内容(serverProperties
)传递给了具体的组件(可见默认是Tomcat),我们配置的端口号就会通过这条路径最终传递给Tomcat,并最终让它在指定端口监听。
简单总结下Spring Boot自动配置的流程:
@EnableAutoConfiguration
注解开启自动配置(@SpringBootApplication
注解默认已包含)META-INF/spring.factories
文件,读取以EnableAutoConfiguration的全限定类名对应的值,作为候选配置类。这里默认给出了很多组件的自动配置类。@Import
),或者给出一些配置条件,并且会通过@Bean
注解把该组件所包含的组件注入到spring容器中以供使用。application.properties
中的指定前缀绑定。第3步注入组件的时候,组件可能还会获取配置文件类中的内容,所以用户可以在application.properties
修改指定配置,来制定自己的个性化服务。