个人感觉应该是比较少的,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的原理也有一定了解,所以今天这篇文章一些过于细节的内容我就不赘述了,大家可以翻看之前的文章。
(资料图)
要说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出现之前就已经存在多年了。
接下来我们来看看@EnableAutoConfiguration是如何实现自动化配置的。
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@AutoConfigurationPackage@Import(AutoConfigurationImportSelector.class)public @interface EnableAutoConfiguration {}
这个注解起关键作用的就是两个东西:
@AutoConfigurationPackage:这个表示自动扫描各种第三方的注解,在之前的文章中松哥已经和大家聊过这个注解的作用了,传送门:@AutoConfigurationPackage 和 @ComponentScan 有何区别?@Import则是在导入AutoConfigurationImportSelector配置类,这个配置类里边就是去加载各种自动化配置类的。AutoConfigurationImportSelector类中的方法比较多,入口的地方则是process方法,所以我们这里就从process方法开始看起:
@Overridepublic 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 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.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 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 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 种也和小伙伴们讲过多次了,这里就不再啰嗦了~
推荐阅读
关于我们| 联系方式| 版权声明| 供稿服务| 友情链接
咕噜网 www.cngulu.com 版权所有,未经书面授权禁止使用
Copyright©2008-2023 By All Rights Reserved 皖ICP备2022009963号-10
联系我们: 39 60 29 14 2@qq.com