@[toc] 虽然咱们在日常开发中,Spring Boot 运用十分多,算是目前 Java 开发领域一个标配了,可是小伙伴们细心想想自己的面试阅历,和 Spring Boot 相关的面试题都有哪些?个人感觉应该是比较少的,Spring Boot 本质上仍是曾经 SSM 那一套,仅仅通过各种 starter 简化了装备罢了,其他都是如出一辙的,所以 Spring Boot 中很多面试题仍是得回归到 Spring 中去解答!当然这并不是说 Spring Boot 中没什么可问的,Spring Boot 中其实也有一个十分经典的面试题,那便是 Spring Boot 中的主动化装备是怎样完成的?今日松哥就来和各位小伙伴聊一下这个问题。

其实松哥之前和小伙伴们聊过相关的问题,不过都是零星的,没有体系梳理过,之前也带领小伙伴们自界说过一个 starter,信任各位小伙伴对于 starter 的原理也有必定了解,所以今日这篇文章一些过于细节的内容我就不赘述了,咱们能够翻看之前的文章。

1. @SpringBootApplication

要说 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 呈现之前就已经存在多年了。

2. @EnableAutoConfiguration

接下来咱们来看看 @EnableAutoConfiguration 是怎么完成主动化装备的。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
}

这个注解起要害作用的便是两个东西:

  1. @AutoConfigurationPackage:这个表明主动扫描各种第三方的注解,在之前的文章中松哥已经和咱们聊过这个注解的作用了,传送门:@AutoConfigurationPackage 和 @ComponentScan 有何差异?
  2. @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);
	}
}

从类名就能够看出来,跟主动化装备相关的目标是由 AutoConfigurationEntry autoConfigurationEntry = ((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 办法去获取一切候选的主动化装备类,这些候选的主动化装备类首要来自两个当地:

  1. 在之前的自界说 starter 中松哥和咱们聊过,咱们需求在 claspath\:META-INF/spring.factories 中界说出来一切的主动化装备类,这是来源一。
  2. 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 目标中,该目标有两个获取的当地:

  1. 调用 SpringFactoriesLoader.loadFactoryNames 办法获取,这个办法细节我就不带咱们看了,比较简单,本质上便是去加载 META-INF/spring.factories 文件,这个文件中界说了很多的主动化装备类的全途径。
  2. 调用 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 办法表明需求获取到一切被扫除的主动化装备类,这些被扫除的主动化装备类能够从三个当地获取:

  1. 当时注解的 exclude 特点。
  2. 当时注解的 excludeName 特点。
  3. 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 并不是一个主动化装备类,所以这样写项目发动的时分就会报错,如下:

Spring Boot 启动注解分析

这个反常从哪来的呢?其实便是来自 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.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<AutoConfigurationImportFilter> filters = getAutoConfigurationImportFilters();
		for (AutoConfigurationImportFilter filter : filters) {
			invokeAwareMethods(filter);
		}
		this.configurationClassFilter = new ConfigurationClassFilter(this.beanClassLoader, filters);
	}
	return this.configurationClassFilter;
}

能够看到,这儿获取到的过滤器都是 AutoConfigurationImportFilter 类型的,这个类型的过滤器只有三个实例,如下图:

Spring Boot 启动注解分析

从这三个实例的姓名中,基本上就能看出来各自的作用:

  • 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 种也和小伙伴们讲过多次了,这儿就不再烦琐了~

好啦,经过上面的梳理信任小伙伴们对 Spring Boot 主动化装备类的加载有一个大概的认知了吧~