前言
本文详细解析面试重点—SpringBoot自动配置原理、场景启动器原理,深入源码,直接上干货、绝不拖泥带水。
一、SpringBoot优势概要
一句话:约定大于配置,简化Spring的简化
- 内嵌Web服务器
- 环境启动器starter的使用,简化构建配置
- 无需编写繁琐XML文件
二、SpringBoot自动配置
1. ☠注意☠
首先,要明确自动配置和自动装配的区别。
自动装配是SpringBoot的概念,是通过条件注解和配置元数据的方式,根据应用程序的依赖和环境,自动为应用程序进行配置。
自动装配是Spring的概念,是Spring框架通过依赖注入(Dependency Injection)机制来实现的,它通过自动根据类型或名称匹配来自动关联和装配Bean。
!!!!网络上有很多文章、视频张嘴就是SpringBoot的自动装配,这些基本不用看了,属实有些乐色!
2.自动配置详解
- 创建一个SpringBoot2项目用作示例:
主配置类:
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringBootTestApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootTestApplication.class, args);
}
}
POM文件:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<packaging>pom</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>SpringBoot-Test</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>SpringBoot-Test</name>
<description>SpringBoot-Test</description>
<properties>
<java.version>8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
- 项目结构分析
通过主配置类可以看出SpringBoot的最重要注解是@SpringBootApplication
通过POM文件可知SpringBoot项目的父项目是spring-boot-starter-parent
- @SpringBootApplication详解
在IDEA中按住CTRL键进入@SpringBootApplication注解:
package org.springframework.boot.autoconfigure;
import ...;
@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 {
@AliasFor(
annotation = EnableAutoConfiguration.class
)
Class<?>[] exclude() default {};
......
}
可以看到@SpringBootApplication主要由:
@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan注解构成
我们逐个分析以上三个注解:
1. @SpringBootConfiguration注解
源码:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
@Indexed
public @interface SpringBootConfiguration {
@AliasFor(
annotation = Configuration.class
)
boolean proxyBeanMethods() default true;
}
可以看出该注解由@Configuration注解和其他注解组成,所以,该注解的作用和@Configuration注解作用相似,代表当前是一个SpringBoot配置类。
2. @EnableAutoConfiguration注解
源码:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
该注解包含的注解主要有:@AutoConfigurationPackage、@Import({AutoConfigurationImportSelector.class})
- @AutoConfigurationPackage源码:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({Registrar.class})
public @interface AutoConfigurationPackage {
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
}
可以看出@AutoConfigurationPackage通过内部的@Import注解向IOC容器中导入了类: Registrar.class
Registrar.class源码:
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
Registrar() {
}
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
AutoConfigurationPackages.register(registry, (String[])(new AutoConfigurationPackages.PackageImports(metadata)).getPackageNames().toArray(new String[0]));
}
public Set<Object> determineImports(AnnotationMetadata metadata) {
return Collections.singleton(new AutoConfigurationPackages.PackageImports(metadata));
}
}
发现,@AutoConfigurationPackage利用Registrar给容器中导入一系列组件,将主配置类所在包下的所有组件导入容器中。
- @Import({AutoConfigurationImportSelector.class})源码:
public class AutoConfigurationImportSelector implements DeferredImportSelector,
BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware,
Ordered{
.......
}
发现AutoConfigurationImportSelector实现了DeferredImportSelector接口,这就不得不提一下@Import注解的使用方式了,可以看看这篇文章:
@Import三种使用方式
@Import作用是向IOC容器中导入组建,其中有一种就是实现ImportSelector接口,并实现其中的selectImports方法,该方法的作用是返回一个由想要导入IOC容器的对象组成的数组。
查看AutoConfigurationImportSelector中实现的selectImports方法:
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return NO_IMPORTS;
} else {
AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
}
发现其中调用了getAutoConfigurationEntry方法,继续跟进:
protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
} else {
AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
configurations = this.removeDuplicates(configurations);
Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
this.checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = this.getConfigurationClassFilter().filter(configurations);
this.fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
}
}
很明显,getAutoConfigurationEntry方法又调用了getCandidateConfigurations方法,继续深入:
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.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;
}
发现getCandidateConfigurations调用了SpringFactoriesLoader的loadFactoryNames方法:
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
ClassLoader classLoaderToUse = classLoader;
if (classLoader == null) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}
String factoryTypeName = factoryType.getName();
return (List)loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}
此方法又调用了loadSpringFactories方法:
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
Map<String, List<String>> result = (Map)cache.get(classLoader);
if (result != null) {
return result;
} else {
HashMap result = new HashMap();
try {
Enumeration urls = classLoader.getResources("META-INF/spring.factories");
while(urls.hasMoreElements()) {
URL url = (URL)urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
Iterator var6 = properties.entrySet().iterator();
while(var6.hasNext()) {
Entry<?, ?> entry = (Entry)var6.next();
String factoryTypeName = ((String)entry.getKey()).trim();
String[] factoryImplementationNames = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
String[] var10 = factoryImplementationNames;
int var11 = factoryImplementationNames.length;
for(int var12 = 0; var12 < var11; ++var12) {
String factoryImplementationName = var10[var12];
((List)result.computeIfAbsent(factoryTypeName, (key) -> {
return new ArrayList();
})).add(factoryImplementationName.trim());
}
}
}
result.replaceAll((factoryType, implementations) -> {
return (List)implementations.stream().distinct().collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
});
cache.put(classLoader, result);
return result;
} catch (IOException var14) {
throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var14);
}
}
}
可以看出其从META-INF/spring.factories位置来加载一个文件。默认扫描我们当前系统里面所有META-INF/spring.factories位置的文件。经过一层层返回之后,使用@Import注解导入到IOC容器
3. @ComponentScan注解
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
此注解作用是默认扫描主配置类所在包及其子包中的@Component注解及其衍生注解
三、Starter(场景启动器)原理
1. 什么是场景启动器
Spring Boot场景启动器(Starter)是Spring Boot框架提供的一种便利机制,用于快速引入和配置特定场景下所需的依赖项。每个场景启动器都是一个独立的Maven项目,它提供了一组相关的依赖和配置,以便简化和加速特定场景下的应用程序开发。
2. 场景启动器特点
- 集成依赖项:每个场景启动器都定义了一组相关的依赖项。这些依赖项被认为在特定场景中非常有用,比如Web开发、数据库访问、消息队列等等。通过使用场景启动器,开发者可以快速引入和配置所需的依赖项,而无需手动添加和管理这些依赖项。
- 自动配置:场景启动器还提供了一些默认的自动配置,旨在简化特定场景下的配置工作。这些自动配置通常基于Spring Boot的条件注解和配置元数据机制,根据应用程序的依赖和环境来自动配置必要的组件和选项。通过使用场景启动器,开发者可以快速启动和运行基本的应用程序,而无需过多关注和编写配置代码。
- 统一引入和管理:场景启动器通过简化和标准化依赖项的引入和配置,提供了一种统一的方式来解决常见的开发场景。开发者可以通过在项目的pom.xml文件中添加相应的场景启动器依赖,一次性引入和管理所有相关的依赖项,从而简化了项目的依赖管理和版本冲突的问题。
3. 以spring-boot-starter-web为示例
import ....;
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnWebApplication(
type = Type.SERVLET
)
@ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class})
@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})
@AutoConfigureOrder(-2147483638)
@AutoConfigureAfter({DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class})
public class WebMvcAutoConfiguration {
public static final String DEFAULT_PREFIX = "";
public static final String DEFAULT_SUFFIX = "";
private static final String SERVLET_LOCATION = "/";
public WebMvcAutoConfiguration() {
}
@Bean
@ConditionalOnMissingBean({HiddenHttpMethodFilter.class})
@ConditionalOnProperty(
prefix = "spring.mvc.hiddenmethod.filter",
name = {"enabled"}
)
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new OrderedHiddenHttpMethodFilter();
}
@Bean
@ConditionalOnMissingBean({FormContentFilter.class})
@ConditionalOnProperty(
prefix = "spring.mvc.formcontent.filter",
name = {"enabled"},
matchIfMissing = true
)
public OrderedFormContentFilter formContentFilter() {
return new OrderedFormContentFilter();
}
static class OptionalPathExtensionContentNegotiationStrategy implements ContentNegotiationStrategy {
private static final String SKIP_ATTRIBUTE = PathExtensionContentNegotiationStrategy.class.getName() + ".SKIP";
private final ContentNegotiationStrategy delegate;
OptionalPathExtensionContentNegotiationStrategy(ContentNegotiationStrategy delegate) {
this.delegate = delegate;
}
public List<MediaType> resolveMediaTypes(NativeWebRequest webRequest) throws HttpMediaTypeNotAcceptableException {
Object skip = webRequest.getAttribute(SKIP_ATTRIBUTE, 0);
return skip != null && Boolean.parseBoolean(skip.toString()) ? MEDIA_TYPE_ALL_LIST : this.delegate.resolveMediaTypes(webRequest);
}
}
static class ResourceChainResourceHandlerRegistrationCustomizer implements WebMvcAutoConfiguration.ResourceHandlerRegistrationCustomizer {
private final Resources resourceProperties;
ResourceChainResourceHandlerRegistrationCustomizer(Resources resourceProperties) {
this.resourceProperties = resourceProperties;
}
以上文中的SpringBoot自动配置为基础,可知SpringBoot会扫描我们引入的依赖中的META-INF目录下的spring.factories文件中以EnableAutoConfiguration为key的值,将其加入到IOC容器中,但是并不是所有的对象都会加入到IOC容器中,应为这些starter的主配置类中都会有@Conditional注解来指定约束条件:
- @ConditionalOnBean:当容器里有指定 Bean 的条件下
- @ConditionalOnMissingBean:当容器里没有指定 Bean 的情况下
- @ConditionalOnSingleCandidate:当指定 Bean 在容器中只有一个,或者虽然有多个但是指定首选 Bean
- @ConditionalOnClass:当类路径下有指定类的条件下
- @ConditionalOnMissingClass:当类路径下没有指定类的条件下
- @ConditionalOnProperty:指定的属性是否有指定的值
- @ConditionalOnResource:类路径是否有指定的值
- @ConditionalOnExpression:基于 SpEL 表达式作为判断条件
- @ConditionalOnJava:基于 Java 版本作为判断条件
- @ConditionalOnJndi:在 JNDI 存在的条件下差在指定的位置
- @ConditionalOnNotWebApplication:当前项目不是 Web 项目的条件下
- @ConditionalOnWebApplication:当前项目是 Web 项 目的条件下
4. starter整体结构
xxxAutoConfiguration:starter中的主配置类,其中定义了特定条件和默认配置文件的指定
xxxProperties:默认文件,可以在我们使用starter的时候进行配置覆盖,这里重点体现了springboot的约定大于配置。
总结
SpringBoot 使用@EnableAutoConfiguration开启自动配置功能,通过@Import注解加载实现了ImportSelector接口的类并最终加载META-INF/spring.factories中的自动配置类实现自动装配,自动配置类其实就是通过@Conditional按需加载的配置类,想要其生效必须引入spring-boot-starter-xxx包实现起步依赖。