虽然我们在日常开发中,Spring Boot 使用非常多,算是目前 Java 开发领域一个标配了,但是小伙伴们仔细想想自己的面试经历,和 Spring Boot 相关的面试题都有哪些?个人感觉应该是比较少的,Spring Boot 本质上还是曾经 SSM 那一套,只是通过各种 starter 简化了配置而已,其他都是一模一样的,所以 Spring Boot 中很多面试题还是得回归到 Spring 中去解答!当然这并不是说 Spring Boot 中没什么可问的,Spring Boot 中其实也有一个非常经典的面试题,那就是 Spring Boot 中的自动化配置是怎么实现的?今天就来和各位小伙伴聊一下这个问题。
其实之前和小伙伴们聊过相关的问题,不过都是零散的,没有系统梳理过,之前也带领小伙伴们自定义过一个 starter,相信各位小伙伴对于 starter 的原理也有一定了解,所以今天这篇文章一些过于细节的内容我就不赘述了,大家可以翻看之前的文章。
要说 Spring Boot 的自动化配置,那必须从项目的启动类 @SpringBootApplication
说起,这是整个 Spring Boot 宇宙的起点,我们先来看下这个注解:
@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 {
}
可以看到,@SpringBootApplication
注解组合了多个常见注解的功能,其中:
@SpringBootConfiguration
是一个支持配置类的注解,这里我们也不做讨论。@EnableAutoConfiguration
这个注解就表示开启自动化配置,这是我们今天要聊得重点。@ComponentScan
是一个包扫描注解,为什么 Spring Boot
项目中的 Bean
只要放对位置就会被自动扫描到,和这个注解有关。别看这里注解多,其实真正由 Spring Boot
提供的注解一共就两个,分别是 @SpringBootConfiguration
和 @EnableAutoConfiguration
两个,其他注解在 Spring Boot
出现之前就已经存在多年了。
接下来我们来看看 @EnableAutoConfiguration
是如何实现自动化配置的。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
}
这个注解起关键作用的就是两个东西:
@AutoConfigurationPackage
:这个表示自动扫描各种第三方的注解,在之前的文章中已经和大家聊过这个注解的作用了,传送门:@AutoConfigurationPackage 和 @ComponentScan 有何区别?@Import
则是在导入 AutoConfigurationImportSelector
配置类,这个配置类里边就是去加载各种自动化配置类的。AutoConfigurationImportSelector
类中的方法比较多,入口的地方则是 process 方法,所以我们这里就从 process 方法开始看起:
@Override
public void process(AnnotationMetadata annotationMetadata, DeferredImportSelector deferredImportSelector) {
Assert.state(deferredImportSelector instanceof AutoConfigurationImportSelector,
() -> String.format("Only %s implementations are supported, got %s",
AutoConfigurationImportSelector.class.getSimpleName(),
deferredImportSelector.getClass().getName()));
AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector) deferredImportSelector)
.getAutoConfigurationEntry(annotationMetadata);
this.autoConfigurationEntries.add(autoConfigurationEntry);
for (String importClassName : autoConfigurationEntry.getConfigurations()) {
this.entries.putIfAbsent(importClassName, annotationMetadata);
}
}
从类名就可以看出来,跟自动化配置相关的对象是由 AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector) deferredImportSelector).getAutoConfigurationEntry(annotationMetadata);
进行加载的。
当然这里的 getAutoConfigurationEntry
方法实际上就是当前类提供的方法,我们来看下该方法:
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
AnnotationAttributes attributes = getAttributes(annotationMetadata);
List configurations = getCandidateConfigurations(annotationMetadata, attributes);
configurations = removeDuplicates(configurations);
Set exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = getConfigurationClassFilter().filter(configurations);
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
这里源码的方法命名都做的不错,基本上都能做到见名知意,小伙伴们日常开发中,应该向这样的命名思路看齐。接下来我们就来挨个看一下这里的关键方法。
首先调用 isEnabled 方法去判断自动化配置到底有没有开启,这个主要是因为我们及时在项目中引入了 spring-boot-starter-xxx 之后,我们也可以通过在 application.properties 中配置 spring.boot.enableautoconfiguration=false
来关闭所有的自动化配置。
相关源码如下:
protected boolean isEnabled(AnnotationMetadata metadata) {
if (getClass() == AutoConfigurationImportSelector.class) {
return getEnvironment().getProperty(EnableAutoConfiguration.ENABLED_OVERRIDE_PROPERTY, Boolean.class, true);
}
return true;
}
接下来调用 getCandidateConfigurations
方法去获取所有候选的自动化配置类,这些候选的自动化配置类主要来自两个地方:
starter
中和大家聊过,我们需要在 claspath\:META-INF/spring.factories
中定义出来所有的自动化配置类,这是来源一。Spring Boot
自带的自动化配置类,这个在之前的 vhr 视频中也和小伙伴们多次讲过,Spring Boot
自带的自动化配置类位于 spring-boot-autoconfigure-3.0.6.jar!\META-INF\spring\org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件中。相关源码如下:
protected List getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List configurations = new ArrayList<>(
SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()));
ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader()).forEach(configurations::add);
Assert.notEmpty(configurations,
"No auto configuration classes found in META-INF/spring.factories nor in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you "
+ "are using a custom packaging, make sure that file is correct.");
return configurations;
}
这里加载到的自动化配置类的全路径被存入到 configurations
对象中,该对象有两个获取的地方:
SpringFactoriesLoader.loadFactoryNames
方法获取,这个方法细节我就不带大家看了,比较简单,本质上就是去加载 META-INF/spring.factories
文件,这个文件中定义了大量的自动化配置类的全路径。ImportCandidates.load
方法去加载,这个就是加载 spring-boot-autoconfigure-3.0.6.jar!\META-INF\spring\org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件中的自动化配置类。如果这两个地方都没有加载到任何自动化配置类,那么就会抛出一个异常。
removeDuplicates
方法表示移除候选自动化配置类中重复的类,移除的思路也很有意思,就用一个 LinkedHashSet
中转一下就行了,源码如下:
protected final List removeDuplicates(List list) {
return new ArrayList<>(new LinkedHashSet<>(list));
}
可以看到这些源码里有时候一些解决思路也很有意思。
getExclusions
方法表示需要获取到所有被排除的自动化配置类,这些被排除的自动化配置类可以从三个地方获取:
exclude
属性。excludeName
属性。application.properties
配置文件中的 spring.autoconfigure.exclude
属性。来看一下相关源码:
protected Set getExclusions(AnnotationMetadata metadata, AnnotationAttributes attributes) {
Set excluded = new LinkedHashSet<>();
excluded.addAll(asList(attributes, "exclude"));
excluded.addAll(asList(attributes, "excludeName"));
excluded.addAll(getExcludeAutoConfigurationsProperty());
return excluded;
}
跟上面讲解的三点刚好对应。
这个方法是检查所有被排除的自动化配置类,由于 Spring Boot
中的自动化配置类可以自定义,并不需要统一实现某一个接口或者统一继承某一个类,所以在写排除类的时候,如果写错了编译是校验不出来的,像下面这种:
@SpringBootApplication(exclude = HelloController.class)
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
由于 HelloController
并不是一个自动化配置类,所以这样写项目启动的时候就会报错,如下:
这个异常从哪来的呢?其实就是来自 checkExcludedClasses
方法,我们来看下该方法:
private void checkExcludedClasses(List configurations, Set exclusions) {
List invalidExcludes = new ArrayList<>(exclusions.size());
for (String exclusion : exclusions) {
if (ClassUtils.isPresent(exclusion, getClass().getClassLoader()) && !configurations.contains(exclusion)) {
invalidExcludes.add(exclusion);
}
}
if (!invalidExcludes.isEmpty()) {
handleInvalidExcludes(invalidExcludes);
}
}
protected void handleInvalidExcludes(List invalidExcludes) {
StringBuilder message = new StringBuilder();
for (String exclude : invalidExcludes) {
message.append("\t- ").append(exclude).append(String.format("%n"));
}
throw new IllegalStateException(String.format(
"The following classes could not be excluded because they are not auto-configuration classes:%n%s",
message));
}
可以看到,在 checkExcludedClasses
方法中,会首先找到所有位于当前类路径下但是却不包含在 configurations
中的所有被排除的自动化配置类,由于 configurations
中的就是所有的自动化配置类了,所以这些不存在于 configurations
中的类都是有问题的,都不是自动化配置类,将这些有问题的类收集起来,存入到 invalidExcludes
变量中,然后再进行额外的处理。
所谓额外的处理就是在 handleInvalidExcludes
方法中抛出异常,前面截图中的异常就是来自这里。
这个方法就一个任务,就是从 configurations
中移除掉那些被排除的自动化配置类。configurations
本身就是 List
集合,exclusions
则是一个 Set
集合,所以这里直接移除即可。
现在我们已经加载了所有的自动化配置类了,但是这些配置类并不是都会生效,具体是否生效,还要看你的项目是否使用了具体的依赖。
例如,现在加载的自动化配置里里边就包含了 RedisAutoConfiguration,这个是自动配置 Redis 的,但是由于我的项目中并没有使用 Redis,所以这个自动化配置类并不会生效。这个过程就是由 getConfigurationClassFilter().filter(configurations);
来完成的。
先说一个预备知识:
由于我们项目中的自动化配置类特别多,每一个自动化配置类都会依赖别的类,当别的类存在时,这个自动化配置类才会生效,这一堆互相之间的依赖关系,存在于 spring-boot-autoconfigure-3.0.6.jar!/META-INF/spring-autoconfigure-metadata.properties
文件之中,我随便举一个该文件中的配置:
org.springframework.boot.autoconfigure.amqp.RabbitAnnotationDrivenConfiguration.ConditionalOnClass=org.springframework.amqp.rabbit.annotation.EnableRabbit
表示 RabbitAnnotationDrivenConfiguration 类要生效有一个必备条件就是当前项目类路径下要存在 org.springframework.amqp.rabbit.annotation.EnableRabbit
。我们来看看 RabbitAnnotationDrivenConfiguration 类的注解:
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(EnableRabbit.class)
class RabbitAnnotationDrivenConfiguration {
}
这个类和配置文件中的内容一致。
这个预备知识搞懂了,接下来的内容就好理解了。
先来看 getConfigurationClassFilter 方法,这个就是获取所有的过滤器,如下:
private ConfigurationClassFilter getConfigurationClassFilter() {
if (this.configurationClassFilter == null) {
List filters = getAutoConfigurationImportFilters();
for (AutoConfigurationImportFilter filter : filters) {
invokeAwareMethods(filter);
}
this.configurationClassFilter = new ConfigurationClassFilter(this.beanClassLoader, filters);
}
return this.configurationClassFilter;
}
可以看到,这里获取到的过滤器都是 AutoConfigurationImportFilter 类型的,这个类型的过滤器只有三个实例,如下图:
从这三个实例的名字中,基本上就能看出来各自的作用:
@ConditionalOnClass
的判定条件,看名字就知道用来判断当前 classpath 下是否存在某个类。ConditionalOnWebApplication
的判定条件,用来判断当前系统环境是否是一个 Web 环境。@ConditionalOnBean
的判定条件,就是判断当前系统下是否存在某个 Bean。这里获取到的三个 AutoConfigurationImportFilter 过滤器其实就是上面这三个。接下来执行 filter 方法,如下:
List filter(List configurations) {
long startTime = System.nanoTime();
String[] candidates = StringUtils.toStringArray(configurations);
boolean skipped = false;
for (AutoConfigurationImportFilter filter : this.filters) {
boolean[] match = filter.match(candidates, this.autoConfigurationMetadata);
for (int i = 0; i < match.length; i++) {
if (!match[i]) {
candidates[i] = null;
skipped = true;
}
}
}
if (!skipped) {
return configurations;
}
List result = new ArrayList<>(candidates.length);
for (String candidate : candidates) {
if (candidate != null) {
result.add(candidate);
}
}
return result;
}
这里就是遍历这三个过滤器,然后分别调用各自的 match 方法和 144 个自动化配置类进行匹配,如果这些自动化配置类所需要的条件得到满足,则 match 数组对应的位置就为 true,否则就为 false。
然后遍历 match 数组,将不满足条件的自动化配置类置为 null,最后再把这些 null 移除掉。
这样就获取到了我们需要进行自动化配置的类了。
最后一句 fireAutoConfigurationImportEvents 则是触发自动化配置类导入事件,这个没啥好说的~
当这些自动化配置类加载进来之后,接下来就是各种条件注解来决定这些配置类是否生效了,这些都比较简单了,之前在 vhr 种也和小伙伴们讲过多次了,这里就不再啰嗦了~
好啦,经过上面的梳理相信小伙伴们对 Spring Boot 自动化配置类的加载有一个大概的认知了吧~