个人感觉应该是比较少的,Spring Boot 本质上还是曾经 SSM 那一套,只是通过各种 starter 简化了配置而已,其他都是一模一样的,所以 Spring Boot 中很多面试题还是得回归到 Spring 中去解答!当然这并不是说 Spring Boot 中没什么可问的,Spring Boot 中其实也有一个非常经典的面试题,那就是 Spring Boot 中的自动化配置是怎么实现的?
虽然我们在日常开发中,SpringBoot使用非常多,算是目前Java开发领域一个标配了,但是小伙伴们仔细想想自己的面试经历,和SpringBoot相关的面试题都有哪些?个人感觉应该是比较少的,SpringBoot本质上还是曾经SSM那一套,只是通过各种starter简化了配置而已,其他都是一模一样的,所以SpringBoot中很多面试题还是得回归到Spring中去解答!当然这并不是说SpringBoot中没什么可问的,SpringBoot中其实也有一个非常经典的面试题,那就是SpringBoot中的自动化配置是怎么实现的?今天松哥就来和各位小伙伴聊一下这个问题。
其实松哥之前和小伙伴们聊过相关的问题,不过都是零散的,没有系统梳理过,之前也带领小伙伴们自定义过一个starter,相信各位小伙伴对于starter的原理也有一定了解,所以今天这篇文章一些过于细节的内容我就不赘述了,大家可以翻看之前的文章。
1. @SpringBootApplication
要说SpringBoot的自动化配置,那必须从项目的启动类@SpringBootApplication说起,这是整个SpringBoot宇宙的起点,我们先来看下这个注解:
@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是一个包扫描注解,为什么SpringBoot项目中的Bean只要放对位置就会被自动扫描到,和这个注解有关。
别看这里注解多,其实真正由SpringBoot提供的注解一共就两个,分别是@SpringBootConfiguration和@EnableAutoConfiguration两个,其他注解在SpringBoot出现之前就已经存在多年了。
2. @EnableAutoConfiguration
接下来我们来看看@EnableAutoConfiguration是如何实现自动化配置的。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
}
这个注解起关键作用的就是两个东西:
- @AutoConfigurationPackage:这个表示自动扫描各种第三方的注解,在之前的文章中松哥已经和大家聊过这个注解的作用了,传送门:@AutoConfigurationPackage 和 @ComponentScan 有何区别?
- @Import则是在导入AutoConfigurationImportSelector配置类,这个配置类里边就是去加载各种自动化配置类的。
3. 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);
}
}
从类名就可以看出来,跟自动化配置相关的对象是由AutoConfigurationEntryautoConfigurationEntry=((AutoConfigurationImportSelector)deferredImportSelector).getAutoConfigurationEntry(annotationMetadata);进行加载的。
当然这里的getAutoConfigurationEntry方法实际上就是当前类提供的方法,我们来看下该方法:
protected AutoConfigurationEntry getAutoConfigurationEntry(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 = getConfigurationClassFilter().filter(configurations);
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
这里源码的方法命名都做的不错,基本上都能做到见名知意,小伙伴们日常开发中,应该向这样的命名思路看齐。接下来我们就来挨个看一下这里的关键方法。
3.1 isEnabled
首先调用 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;
}
3.2 getCandidateConfigurations
接下来调用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<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> 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文件中的自动化配置类。
如果这两个地方都没有加载到任何自动化配置类,那么就会抛出一个异常。
3.3 removeDuplicates
removeDuplicates方法表示移除候选自动化配置类中重复的类,移除的思路也很有意思,就用一个LinkedHashSet中转一下就行了,源码如下:
protected final <T> List<T> removeDuplicates(List<T> list) {
return new ArrayList<>(new LinkedHashSet<>(list));
}
可以看到这些源码里有时候一些解决思路也很有意思。
3.4 getExclusions
getExclusions方法表示需要获取到所有被排除的自动化配置类,这些被排除的自动化配置类可以从三个地方获取:
- 当前注解的exclude属性。
- 当前注解的excludeName属性。
- application.properties配置文件中的spring.autoconfigure.exclude属性。
来看一下相关源码:
protected Set<String> getExclusions(AnnotationMetadata metadata, AnnotationAttributes attributes) {
Set<String> excluded = new LinkedHashSet<>();
excluded.addAll(asList(attributes, "exclude"));
excluded.addAll(asList(attributes, "excludeName"));
excluded.addAll(getExcludeAutoConfigurationsProperty());
return excluded;
}
跟上面讲解的三点刚好对应。
3.5 checkExcludedClasses
这个方法是检查所有被排除的自动化配置类,由于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<String> configurations, Set<String> exclusions) {
List<String> 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<String> 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方法中抛出异常,前面截图中的异常就是来自这里。
3.6 removeAll
这个方法就一个任务,就是从configurations中移除掉那些被排除的自动化配置类。configurations本身就是List集合,exclusions则是一个Set集合,所以这里直接移除即可。
3.7 filter
现在我们已经加载了所有的自动化配置类了,但是这些配置类并不是都会生效,具体是否生效,还要看你的项目是否使用了具体的依赖。
例如,现在加载的自动化配置里里边就包含了 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.Cnotallow=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<AutoConfigurationImportFilter> filters = getAutoConfigurationImportFilters();
for (AutoConfigurationImportFilter filter : filters) {
invokeAwareMethods(filter);
}
this.configurationClassFilter = new ConfigurationClassFilter(this.beanClassLoader, filters);
}
return this.configurationClassFilter;
}
可以看到,这里获取到的过滤器都是 AutoConfigurationImportFilter 类型的,这个类型的过滤器只有三个实例,如下图:
从这三个实例的名字中,基本上就能看出来各自的作用:
- OnClassCondition:这个就是条件注解@ConditionalOnClass的判定条件,看名字就知道用来判断当前 classpath 下是否存在某个类。
- OnWebApplicationCondition:这个是条件注解ConditionalOnWebApplication的判定条件,用来判断当前系统环境是否是一个 Web 环境。
- OnBeanCondition:这个是条件注解@ConditionalOnBean的判定条件,就是判断当前系统下是否存在某个 Bean。
这里获取到的三个 AutoConfigurationImportFilter 过滤器其实就是上面这三个。接下来执行 filter 方法,如下:
List<String> filter(List<String> 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<String> 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 种也和小伙伴们讲过多次了,这里就不再啰嗦了~
©本文为清一色官方代发,观点仅代表作者本人,与清一色无关。清一色对文中陈述、观点判断保持中立,不对所包含内容的准确性、可靠性或完整性提供任何明示或暗示的保证。本文不作为投资理财建议,请读者仅作参考,并请自行承担全部责任。文中部分文字/图片/视频/音频等来源于网络,如侵犯到著作权人的权利,请与我们联系(微信/QQ:1074760229)。转载请注明出处:清一色财经