Springboot的自动配置是springboot的精髓,最近学习了相关原理,特在此记录一下
我们在使用springboot的时候,部分属性可以进行yml或properties文件的配置,比如server.port=8080等等,部分属性已经被自动配置了。我们如何知道哪些属性(比如server.port)是可以配置的呢?配置怎么写呢?自动配置的原理是什么?
- 方法一:查阅官方文档:https://www.springcloud.cc/spring-boot.html#common-application-properties
- 方法二:查询源码
接下来就对自动配置原理进行一定介绍
首先,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"
并生成键值对→取出其中key
为EnableAutoConfiguration
类名的键值对,转换成一个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})
:
- 开启指定类的
ConfigurationProperties
功能:
如图,该类上支持了@ConfigurationProperties
注解,自动从配置文件(yml或properties)中取出前缀为“server”
的配置项的值,并与类中的属性进行绑定,如图中的server.port
,server.address
等等。springboot中,所有能在配置文件(yml或properties)中配置的项,都封装在xxxProperties
中,如这里的ServerProperties
由此,可得出它的第二个功能: - 将配置文件中的值和
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
类绑定,又通过配置类内的属性properties
与xxxProperties
绑定,再用这个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类中获取某些属性。可以在配置文件中指定这些属性的值