前言
看完前面两节对的SpringBoot启动原理以及配置文件加载的分析之后,大家是不是已经对SpringBoot的运行机制有一个基本的了解了呢?但是这些还远远不够,往后看的朋友会发现我编写的SpringBoot入门到进阶系列的几乎每篇都会涉及到源码的分析,因为只有熟悉了SpringBoot的加载运行原理,才能更加灵活的使用SpringBoot做出自己想要的东西,在座的各位觉得呢?
(PS:当然啦,我的分析不一定是最准确的,但是想努力做到让大家能够看懂,如果有错误的地方,欢迎各位指正!)
那么本节,咱们继续学习SpringBoot的Conditional派生注解并针对SpringBoot的自动配置原理进行分析。
@Conditional派生注解
因为接下来我们的底层分析中包含大量的@Conditional
派生注解,因此为了避免大家在看的时候一脸懵逼,我们还是先来了解一下基本的@Conditional
派生注解有哪些,以及对应的意思是什么。
注解名称 | 满足条件 |
---|---|
@Conditional | 派生注解相关属性 |
@ConditionalOnJava | 系统的java版本是否符合要求 |
@ConditionalOnBean | 容器中存在指定Bean |
@ConditionalOnMissingBean | 容器中不存在指定Bean |
@ConditionalOnExpression | 满足SpEL表达式指定 |
@ConditionalOnClass | 系统中有指定的类 |
@ConditionalOnMissingClass | 系统中没有指定的类 |
@ConditionalOnSingleCandidate | 容器中只有一个指定的Bean,或者这个Bean是首选Bean |
@ConditionalOnProperty | 系统中指定的属性是否有指定的值 |
@ConditionalOnResource | 类路径下是否存在指定资源文件 |
@ConditionalOnWebApplication | 当前是web环境 |
@ConditionalOnNotWebApplication | 当前不是web环境 |
@ConditionalOnJndi | JNDI存在指定项 |
以上的注解只有在满足条件的时候,当前的配置类才会生效。
SpringBoot自动配置原理解析
之前没有接触过SpringBoot自动配置类的配置文件的朋友可能会很困惑,这些配置属性在properties文件或者yml文件该怎么写?到底哪些属性值可以被改变?
因此在分析之前给大家一个spring官方出具的关于所有自动配置类所对应的配置文件的填写指南
https://docs.spring.io/spring-boot/docs
内容很多,大家可以先点进去瞅瞅,大概就是下面这个样子。
其实此次分析相当于在之前的SpringBoot的启动原理之上略作补充,想要研究自动配置原理,咱们还是从 “源头” 开始调查起,虽然我们之前Debug得时候知道SpringBoot默认内置118个配置类,存在spring.factories文件中,但是到底怎么找到这个文件名字去加载呢?这还是个未知得 “谜题”,所以我们接着上回得旅程继续探险。
找到 “源头” 之后,咱们还是进入@EnableAutoConfiguration
注解中
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
//利用AutoConfigurationImportSelector选择器给Spring导入一些组件
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
...
}
咱们可以查看AutoConfigurationImportSelector.class
里的selectImports()方法的内容得知获取了哪些组件
@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());
}
咱们重点分析getAutoConfigurationEntry(),这个方法主要做的事情就是先获取所有类上有@Configuration
的配置类候选对象,并且移除掉那些可能重复的配置类以及被排除的配置类,因为我们实际在配置pom.xml的时候并没有导入并应用SpringBoot默认内置的118个配置类,因此SpringBoot默认会在此处进行一遍筛选,接着再判断配置类上是否有@OnClassCondition
注解,并判断是否满足条件,再进行删选一次,当所有的候选对象都筛选完成之后,再触发广播事件AutoConfigurationImportEvent
/**
* Return the {@link AutoConfigurationEntry} based on the {@link AnnotationMetadata}
* of the importing {@link Configuration @Configuration} class.
* @param autoConfigurationMetadata the auto-configuration metadata
* @param annotationMetadata the annotation metadata of the configuration class
* @return the auto-configurations that should be imported
*/
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的List集合中移除
//根据@OnClassCondition注解过滤调一些条件没有满足的配置类
configurations = filter(configurations, autoConfigurationMetadata);
// 找到所有需要被应用的候选配置类并触发广播事件AutoConfigurationImportEvent
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
好到这里,咱么的思路是理清楚了,但是这些候选配置对象具体从何而来呢?咱们打开getCandidateConfigurations
方法,这个方法本质上就是通过SpringFactoriesLoader调用loadFactoryNames()
方法从META-INF/spring.factories中获取候选对象的名称,当spring.factories文件是空文件是,会抛出异常"No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct."
因此,如果抛出此异常,只有两种情况,一是文件中的内容被误删除或者文件被误删了,再者就是文件的路径不对,SpringBoot在启动的时候找不到对应的文件。
/**
* Return the auto-configuration class names that should be considered. By default
* this method will load candidates using {@link SpringFactoriesLoader} with
* {@link #getSpringFactoriesLoaderFactoryClass()}.
* @param metadata the source metadata
* @param attributes the {@link #getAttributes(AnnotationMetadata) annotation
* attributes}
* @return a list of candidate configurations
*/
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
//扫描所有jar包类路径下的META-INF/spring.factories,把扫描到的这些文件的内容包装成properties对象
//并从properties中获取到EnableAutoConfiguration.class类(类名)对应的值,然后把他们添加在List容器中
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;
}
虽然根据上面的信息我们已经知道所有的候选对象都在META-INF/spring.factories中,但是还是想找出来他具体在哪个位置,反正也到这里了,咱们继续点开看看loadFactoryNames()
方法吧~
loadFactoryNames()
会先调用loadSpringFactories()
方法加载Spring的类工厂,这个方法中有个常量FACTORIES_RESOURCE_LOCATION
,值就是META-INF/spring.factories
,因此loadSpringFactories()本质上就是使用classLoader类加载器去META-INF/spring.factories
中加载配置文件,最后调用PropertiesLoaderUtils.loadProperties(resource)
方法将我们一个个UrlResource资源(代表着factories文件中一个个配置类的资源定位)封装为properties对象,因此在后面我们讲到SpringBoot自动管理的时候,会在每一个AutoConfiguration自动配置类中找到它对应的Properties类。至于后面的getOrDefault(factoryClassName, Collections.emptyList())
主要是判断loadSpringFactories(classLoader)
返回过来的Map中的key(factoryClassName的工厂名称)有没有具体对应的entry,如果没有则返回空的List列表而不是返回null,避免出现空指针异常。
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}
try {
//扫描所有jar包类路径下 META-INF/spring.factories
//把扫描到的这些文件的内容包装成properties对象
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
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()) {
String factoryClassName = ((String) entry.getKey()).trim();
for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
result.add(factoryClassName, factoryName.trim());
}
}
}
cache.put(classLoader, result);
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}
其实大家有兴趣可以自行Debug试试看,这里的result中是不是还有我们上节才学习的PropertySourceLoader(properties配置文件加载器)?这里的result对象本质上就是一个Map,从cache缓存对象中获取,如果说缓存中获取的result不为空则直接返回result对象。
点开PropertySourceLoader之后是不是发现咱们的两个 “老朋友” ?显然SpringBoot将PropertiesPropertySourceLoader和YamlPropertySourceLoader都作为配置文件资源加载器储存在同一个Map集合中了,key都为PropertySourceLoader
自动配置类进行自动配置功能
想了解自动配置,咱们得先找个 “模板” 吧,这里就以我们熟悉得HttpEncodingAutoConfiguration(编码自动配置器)为例子。大家先不要看下面得代码部分,仔细阅读一下类上得注解
@Configuration//代表这是一个配置文件
@EnableConfigurationProperties(HttpProperties.class) //启动配置文件的配置类
//代表启用这个配置文件,让它生效,必须是SERVLET的运行环境
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
//表示想要这个配置文件生效,必须要有CharacterEncodingFilter这个类
@ConditionalOnClass(CharacterEncodingFilter.class)
//如果配置了spring.http.encoding,那么当条件不成立,配置不生效 ,默认为true生效
@ConditionalOnProperty(prefix = "spring.http.encoding", value = "enabled", matchIfMissing = true)
public class HttpEncodingAutoConfiguration {
//它已经和SpringBoot的配置文件映射了,就是上面封装的properties类
private final HttpProperties.Encoding properties;
//只有一个有参构造器的情况下,参数的值就会从容器中拿
public HttpEncodingAutoConfiguration(HttpProperties properties) {
this.properties = properties.getEncoding();
}
//创建过滤器对象
@Bean //给容器中添加一个组件,这个组件的某些值需要从properties中获取
@ConditionalOnMissingBean //判断容器没有CharacterEncodingFilter这个组件?没有的话,此配置类不会生效
public CharacterEncodingFilter characterEncodingFilter() {
CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
filter.setEncoding(this.properties.getCharset().name());//设置编码集
filter.setForceRequestEncoding(this.properties.shouldForce(Type.REQUEST));//对请求设置编码
filter.setForceResponseEncoding(this.properties.shouldForce(Type.RESPONSE));//对相应设置编码
return filter;
}
}
所有在配置文件中能配置的属性都是在xxxxProperties类中封装着,因此配置文件配置什么,咱们就可以根据文件中的属性头,例如下方的spring.http.encoding
找到对应的属性类,如HttpEncodingProperties,然后通过http.encoding
找到这个类对应的配置属性,就可以通过.properties文件或者.yml文件对我们的类的属性进行自定义配置啦!是不是相当灵活~
//从配置文件中获取指定的值和bean的属性进行绑定
@ConfigurationProperties(prefix = "spring.http.encoding")
public class HttpEncodingProperties {
public static final Charset DEFAULT_CHARSET;
总结:
- SpringBoot启动的时候会加载大量的自动配置类,这些配置类都源自spring.factories文件
- 在创建项目之初,我们需要的功能有没有SpringBoot默认写好的自动配置类,有的话就不用配置啦,当然重复配置了,SpringBoot也会帮我们过滤筛选掉
- 接着再来看这个自动配置类中到底配置了哪些组件(只要我们要用的组件有,就不需要再来配置了)
- 给容器中自动配置类添加组件的时候,会从properties类中获取某些属性。我们就可
以在配置文件中指定这些属性的值- xxxxAutoConfigurartion:一般自动配置类的结尾都是AutoConfigurartion,用于给容器中添加组件
- xxxxProperties:一般自动配置类中都包含Properties类,封装配置文件中的默认配置