64-SpringBoot源码分析

news/2024/11/18 11:11:37/文章来源:https://www.cnblogs.com/liujiaqi1101/p/18199242
  1. Starter 是什么? 我们如何使用这些 Starter?
  2. 为什么包扫描只会扫描核心启动类所在的包及其子包?
  3. 在 SpringBoot 启动过程中,是如何完成自动配置的?
  4. 内嵌 Tomcat 是如何创建并启动的?
  5. 引入了 web 场景对应的 Starter,SpringMVC 是如何完成自动装配的?

1. 源码环境构建

https://github.com/spring-projects/spring-boot/releases 下载对应版本的源码(本次采用 spring-boot-2.2.9.RELEASE)

(1)环境准备

  1. JDK1.8+
  2. Maven3.5+

(2)编译源码

  1. 进⼊ spring-boot 源码根⽬录
  2. 执⾏ mvn 命令:mvn clean install -DskipTests -Pfast // 跳过测试⽤例,会下载⼤量 jar 包(时间会长一些)

(3)导入 IDEA

  1. 将编译后的项目使用 IDEA 打开

  2. 打开 pom.xml 关闭 Maven 代码检查

    <properties><revision>2.2.9.RELEASE</revision><main.basedir>${basedir}</main.basedir><disable.checks>true</disable.checks>
    </properties>
    

(4)新建一个 module

(5)新建一个 Controller

@RestController
public class TestController {@RequestMapping("/test")public String test(){System.out.println("源码环境搭建完成");return "源码环境搭建完成";}}

2. 依赖管理

【Q1】为什么导入 dependency 时不需要指定版本?

在 Spring Boot 入门程序中,项目 pom.xml 文件有两个核心依赖,分别是 spring-boot-starter-parent 和 spring-boot-starter-web,关于这两个依赖的相关介绍具体如下。

在 spring-boot-mytest 项目中的 pom.xml 中找到 spring-boot-starter-parent 依赖,示例代码如下:

<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.2.9.RELEASE</version><relativePath/> <!-- lookup parent from repository -->
</parent>

上述代码中,将 spring-boot-starter-parent 依赖作为 Spring Boot 项目的统一父项目依赖管理,并将项目版本号统一为 2.2.9.RELEASE,该版本号根据实际开发需求是可以修改的。

进入 spring-boot-starter-parent 底层源文件,先看 spring-boot-starter-parent 做了哪些事。首先看 <properties> 节点。

<properties><main.basedir>${basedir}/../../..</main.basedir><java.version>1.8</java.version><resource.delimiter>@</resource.delimiter> <!-- delimiter that doesn't clash with Spring ${} placeholders --><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><maven.compiler.source>${java.version}</maven.compiler.source><maven.compiler.target>${java.version}</maven.compiler.target>
</properties>

在这里 spring-boot-starter-parent 定义了:

  1. 工程的 Java 版本为 1.8
  2. 工程代码的编译源文件编码格式为 UTF-8
  3. 工程编译后的文件编码格式为 UTF-8
  4. Maven 打包编译的版本

接下来看 POM 的 <build> 节点,分别定义了 <resources><pluginManagement><plugins>

首先来看下 <resources> 节点,里面定义了资源过滤。针对 application 的 yml、properties 格式进行过滤,可以支持不同环境的配置,比如 application-dev.yml、application-test.yml、application-dev.properties、application-dev.properties 等。

<resources><resource><directory>${basedir}/src/main/resources</directory><filtering>true</filtering><includes><include>**/application*.yml</include><include>**/application*.yaml</include><include>**/application*.properties</include></includes></resource><resource><directory>${basedir}/src/main/resources</directory><excludes><exclude>**/application*.yml</exclude><exclude>**/application*.yaml</exclude><exclude>**/application*.properties</exclude></excludes></resource>
</resources>

<pluginManagement> 则是引入了相应的插件和对应的版本依赖。

最后来看 spring-boot-starter-parent 的父依赖 spring-boot-dependencies。

我们看定义POM,这个才是SpringBoot项目的真正管理依赖的项目,其 properties 节点定义了 SpringBoot 相关的版本。

<properties><main.basedir>${basedir}/../..</main.basedir><!-- Dependency versions --><activemq.version>5.15.13</activemq.version><antlr2.version>2.7.7</antlr2.version><appengine-sdk.version>1.9.81</appengine-sdk.version><artemis.version>2.10.1</artemis.version><aspectj.version>1.9.6</aspectj.version><assertj.version>3.13.2</assertj.version><atomikos.version>4.0.6</atomikos.version><awaitility.version>4.0.3</awaitility.version><bitronix.version>2.1.4</bitronix.version><byte-buddy.version>1.10.13</byte-buddy.version><caffeine.version>2.8.5</caffeine.version><cassandra-driver.version>3.7.2</cassandra-driver.version><classmate.version>1.5.1</classmate.version>...
</properties>       

spring-boot-dependencies 的 <dependencyManagement> 节点里,<dependencies> 定义了 SpringBoot 版本的依赖的组件以及相应版本。

<dependencyManagement><dependencies><!-- Spring Boot --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot</artifactId><version>${revision}</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-test</artifactId><version>${revision}</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-test-autoconfigure</artifactId><version>${revision}</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-actuator</artifactId><version>${revision}</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-actuator-autoconfigure</artifactId><version>${revision}</version></dependency>...
</dependencyManagement>  

spring-boot-starter-parent 通过继承 spring-boot-dependencies 从而实现了 SpringBoot 的版本依赖管理,所以我们的 SpringBoot 工程继承 spring-boot-starter-parent 后已经具备版本锁定等配置了,这也就是在 Spring Boot 项目中部分依赖不需要写版本号的原因。

【Q2】spring-boot-starter-parent 父依赖启动器的主要作用是进行版本统一管理,那么项目运行依赖的 JAR 包是从何而来的?

查看 spring-boot-starter-web 依赖文件源码,核心代码具体如下:

<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-json</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-tomcat</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId><exclusions><exclusion><groupId>org.apache.tomcat.embed</groupId><artifactId>tomcat-embed-el</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-web</artifactId></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-webmvc</artifactId></dependency>
</dependencies>

从上述代码可以发现,spring-boot-starter-web 依赖启动器的主要作用是打包了 Web 开发场景所需的底层所有依赖(基于依赖传递,当前项目也存在对应的依赖 jar 包)。

正是如此,在 pom.xml 中引入 spring-boot-starter-web 依赖启动器时,就可以实现 Web 场景开发,而不需要额外导入 Tomcat 服务器以及其他 Web 依赖文件等。

当然,这些引入的依赖文件的版本号还是由 spring-boot-starter-parent 父依赖进行的统一管理。

Spring Boot 除了提供有上述介绍的 Web 依赖启动器外,还提供了其他许多开发场景的相关依赖,我们可以打开 Spring Boot 官方文档,搜索“Starters”关键字查询场景依赖启动器。

列出了 Spring Boot 官方提供的部分场景依赖启动器,这些依赖启动器适用于不同的场景开发,使用时只需要在 pom.xml 文件中导入对应的依赖启动器即可。

需要说明的是,Spring Boot 官方并不是针对所有场景开发的技术框架都提供了场景启动器,例如阿里巴巴的 Druid 数据源等,Spring Boot 官方就没有提供对应的依赖启动器。为了充分利用 Spring Boot 框架的优势,在 Spring Boot 官方没有整合这些技术框架的情况下,Druid 等技术框架所在的开发团队主动与 Spring Boot 框架进行了整合,实现了各自的依赖启动器,例如 druid-spring-boot-starter 等。我们在 pom.xml 文件中引入这些第三方的依赖启动器时,切记要配置对应的版本号。

3. 自动配置

自动配置:根据我们添加的 jar 包依赖,会自动将一些配置类的 bean 注册进 IoC 容器,我们可以需要的地方使用 @Autowired 或 @Resource 等注解来使用它。

Q:Spring Boot 到底是如何进行自动配置的,都把哪些组件进行了自动配置?

SpringBoot 应用的启动入口是 @SpringBootApplication 注解标注类中的 main() 方法。

SpringBoot 应用标注 @SpringBootApplication 在某个类上说明这个类是 SpringBoot 的主配置类,SpringBoot 就应该运行这个类的 main() 方法启动 SpringBoot 应用。

下面查看 @SpringBootApplication 内部源码进行分析 ,核心代码具体如下:

@Target({ElementType.TYPE})             // 注解的适用范围,Type表示注解可以描述在类、接口、注解或枚举中
@Retention(RetentionPolicy.RUNTIME)     // 表示注解的生命周期,Runtime运行时
@Documented                             // 表示注解可以记录在javadoc中
@Inherited                              // 表示可以被子类继承该注解
@SpringBootConfiguration                // * 标明该类为配置类
@EnableAutoConfiguration                // * 启动自动配置功能
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {// 根据class来排除特定的类,使其不能加入spring容器,传入参数value类型是class类型。@AliasFor(annotation = EnableAutoConfiguration.class)Class<?>[] exclude() default {};// 根据classname 来排除特定的类,使其不能加入spring容器,传入参数value类型是class的全类名字符串数组。@AliasFor(annotation = EnableAutoConfiguration.class)String[] excludeName() default {};// 指定扫描包,参数是包名的字符串数组。@AliasFor(annotation = ComponentScan.class, attribute = "basePackages")String[] scanBasePackages() default {};// 扫描特定的包,参数类似是Class类型数组。@AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")Class<?>[] scanBasePackageClasses() default {};}

从上述源码可以看出,@SpringBootApplication 注解是一个组合注解,前面 4 个是注解的元数据信息, 我们主要看 @SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan 这三个核心注解,关于这三个核心注解的相关说明具体如下。

3.1 @SpringBootConfiguration

@SpringBootConfiguration 是 SpringBoot 的配置类,标注在某个类上,表示这是一个 SpringBoot 的配置类。

查看 @SpringBootConfiguration 注解源码可以看出,该注解内部有一个核心注解 @Configuration,这是 Spring 框架提供的,表示当前类为一个配置类(XML 配置文件的注解表现形式),并可以被组件扫描器扫描。由此可见,@SpringBootConfiguration 注解的作用与 @Configuration 注解相同,都是标识一个可以被组件扫描器扫描的配置类,只不过 @SpringBootConfiguration 是被 Spring Boot 进行了重新封装命名而已。

@Configuration // 配置类的作用等同于配置文件,配置类也是容器中的一个对象
public @interface SpringBootConfiguration {}@Component
public @interface Configuration {}

3.2 @EnableAutoConfiguration

// 自动配置包
@AutoConfigurationPackage// Spring的底层注解@Import,给容器中导入一个组件:AutoConfigurationPackages.Registrar.class 
@Import(AutoConfigurationImportSelector.class)// 告诉SpringBoot开启自动配置功能,这样自动配置才能生效。
public @interface EnableAutoConfiguration {String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";// 返回不会被导入到 Spring 容器中的类Class<?>[] exclude() default {};// 返回不会被导入到 Spring 容器中的类名String[] excludeName() default {};}

Spring 中有很多以 Enable 开头的注解,其作用就是借助 @Import 来收集并注册特定场景相关的 Bean,并加载到 IoC 容器。@EnableAutoConfiguration 就是借助 @Import 来收集所有符合自动配置条件的 bean 定义,并加载到 IoC 容器。

在 @Import 注解的参数中可以填写类名,例如 @Import(Abc.class),根据 Abc 类的不同类型,Spring 容器有以下 4 种处理方式:

实现接口 处理方式
Abc 类实现了 ImportSelector 接口 Spring 容器就会实例化 Abc 类,并且调用其 selectImports 方法;
Abc 类实现了 DeferredImportSelector 接口 DeferredImportSelector 是 ImportSelector 的子类。Spring 容器会实例化 Abc 类,并且调用其 selectImports 方法,和 ImportSelector 的实例不同的是,DeferredImportSelector#selectImports 方法调用时机晚于 ImportSelector 的实例,要等到 @Configuration 注解中相关的业务全部都处理完了才会调用(具体逻辑在 ConfigurationClassParser.processDeferredImportSelectors 方法中);
Abc 类实现了ImportBeanDefinitionRegistrar接口 Spring 容器就会实例化 Abc 类,并且调用其 registerBeanDefinitions 方法;
Abc 没有实现上述任一接口 Spring 容器就会实例化 Abc 类。

a. @AutoConfigurationPackage

@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {}

@AutoConfigurationPackage 自动配置包,它也是一个组合注解,其中最重要的注解是 @Import(AutoConfigurationPackages.Registrar.class),它是 Spring 框架的底层注解,它的作用就是给容器中导入某个组件类,如 @Import(AutoConfigurationPackages.Registrar.class) 就是将 Registrar 这个组件类导入到容器中,可查看 Registrar 类中 registerBeanDefinitions 方法:

@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {// 将注解标注的元信息传入,获取到相应的包名register(registry, new PackageImport(metadata).getPackageName());
}

我们对 new PackageImport(metadata).getPackageName() 进行检索,会发现结果是:com.itheima。

再看 register 方法:

// 这里参数 packageNames 缺省情况下就是一个字符串,是使用了 @SpringBootApplication 的 SpringBoot 应用程序入口类所在的包
public static void register(BeanDefinitionRegistry registry, String... packageNames) {if (registry.containsBeanDefinition(BEAN)) {// 如果该bean已经注册,则将要注册包名称添加进去BeanDefinition beanDefinition = registry.getBeanDefinition(BEAN);ConstructorArgumentValues constructorArguments = beanDefinition.getConstructorArgumentValues();constructorArguments.addIndexedArgumentValue(0, ddBasePackages(constructorArguments, packageNames));}else {// => 如果该bean尚未注册,则注册该bean定义,参数中提供的包名称会被设置到bean定义的构造参数中GenericBeanDefinition beanDefinition = new GenericBeanDefinition();beanDefinition.setBeanClass(BasePackages.class);beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0, packageNames);beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);registry.registerBeanDefinition(BEAN, beanDefinition);}
}

AutoConfigurationPackages.Registrar 这个类就干一个事,注册一个 Bean 定义,这个 Bean 定义就是 org.springframework.boot.autoconfigure.AutoConfigurationPackages.BasePackages,它有一个参数,这个参数是使用了 @AutoConfigurationPackage 这个注解的类所在的包路径,保存自动配置类以供之后的使用,比如给 JPA entity 扫描器用来扫描开发人员通过注解 @Entity 定义的 entity 类。

b. @Import(AutoConfigurationImportSelector.class)

@Import({AutoConfigurationImportSelector.class}) 会将 AutoConfigurationImportSelector 这个类导入到 Spring 容器中,AutoConfigurationImportSelector 可以帮助 SpringBoot 应用将所有符合条件的 @Configuration 配置都加载到当前 SpringBoot 创建并使用的 IoC 容器(ApplicationContext)中。

可以看到 AutoConfigurationImportSelector 重点是实现了 DeferredImportSelector 接口和各种 Aware 接口,然后 DeferredImportSelector 接口又继承了 ImportSelector 接口(分别表示在某个时机会被回调)。

确定自动配置实现逻辑的入口方法:跟自动配置逻辑相关的入口方法在 DeferredImportSelectorGrouping 类的 getImports() 方法处,因此我们就从 DeferredImportSelectorGrouping 类的 getImports 方法来开始分析 SpringBoot 的自动配置源码好了。

先看一下 getImports() 方法代码:

/* ConfigurationClassParser.java */
public Iterable<Group.Entry> getImports() {// 遍历DeferredImportSelectorHolder对象集合deferredImports,deferredImports集合// 装了各种ImportSelector,当然这里装的是AutoConfigurationImportSelector。for (DeferredImportSelectorHolder deferredImport : this.deferredImports) {//【1】利用AutoConfigurationGroup的process方法来处理自动配置的相关逻辑//    决定导入哪些配置类(这个是我们分析的重点,自动配置的逻辑全在这了)this.group.process(deferredImport.getConfigurationClass().getMetadata(), deferredImport.getImportSelector());}//【2】经过上面的处理后,然后再进行选择导入哪些配置类return this.group.selectImports();
}
  • 标【1】处的的代码是我们分析的重中之重,自动配置的相关的绝大部分逻辑全在这里了。其主要做的事情就是在 this.group 即 AutoConfigurationGroup 对象的 process 方法中,传入的 AutoConfigurationImportSelector 对象来选择一些符合条件的自动配置类,过滤掉一些不符合条件的自动配置类,就是这么个事情。
  • AutoConfigurationGroup 是 AutoConfigurationImportSelector 的内部类,主要用来处理自动配置相关的逻辑,拥有 process() 和 selectImports() 方法,然后拥有 entries 和 autoConfigurationEntries 集合属性,这两个集合分别存储被处理后的符合条件的自动配置类,我们知道这些就足够了;
  • AutoConfigurationImportSelector 承担自动配置的绝大部分逻辑,负责选择一些符合条件的自动配置类
  • metadata 标注在SpringBoot启动类上的@SpringBootApplication注解元数据;
  • 标【2】的 this.group.selectImports 的方法主要是针对前面的 process 方法处理后的自动配置类再进一步有选择的选择导入,再进入到 AutoConfigurationImportSelector$AutoConfigurationGroup 的 process 方法。

再进入到 AutoConfigurationImportSelector$AutoConfigurationGroup 的 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()));//【1】调用getAutoConfigurationEntry方法得到自动配置类放入autoConfigurationEntry对象中AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector) deferredImportSelector).getAutoConfigurationEntry(getAutoConfigurationMetadata(), annotationMetadata);//【2】又将封装了自动配置类的autoConfigurationEntry对象装进autoConfigurationEntries集合this.autoConfigurationEntries.add(autoConfigurationEntry);//【3】遍历刚获取的自动配置类for (String importClassName : autoConfigurationEntry.getConfigurations()) {// 这里符合条件的自动配置类作为key,annotationMetadata作为值放进entries集合this.entries.putIfAbsent(importClassName, annotationMetadata);}
}

上面代码中我们再来看标【1】的方法 getAutoConfigurationEntry,这个方法主要是用来获取自动配置类有关,承担了自动配置的主要逻辑。

/* AutoConfigurationImportSelector.java */// 获取符合条件的自动配置类,避免加载不必要的自动配置类从而造成内存浪费
protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,AnnotationMetadata annotationMetadata) {// 获取是否有配置spring.boot.enableautoconfiguration属性,默认返回trueif (!isEnabled(annotationMetadata)) {return EMPTY_ENTRY;}// 获得@Congiguration标注的Configuration类即被审视introspectedClass的注解数据,// 比如:@SpringBootApplication(exclude = FreeMarkerAutoConfiguration.class)// 将会获取到exclude = FreeMarkerAutoConfiguration.class和excludeName=""的注解数据AnnotationAttributes attributes = getAttributes(annotationMetadata);// 【1】得到spring.factories文件配置的所有自动配置类List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);// 利用LinkedHashSet移除重复的配置类configurations = removeDuplicates(configurations);// 得到要排除的自动配置类,比如注解属性exclude的配置类// 比如:@SpringBootApplication(exclude = FreeMarkerAutoConfiguration.class)// 将会获取到exclude = FreeMarkerAutoConfiguration.class的注解数据Set<String> exclusions = getExclusions(annotationMetadata, attributes);// 检查要被排除的配置类,因为有些不是自动配置类,故要抛出异常checkExcludedClasses(configurations, exclusions);// 【2】将要排除的配置类移除configurations.removeAll(exclusions);// 【3】因为从spring.factories文件获取的自动配置类太多,如果有些不必要的自动配置类都加载进内存,会造成内存浪费//  因此这里需要进行过滤。注意这里会调用AutoConfigurationImportFilter的match方法来判断是否符合//  @ConditionalOnBean、@ConditionalOnClass或@ConditionalOnWebApplication,后面会重点分析一下configurations = filter(configurations, autoConfigurationMetadata);// 【4】获取了符合条件的自动配置类后,此时触发AutoConfigurationImportEvent事件,// 目的是告诉ConditionEvaluationReport条件评估报告器对象来记录符合条件的自动配置类// 该事件什么时候会被触发?=> 在刷新容器时调用invokeBeanFactoryPostProcessors后置处理器时触发fireAutoConfigurationImportEvents(configurations, exclusions);// 【5】将符合条件和要排除的自动配置类封装进AutoConfigurationEntry对象,并返回return new AutoConfigurationEntry(configurations, exclusions); 
}

进入【1】getCandidateConfigurations 方法,该方法中有一个重要方法 loadFactoryNames(),这个方法是让 SpringFactoryLoader 去加载一些组件的名字。

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {// 这个方法需要传入两个参数:// 1. getSpringFactoriesLoaderFactoryClass()这个方法返回的是EnableAutoConfiguration.class// 2. getBeanClassLoader()这个方法返回的是beanClassLoader(类加载器)List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),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;
}

继续点开 loadFactoryNames() 方法:

	public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {String factoryTypeName = factoryType.getName();return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());}private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {MultiValueMap<String, String> result = cache.get(classLoader);if (result != null) {return result;}try {// 如果类加载器不为null,则加载类路径下spring.factories文件,将其中设置的配置类的全路径信息封装为Enumeration类对象。Enumeration<URL> urls = (classLoader != null ?classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));result = new LinkedMultiValueMap<>();while (urls.hasMoreElements()) {URL url = urls.nextElement();UrlResource resource = new UrlResource(url);Properties properties = PropertiesLoaderUtils.loadProperties(resource);for (Map.Entry<?, ?> entry : properties.entrySet()) {String factoryTypeName = ((String) entry.getKey()).trim();for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {result.add(factoryTypeName, factoryImplementationName.trim());}}}cache.put(classLoader, result);return result;}catch (IOException ex) {throw new IllegalArgumentException("Unable to load factories from location [" + FACTORIES_RESOURCE_LOCATION + "]", ex);}}

从代码中我们可以知道,在这个方法中会遍历整个 ClassLoader 中所有 jar 包下的 spring.factories 文件。spring.factories 里面保存着 SpringBoot 的默认提供的自动配置类。

getAutoConfigurationEntry() 方法主要做的事情就是获取符合条件的自动配置类,避免加载不必要的自动配置类从而造成内存浪费。我们下面总结下getAutoConfigurationEntry() 方法主要做的事情:

  1. 从 spring.factories 配置文件中加载 EnableAutoConfiguration 自动配置类,获取的自动配置类如上图所示。
  2. 若 @EnableAutoConfiguration 等注解标有要 exclude 的自动配置类,那么再将这个自动配置类排除掉;
  3. 排除掉要 exclude 的自动配置类后,然后再调用 filter 方法进行进一步的过滤,再次排除一些不符合条件的自动配置类;
  4. 经过重重过滤后,此时再触发 AutoConfigurationImportEvent 事件,告诉 ConditionEvaluationReport 条件评估报告器对象来记录符合条件的自动配置类;
  5. 最后再将符合条件的自动配置类返回。

总结了 getAutoConfigurationEntry 方法主要的逻辑后,我们再来细看一下 AutoConfigurationImportSelector 的 filter 方法:

/* AutoConfigurationImportSelector.java */
private List<String> filter(List<String> configurations, AutoConfigurationMetadata autoConfigurationMetadata) {long startTime = System.nanoTime();// 将从spring.factories中获取的自动配置类转出字符串数组String[] candidates = StringUtils.toStringArray(configurations);// 定义skip数组,是否需要跳过。注意skip数组与candidates数组顺序一一对应boolean[] skip = new boolean[candidates.length];boolean skipped = false;// getAutoConfigurationImportFilters方法:拿到OnBeanCondition,OnClassCondition和OnWebApplicationCondition// 然后遍历这三个条件类去过滤从spring.factories加载的大量配置类for (AutoConfigurationImportFilter filter : getAutoConfigurationImportFilters()) {// 调用各种aware方法,将beanClassLoader,beanFactory等注入到filter对象中,// 这里的filter对象即OnBeanCondition,OnClassCondition或OnWebApplicationConditioninvokeAwareMethods(filter);// 判断各种filter来判断每个candidate(这里实质要通过candidate(自动配置类)拿到其标注的// @ConditionalOnClass,@ConditionalOnBean和@ConditionalOnWebApplication里面的注解值)是否匹配,// 注意candidates数组与match数组一一对应/**********************【主线,重点关注】********************************/boolean[] match = filter.match(candidates, autoConfigurationMetadata);// 遍历match数组,注意match顺序跟candidates的自动配置类一一对应for (int i = 0; i < match.length; i++) {// 若有不匹配的话if (!match[i]) {// 不匹配的将记录在skip数组,标志skip[i]为true,也与candidates数组一一对应skip[i] = true;// 因为不匹配,将相应的自动配置类置空candidates[i] = null;// 标注skipped为trueskipped = true; }}} // 这里表示若所有自动配置类经过OnBeanCondition,OnClassCondition和OnWebApplicationCondition过滤后,全部都匹配的话,则全部原样返回if (!skipped) {return configurations;}// 建立result集合来装匹配的自动配置类List<String> result = new ArrayList<>(candidates.length); for (int i = 0; i < candidates.length; i++) {// 若skip[i]为false,则说明是符合条件的自动配置类,此时添加到result集合中if (!skip[i]) { result.add(candidates[i]);}}// 打印日志if (logger.isTraceEnabled()) {int numberFiltered = configurations.size() - result.size();logger.trace("Filtered " + numberFiltered + " auto configuration class in "+ TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)+ " ms");}// 最后返回符合条件的自动配置类return new ArrayList<>(result);
}

AutoConfigurationImportSelector#filter() 方法主要做的事情就是调用 AutoConfigurationImportFilter 接口的 match 方法来判断每一个自动配置类上的条件注解(若有的话)@ConditionalOnClass、@ConditionalOnBean 或 @ConditionalOnWebApplication 是否满足条件,若满足则返回 true 说明匹配,若不满足则返回 false 说明不匹配。

我们现在知道 AutoConfigurationImportSelector#filter 方法主要做了什么事情就行了,现在先不用研究的过深。

@Conditional 是 Spring4 新提供的注解,它的作用是按照一定的条件进行判断,满足条件给容器注册 bean。

  • @ConditionalOnBean:仅仅在当前上下文中存在某个对象时,才会实例化一个Bean。
  • @ConditionalOnClass:某个class位于类路径上,才会实例化一个Bean。
  • @ConditionalOnExpression:当表达式为true的时候,才会实例化一个Bean。基于SpEL表达式的条件判断。
  • @ConditionalOnMissingBean:仅仅在当前上下文中不存在某个对象时,才会实例化一个Bean。
  • @ConditionalOnMissingClass:某个class类路径上不存在的时候,才会实例化一个Bean。
  • @ConditionalOnNotWebApplication:不是web应用,才会实例化一个Bean。
  • @ConditionalOnWebApplication:当项目是一个Web项目时进行实例化。
  • @ConditionalOnNotWebApplication:当项目不是一个Web项目时进行实例化。
  • @ConditionalOnProperty:当指定的属性有指定的值时进行实例化。
  • @ConditionalOnJava:当JVM版本为指定的版本范围时触发实例化。
  • @ConditionalOnResource:当类路径下有指定的资源时触发实例化。
  • @ConditionalOnJndi:在JNDI存在的条件下触发实例化。
  • @ConditionalOnSingleCandidate:当指定的Bean在容器中只有一个,或者有多个但是指定了首选的Bean时触发实例化。

有选择的导入自动配置类

this.group.selectImports() 方法是如何进一步有选择的导入自动配置类的。

/* AutoConfigurationImportSelector$AutoConfigurationGroup.java */public Iterable<Entry> selectImports() {if (this.autoConfigurationEntries.isEmpty()) {return Collections.emptyList();} // 这里得到所有要排除的自动配置类的set集合Set<String> allExclusions = this.autoConfigurationEntries.stream().map(AutoConfigurationEntry::getExclusions).flatMap(Collection::stream).collect(Collectors.toSet());// 这里得到经过滤后所有符合条件的自动配置类的set集合Set<String> processedConfigurations = this.autoConfigurationEntries.stream() .map(AutoConfigurationEntry::getConfigurations).flatMap(Collection::stream).collect(Collectors.toCollection(LinkedHashSet::new));// 移除掉要排除的自动配置类processedConfigurations.removeAll(allExclusions); // 对标注有@Order注解的自动配置类进行排序,return sortAutoConfigurations(processedConfigurations,getAutoConfigurationMetadata()).stream().map((importClassName) -> new Entry(this.entries.get(importClassName), importClassName)).collect(Collectors.toList());
}

可以看到,selectImports() 方法主要是针对经过排除掉 exclude 的和被 AutoConfigurationImportFilter 接口过滤后的满足条件的自动配置类再进一步排除 exclude 的自动配置类,然后再排序。

最后,我们再总结下 SpringBoot 自动配置的原理,主要做了以下事情:

  1. 从 spring.factories 配置文件中加载自动配置类;
  2. 加载的自动配置类中排除掉 @EnableAutoConfiguration 注解的 exclude 属性指定的自动配置类;
  3. 然后再用 AutoConfigurationImportFilter 接口去过滤自动配置类是否符合其标注注解(若有标注的话)@ConditionalOnClass、@ConditionalOnBean 和@ConditionalOnWebApplication 的条件,若都符合的话则返回匹配结果;
  4. 然后触发 AutoConfigurationImportEvent 事件,告诉 ConditionEvaluationReport 条件评估报告器对象来分别记录符合条件和 exclude 的自动配置类。
  5. 最后 Spring 再将最后筛选后的自动配置类导入 IoC 容器中。

以 HttpEncodingAutoConfiguration(Http 编码自动配置)为例解释自动配置原理:

// 表示这是一个配置类,和以前编写的配置文件一样,也可以给容器中添加组件
@Configuration// 启动指定类的ConfigurationProperties功能;将配置文件中对应的值和HttpEncodingProperties绑定起来;
@EnableConfigurationProperties({HttpEncodingProperties.class}) // Spring底层@Conditional注解,根据不同的条件,如果满足指定的条件,整个配置类里面的配置就会生效。
// 判断当前应用是否是web应用,如果是,当前配置类生效。并把HttpEncodingProperties加入到 ioc 容器中
@ConditionalOnWebApplication// 判断当前项目有没有这个CharacterEncodingFilter : SpringMVC中进行乱码解决的过滤器
@ConditionalOnClass({CharacterEncodingFilter.class})// 判断配置文件中是否存在某个配置 spring.http.encoding.enabled 如果不存在,判断也是成立的
// matchIfMissing = true 表示即使我们配置文件中不配置spring.http.encoding.enabled=true,也是默认生效的
@ConditionalOnProperty(prefix = "spring.http.encoding",value = {"enabled"},matchIfMissing = true
)
public class HttpEncodingAutoConfiguration {// 它已经和SpringBoot配置文件中的值进行映射了private final HttpEncodingProperties properties;// 只有一个有参构造器的情况下,参数的值就会从容器中拿public HttpEncodingAutoConfiguration(HttpEncodingProperties properties) {this.properties = properties;}@Bean // 给容器中添加一个组件,这个组件中的某些值需要从properties中获取@ConditionalOnMissingBean({CharacterEncodingFilter.class})  // 判断容器中没有这个组件public CharacterEncodingFilter characterEncodingFilter() {CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();filter.setEncoding(this.properties.getCharset().name());filter.setForceRequestEncoding(this.properties.shouldForce(Type.REQUEST));filter.setForceResponseEncoding(this.properties.shouldForce(Type.RESPONSE));return filter;}

根据当前不同的条件判断,决定这个配置类是否生效。

一旦这个配置类生效,这个配置类就会给容器中添加各种组件;这些组件的属性是从对应的properties类中获取的,这些类里面的每一个属性又是和配置文件绑定的。

# 我们能配置的属性都是来源于这个功能的properties类
spring.http.encoding.enabled=true
spring.http.encoding.charset=utf-8
spring.http.encoding.force=true

所有在配置文件中能配置的属性都是在 xxxProperties 类中封装着,配置文件能配置什么就可以参照某个功能对应的这个属性类。

// 从配置文件中获取指定的值和bean的属性进行绑定
@ConfigurationProperties(prefix = "spring.http.encoding")
public class HttpEncodingProperties {public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");

小结:

  1. SpringBoot 启动会加载大量的自动配置类
  2. 我们看我们需要实现的功能有没有 SpringBoot 默认写好的自动配置类
  3. 我们再来看这个自动配置类中到底配置了哪些组件(只要我们有我们要用的组件,我们就不需要再来配置了)
  4. 给容器中自动配置类添加组件的时候,会从 properties 类中获取某些属性,我们就可以在配置文件中指定这些属性的值。
    • xxxAutoConfiguration:自动配置类,用于给容器中添加组件从而代替之前我们手动完成大量繁琐的配置。
    • xxxProperties:封装了对应自动配置类的默认属性值,如果我们需要自定义属性值,只需要根据 xxxProperties 寻找相关属性在配置文件设值即可。

3.3 @ComponentScan

主要是从定义的扫描路径中,找出标识了需要装配的类自动装配到 Spring 的 bean 容器中。

常用属性如下:

  • basePackages、value:指定扫描路径,如果为空则以@ComponentScan注解的类所在的包为基本的扫描路径
  • basePackageClasses:指定具体扫描的类
  • includeFilters:指定满足 Filter 条件的类
  • excludeFilters:指定排除 Filter 条件的类

includeFilters 和 excludeFilters 的 FilterType 可选:

  • ANNOTATION(注解类型,默认)
  • ASSIGNABLE_TYPE(指定固定类)
  • ASPECTJ(ASPECTJ 类型)
  • REGEX(正则表达式)
  • CUSTOM(自定义类型,自定义的 Filter 需要实现 TypeFilter 接口)

@ComponentScan 的配置如下:

@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })

借助 excludeFilters 将 TypeExcludeFillter 及 FilterType 这两个类进行排除。

当前 @ComponentScan 注解没有标注 basePackages 及 value,所以扫描路径默认为 @ComponentScan 注解的类所在的包为基本的扫描路径(也就是标注了 @SpringBootApplication 注解的项目启动类所在的路径)。

@EnableAutoConfiguration 注解是通过 @Import 注解加载了自动配置固定的 bean,@ComponentScan 注解自动进行注解扫描。那么真正根据包扫描,把组件类生成实例对象存到 IoC 容器中,又是怎么来完成的?

4. main-run() 执行流程

@SpringBootApplication
public class SpringBootMytestApplication {public static void main(String[] args) {SpringApplication.run(SpringBootMytestApplication.class, args);}
}

点进 run 方法:

public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {// 调用重载方法return run(new Class<?>[] { primarySource }, args);
}public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {// 两件事:1.初始化SpringApplication 2.执行run方法return new SpringApplication(primarySources).run(args);
}

4.1 new SpringApplication

SpringApplication 实例化过程,首先是进入带参数的构造方法,最终回来到两个参数的构造方法。

public SpringApplication(Class<?>... primarySources) {this(null, primarySources);
}@SuppressWarnings({"unchecked", "rawtypes"})
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {// 设置资源加载器为nullthis.resourceLoader = resourceLoader;// 断言加载资源类不能为nullAssert.notNull(primarySources, "PrimarySources must not be null");// 将primarySources数组转换为List,最后放到LinkedHashSet集合中this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));//【1】推断应用类型,后面会根据类型初始化对应的环境。常用的一般都是servlet环境this.webApplicationType = WebApplicationType.deduceFromClasspath();//【2】初始化classpath下 META-INF/spring.factories中已配置的ApplicationContextInitializersetInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));//【3】初始化classpath下所有已配置的 ApplicationListenersetListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));//【4】根据调用栈,推断出 main 方法的类名this.mainApplicationClass = deduceMainApplicationClass();
}

【1】推断应用类型

/*** 判断应用的类型* NONE: 应用程序不是web应用,也不应该用web服务器去启动。* SERVLET: 应用程序应作为基于servlet的web应用程序运行,并应启动嵌入式 Servlet Web(tomcat)服务器。* REACTIVE: 应用程序应作为 reactive web应用程序运行,并应启动嵌入式 reactive web服务器。*/
static WebApplicationType deduceFromClasspath() {// classpath下必须存在org.springframework.web.reactive.DispatcherHandlerif (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)&& !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {return WebApplicationType.REACTIVE;}for (String className : SERVLET_INDICATOR_CLASSES) {// classpath环境下不存在javax.servlet.Servlet或者org.springframework.web.context.ConfigurableWebApplicationContextif (!ClassUtils.isPresent(className, null)) {return WebApplicationType.NONE;}}return WebApplicationType.SERVLET;
}

【2】初始化 classpath 下 META-INF/spring.factories 中已配置的 ApplicationContextInitializer

setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));

进入 getSpringFactoriesInstances:

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {return getSpringFactoriesInstances(type, new Class<?>[] {});
}/*** 通过指定的 classloader 从 META-INF/spring.factories 获取指定的 Spring 的工厂实例*/
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {ClassLoader classLoader = getClassLoader();// => 通过指定的classLoader从 META-INF/spring.factories 的资源文件中,读取 key 为 type.getName() 的 valueSet<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));// 创建工厂实例List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);// 对 Spring 工厂实例排序(org.springframework.core.annotation.Order注解指定的顺序)AnnotationAwareOrderComparator.sort(instances);return instances;
}

loadFactoryNames() 这个方法很重要,这个方法是 spring-core 中提供的从 META-INF/spring.factories 中获取指定的类(key)的统一入口方法。

在这里,获取的是 key 为 org.springframework.context.ApplicationContextInitializer 的类。

ApplicationContextInitializer 是 Spring 框架的类,这个类的主要目的就是在 ConfigurableApplicationContext 调用 refresh() 方法之前,回调这个类的 initialize 方法。

通过 ConfigurableApplicationContext 的实例获取容器的环境 Environment,从而实现对配置文件的修改完善等工作。

【3】初始化 classpath下 META-INF/spring.factories 中已配置的 ApplicationListener

ApplicationListener 的加载过程和上面的 ApplicationContextInitializer 类的加载过程是一样的。不多说了,至于 ApplicationListener 是 Spring 的事件监听器,典型的观察者模式,通过 ApplicationEvent 类和 ApplicationListener 接口,可以实现对 Spring 容器全生命周期的监听,当然也可以自定义监听事件。


关于 SpringApplication 类的构造过程,到这里我们就梳理完了。纵观 SpringApplication 类的实例化过程,我们可以看到,合理的利用该类就能在 Spring 容器创建之前做一些预备工作,和定制化的需求。

比如,自定义 SpringBoot 的 Banner,比如自定义事件监听器,再比如在容器 refresh 之前通过自定义 ApplicationContextInitializer 修改配置一些配置或者获取指定的 bean 都是可以的。

4.2 run(args)

上一小节我们查看了SpringApplication 类的实例化过程,这一小节总结 SpringBoot 启动流程最重要的部分即 run 方法。通过 run 方法梳理出 SpringBoot 启动的流程。

经过深入分析后,大家会发现 SpringBoot 也就是给 Spring 包了一层皮,事先替我们准备好 Spring 所需要的环境及一些基础。

/*** Run the Spring application, creating and refreshing a new ApplicationContext.** 运行Spring应用,并刷新一个新的 ApplicationContext(Spring的上下文)* ConfigurableApplicationContext 是 ApplicationContext 接口的子接口。在 ApplicationContext 基础上* 增加了配置上下文的工具。ConfigurableApplicationContext 是容器的高级接口。*/
public ConfigurableApplicationContext run(String... args) {// 记录程序运行时间StopWatch stopWatch = new StopWatch();stopWatch.start();// ConfigurableApplicationContext Spring 的上下文ConfigurableApplicationContext context = null;Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();// java.awt.headless是J2SE的一种模式用于在缺少显示屏、键盘或者鼠标时的系统配置,很多监控工具如jconsole需要将该值设置为true,系统变量默认为trueconfigureHeadlessProperty();//【1】获取并启动监听器SpringApplicationRunListeners listeners = getRunListeners(args);listeners.starting();try {ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);//【2】准备环境ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);// 处理需要忽略的BeanconfigureIgnoreBeanInfo(environment);// 打印bannerBanner printedBanner = printBanner(environment);//【3】初始化应用上下文context = createApplicationContext();// 实例化SpringBootExceptionReporter.class,用来支持报告关于启动的错误exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,new Class[] { ConfigurableApplicationContext.class }, context);//【4】刷新应用上下文前的准备阶段prepareContext(context, environment, listeners, applicationArguments, printedBanner);//【5】刷新应用上下文refreshContext(context);//【6】刷新应用上下文后的扩展接口afterRefresh(context, applicationArguments);// 时间记录停止stopWatch.stop();if (this.logStartupInfo) {new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);}// 发布容器启动完成事件listeners.started(context);callRunners(context, applicationArguments);}catch (Throwable ex) {handleRunFailure(context, ex, exceptionReporters, listeners);throw new IllegalStateException(ex);}try {listeners.running(context);}catch (Throwable ex) {handleRunFailure(context, ex, exceptionReporters, null);throw new IllegalStateException(ex);}return context;
}

在以上的代码中,启动过程中的重要步骤共分为 6 步:

  1. 获取并启动监听器
  2. 构造应用上下文环境
  3. 初始化应用上下文
  4. 刷新应用上下文前的准备阶段
  5. 刷新应用上下文
  6. 刷新应用上下文后的扩展接口

a. 获取并启动监听器

事件机制在 Spring 是很重要的一部分内容,通过事件机制我们可以监听 Spring 容器中正在发生的一些事件,同样也可以自定义监听事件。Spring 的事件为 Bean 和 Bean 之间的消息传递提供支持。当一个对象处理完某种任务后,通知另外的对象进行某些处理,常用的场景有进行某些操作后发送通知,消息、邮件等情况。

private SpringApplicationRunListeners getRunListeners(String[] args) {Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };// SpringApplicationRunListeners负责在SpringBoot启动的不同阶段,// 广播出不同的消息, 传递给ApplicationListener监听器实现类。return new SpringApplicationRunListeners(logger,getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args));
}

在这里面看到一个熟悉的方法:getSpringFactoriesInstances(),可以看下面的注释,前面的小节我们已经详细介绍过该方法是怎么一步步的获取到 META-INF/spring.factories 中的指定的 key 的 value,获取到以后怎么实例化类的。

回到 run 方法,debug 这个代码 SpringApplicationRunListeners listeners = getRunListeners(args); 看一下获取的是哪个监听器:

EventPublishingRunListener 监听器是 Spring 容器的启动监听器。

b. 构造应用上下文环境

应用上下文环境包括什么呢?包括计算机的环境、Java 环境、Spring 的运行环境、Spring 项目的配置(在 SpringBoot 中就是那个熟悉的 application.properties/yml)等等。

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments) {// => 1. 创建并配置相应的环境ConfigurableEnvironment environment = getOrCreateEnvironment();// => 2. 根据用户配置,配置 environment 系统环境configureEnvironment(environment, applicationArguments.getSourceArgs());// => 3. 启动相应的监听器,其中一个重要的监听器 ConfigFileApplicationListener 就是加载项目配置文件的监听器!listeners.environmentPrepared(environment);bindToSpringApplication(environment);if (this.webApplicationType == WebApplicationType.NONE) {environment = new EnvironmentConverter(getClassLoader()).convertToStandardEnvironmentIfNecessary(environment);}ConfigurationPropertySources.attach(environment);return environment;
}

(1)getOrCreateEnvironment()

private ConfigurableEnvironment getOrCreateEnvironment() {if (this.environment != null) {return this.environment;}// 如果应用类型是 SERVLET 则实例化 StandardServletEnvironmentif (this.webApplicationType == WebApplicationType.SERVLET) {return new StandardServletEnvironment();}return new StandardEnvironment();
}

StandardServletEnvironment 是 StandardEnvironment的子类。这两个对象也没什么好讲的,当是 web 项目的时候,环境上会多一些关于 web 环境的配置。

(2)configureEnvironment()

protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) {// 将 main 函数的 args 封装成 SimpleCommandLinePropertySource 加入环境中configurePropertySources(environment, args);// 激活相应的配置文件configureProfiles(environment, args);
}
  • 在 configurePropertySources(environment, args) 中将 args 封装成了 SimpleCommandLinePropertySource 并加入到了 environment 中。
  • configureProfiles(environment, args) 根据启动参数激活了相应的配置文件。

在执行完方法中的两行代码后,debug 的截图如下:

如下图所示,在启动参数中指定了参数:--spring.profiles.active=prod

(3)listeners.environmentPrepared(environment)

进入到方法一路跟下去就到了 SimpleApplicationEventMulticaster 类的 multicastEvent() 方法。

/* SpringApplicationRunListeners.java */
void environmentPrepared(ConfigurableEnvironment environment) {for (SpringApplicationRunListener listener : this.listeners) {// === Step Into ===listener.environmentPrepared(environment);}
}/* EventPublishingRunListener.java */
@Override
public void environmentPrepared(ConfigurableEnvironment environment) {this.initialMulticaster.multicastEvent(new ApplicationEnvironmentPreparedEvent(this.application, this.args, environment));
}

查看 getApplicationListeners(event, type) 执行结果,发现一个重要的监听器 ConfigFileApplicationListener。

/*** EnvironmentPostProcessor that configures the context environment by loading* properties from well known file locations. By default properties will be loaded from* 'application.properties' and/or 'application.yml' files in the following locations:* - file:./config/* - file:./* - classpath:config/* - classpath:* </ul>* The list is ordered by precedence (properties defined in locations higher in the list* override those defined in lower locations).*/

这个监听器默认的从注释中标签所示的几个位置加载配置文件,并将其加入上下文的 environment 变量中。当然也可以通过配置指定。

c. 初始化应用上下文

在 SpringBoot 工程中,应用类型分为 3 种:

public enum WebApplicationType {/*** 应用程序不是web应用,也不应该用web服务器去启动*/NONE,/*** 应用程序应作为基于servlet的web应用程序运行,并应启动嵌入式servlet web(tomcat)服务器。*/SERVLET,/*** 应用程序应作为 reactive web应用程序运行,并应启动嵌入式 reactive web服务器。*/REACTIVE
}

对应三种应用类型,SpringBoot 项目有三种对应的应用上下文,我们以 web 工程为例,即其上下文为 AnnotationConfigServletWebServerApplicationContext。

public static final String DEFAULT_WEB_CONTEXT_CLASS ="org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext";
public static final String DEFAULT_REACTIVE_WEB_CONTEXT_CLASS ="org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext";
public static final String DEFAULT_CONTEXT_CLASS ="org.springframework.context.annotation.AnnotationConfigApplicationContext";protected ConfigurableApplicationContext createApplicationContext() {Class<?> contextClass = this.applicationContextClass;if (contextClass == null) {try {switch (this.webApplicationType) {case SERVLET:contextClass = Class.forName(DEFAULT_WEB_CONTEXT_CLASS);break;case REACTIVE:contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);break;default:contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);}} catch (ClassNotFoundException ex) {throw new IllegalStateException("Unable create a default ApplicationContext, please specify an ApplicationContextClass", ex);}}return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
}

应用上下文可以理解成 IoC 容器的高级表现形式,应用上下文确实是在 IoC 容器的基础上丰富了一些高级功能。应用上下文对 IoC 容器是持有的关系。他的一个属性 beanFactory 就是 IoC 容器(DefaultListableBeanFactory)。所以他们之间是持有和扩展的关系。

接下来看 GenericApplicationContext 类:

public class GenericApplicationContext extends AbstractApplicationContext implements BeanDefinitionRegistry {private final DefaultListableBeanFactory beanFactory;...public GenericApplicationContext() {this.beanFactory = new DefaultListableBeanFactory();}...
}

beanFactory 正是在 AnnotationConfigServletWebServerApplicationContext 实现的接口 GenericApplicationContext 中定义的。在上面 createApplicationContext() 方法中的,BeanUtils.instantiateClass(contextClass) 这个方法中,不但初始化了 AnnotationConfigServletWebServerApplicationContext 类,也就是我们的上下文 Context,同样也触发了 GenericApplicationContext 类的构造函数,从而 beanFactory(IoC 容器)也创建了。

仔细看它的构造函数,有没有发现一个很熟悉的类 DefaultListableBeanFactory,没错,DefaultListableBeanFactory 就是 IoC 容器真实面目了。在后面的 refresh() 方法分析中,DefaultListableBeanFactory 是无处不在的存在感。

如上图所示,context 就是我们熟悉的上下文(也有人称之为容器,都可以,看个人爱好和理解),beanFactory 就是我们所说的 IoC 容器的真实面孔了。细细感受下上下文和容器的联系和区别,对于我们理解源码有很大的帮助。在我们学习过程中,我们也是将上下文和容器严格区分开来的。

d. 刷新应用上下文前的准备阶段

前面我们介绍了 SpringBoot 启动流程 run() 方法的前三步,接下来再来介绍第 4 步:刷新应用上下文前的准备阶段,也就是 prepareContext() 方法。

private void prepareContext(ConfigurableApplicationContext context,ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,ApplicationArguments applicationArguments, Banner printedBanner) {// 设置容器环境context.setEnvironment(environment);// 执行容器后置处理postProcessApplicationContext(context);// 执行容器中的 ApplicationContextInitializer 包括spring.factories和通过三种方式自定义的applyInitializers(context);// 向各个监听器发送容器已经准备好的事件listeners.contextPrepared(context);if (this.logStartupInfo) {logStartupInfo(context.getParent() == null);logStartupProfileInfo(context);}// 将main函数中的args参数封装成单例Bean,注册进容器context.getBeanFactory().registerSingleton("springApplicationArguments", applicationArguments);// 将 printedBanner 也封装成单例,注册进容器if (printedBanner != null) {context.getBeanFactory().registerSingleton("springBootBanner", printedBanner);}// => Load 核心启动类!Set<Object> sources = getAllSources();Assert.notEmpty(sources, "Sources must not be empty");// 加载我们的启动类,将启动类注入容器load(context, sources.toArray(new Object[0]));// 发布容器已加载事件listeners.contextLoaded(context);
}

首先在 getAllSources() 中拿到了我们的启动类。我们重点讲解这行 load(context, sources.toArray(new Object[0])); 跟进 load() 方法,看源码。

protected void load(ApplicationContext context, Object[] sources) {if (logger.isDebugEnabled()) {logger.debug("Loading source " + StringUtils.arrayToCommaDelimitedString(sources));}// => 1-2、创建 BeanDefinitionLoaderBeanDefinitionLoader loader = createBeanDefinitionLoader(getBeanDefinitionRegistry(context), sources);if (this.beanNameGenerator != null) {loader.setBeanNameGenerator(this.beanNameGenerator);}if (this.resourceLoader != null) {loader.setResourceLoader(this.resourceLoader);}if (this.environment != null) {loader.setEnvironment(this.environment);}// => 3、加载loader.load();
}

(1)先看 getBeanDefinitionRegistry() 方法的源码:

private BeanDefinitionRegistry getBeanDefinitionRegistry(ApplicationContext context) {if (context instanceof BeanDefinitionRegistry) {return (BeanDefinitionRegistry) context;}...
}

这里将我们前文创建的上下文强转为 BeanDefinitionRegistry,他们之间是有继承关系的。BeanDefinitionRegistry 定义了很重要的方法 registerBeanDefinition(),该方法将 BeanDefinition 注册进 DefaultListableBeanFactory 容器的 beanDefinitionMap 中。

(2)再看外层 createBeanDefinitionLoader() 方法,最终进入了 BeanDefinitionLoader 类的构造方法,如下所示:

BeanDefinitionLoader(BeanDefinitionRegistry registry, Object... sources) {Assert.notNull(registry, "Registry must not be null");Assert.notEmpty(sources, "Sources must not be empty");this.sources = sources;// 注解形式的Bean定义读取器 比如:@Configuration @Bean @Component @Controller @Service等等this.annotatedReader = new AnnotatedBeanDefinitionReader(registry);// XML形式的Bean定义读取器this.xmlReader = new XmlBeanDefinitionReader(registry);if (isGroovyPresent()) {this.groovyReader = new GroovyBeanDefinitionReader(registry);}// 类路径扫描器this.scanner = new ClassPathBeanDefinitionScanner(registry);// 扫描器添加排除过滤器this.scanner.addExcludeFilter(new ClassExcludeFilter(sources));
}

先记住上面的三个属性,上面三个属性在 BeanDefinition 的 Resource定位和 BeanDefinition 的注册中起到了很重要的作用。

(3)跟进 load() 方法

private int load(Object source) {Assert.notNull(source, "Source must not be null");// => 从Class加载if (source instanceof Class<?>) {return load((Class<?>) source);}// 从Resource加载if (source instanceof Resource) {return load((Resource) source);}// 从Package加载if (source instanceof Package) {return load((Package) source);}// 从 CharSequence 加载if (source instanceof CharSequence) {return load((CharSequence) source);}throw new IllegalArgumentException("Invalid source type " + source.getClass());
}

当前我们的主类会按 Class 加载。继续跟进 load() 方法。

private int load(Class<?> source) {if (isGroovyPresent() && GroovyBeanDefinitionSource.class.isAssignableFrom(source)) {// Any GroovyLoaders added in beans{} DSL can contribute beans hereGroovyBeanDefinitionSource loader = BeanUtils.instantiateClass(source, GroovyBeanDefinitionSource.class);load(loader);}// 判断主类是不是存在@Component注解,主类@SpringBootApplication是一个组合注解,包含@Component。if (isComponent(source)) {// => 将启动类的 BeanDefinition注册进 beanDefinitionMapthis.annotatedReader.register(source);return 1;}return 0;
}

this.annotatedReader.register(source) 跟进 register() 方法,最终进到 AnnotatedBeanDefinitionReader 类的 doRegisterBean() 方法。

<T> void doRegisterBean(Class<T> annotatedClass, @Nullable Supplier<T> instanceSupplier, @Nullable String name,@Nullable Class<? extends Annotation>[] qualifiers, BeanDefinitionCustomizer... definitionCustomizers) {// 将指定的类封装为AnnotatedGenericBeanDefinitionAnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition(annotatedClass);if (this.conditionEvaluator.shouldSkip(abd.getMetadata())) {return;}abd.setInstanceSupplier(instanceSupplier);// 获取该类的 scope 属性ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(abd);abd.setScope(scopeMetadata.getScopeName());String beanName = (name != null ? name : this.beanNameGenerator.generateBeanName(abd, this.registry));AnnotationConfigUtils.processCommonDefinitionAnnotations(abd);if (qualifiers != null) {for (Class<? extends Annotation> qualifier : qualifiers) {if (Primary.class == qualifier) {abd.setPrimary(true);}else if (Lazy.class == qualifier) {abd.setLazyInit(true);}else {abd.addQualifier(new AutowireCandidateQualifier(qualifier));}}}for (BeanDefinitionCustomizer customizer : definitionCustomizers) {customizer.customize(abd);}BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(abd, beanName);definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);// => 将该BeanDefinition注册到IoC容器的beanDefinitionMap中BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, this.registry);
}

在该方法中将主类封装成 AnnotatedGenericBeanDefinition。最后一步将 BeanDefinition 注册进 beanDefinitionMap。

public static void registerBeanDefinition(BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)throws BeanDefinitionStoreException {// Register bean definition under primary name.String beanName = definitionHolder.getBeanName();// === Step Into ===registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());// Register aliases for bean name, if any.String[] aliases = definitionHolder.getAliases();if (aliases != null) {for (String alias : aliases) {registry.registerAlias(beanName, alias);}}
}

继续跟进 registerBeanDefinition() 方法。

@Override
public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) throws BeanDefinitionStoreException {Assert.hasText(beanName, "Bean name must not be empty");Assert.notNull(beanDefinition, "BeanDefinition must not be null");if (beanDefinition instanceof AbstractBeanDefinition) {try {// 最后一次校验了!对bean的Overrides进行校验,还不知道会在哪处理这些overrides((AbstractBeanDefinition) beanDefinition).validate();} catch (BeanDefinitionValidationException ex) {throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName,"Validation of bean definition failed", ex);}}// 判断是否存在重复名字的bean,之后看允不允许override// 以前使用synchronized实现互斥访问,现在采用ConcurrentHashMapBeanDefinition existingDefinition = this.beanDefinitionMap.get(beanName);if (existingDefinition != null) {// 如果该类不允许 Overriding 直接抛出异常if (!isAllowBeanDefinitionOverriding()) {throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName,"Cannot register bean definition [" + beanDefinition + "] for bean '" + beanName +"': There is already [" + existingDefinition + "] bound.");} else if (existingDefinition.getRole() < beanDefinition.getRole()) {// e.g. was ROLE_APPLICATION, now overriding with ROLE_SUPPORT or ROLE_INFRASTRUCTUREif (logger.isWarnEnabled()) {logger.warn("Overriding user-defined bean definition for bean '" + beanName +"' with a framework-generated bean definition: replacing [" +existingDefinition + "] with [" + beanDefinition + "]");}} else if (!beanDefinition.equals(existingDefinition)) {if (logger.isInfoEnabled()) {logger.info("Overriding bean definition for bean '" + beanName +"' with a different definition: replacing [" + existingDefinition +"] with [" + beanDefinition + "]");}} else {if (logger.isDebugEnabled()) {logger.debug("Overriding bean definition for bean '" + beanName +"' with an equivalent definition: replacing [" + existingDefinition +"] with [" + beanDefinition + "]");}}// 注册进beanDefinitionMapthis.beanDefinitionMap.put(beanName, beanDefinition);} else {if (hasBeanCreationStarted()) {// Cannot modify startup-time collection elements anymore (for stable iteration)synchronized (this.beanDefinitionMap) {this.beanDefinitionMap.put(beanName, beanDefinition);List<String> updatedDefinitions = new ArrayList<>(this.beanDefinitionNames.size() + 1);updatedDefinitions.addAll(this.beanDefinitionNames);updatedDefinitions.add(beanName);this.beanDefinitionNames = updatedDefinitions;if (this.manualSingletonNames.contains(beanName)) {Set<String> updatedSingletons = new LinkedHashSet<>(this.manualSingletonNames);updatedSingletons.remove(beanName);this.manualSingletonNames = updatedSingletons;}}} else {// Still in startup registration phase// 如果仍处于启动注册阶段,注册进beanDefinitionMapthis.beanDefinitionMap.put(beanName, beanDefinition);this.beanDefinitionNames.add(beanName);this.manualSingletonNames.remove(beanName);}this.frozenBeanDefinitionNames = null;}if (existingDefinition != null || containsSingleton(beanName)) {resetBeanDefinition(beanName);}
}

最终来到 DefaultListableBeanFactory 类的 registerBeanDefinition() 方法,DefaultListableBeanFactory 类还熟悉吗?相信大家一定非常熟悉这个类了。

DefaultListableBeanFactory 是 IoC 容器的具体产品。仔细看这个方法 registerBeanDefinition(),首先会检查是否已经存在,如果存在并且不允许被覆盖则直接抛出异常。不存在的话就直接注册进 beanDefinitionMap 中。

debug 跳过 prepareContext() 方法,可以看到,启动类的 BeanDefinition 已经注册进来了。

到这里启动流程的第五步就算讲完了,因为启动类 BeanDefinition 的注册流程和后面我们自定义的 BeanDefinition 的注册流程是一样的。这先介绍一遍这个流程,后面熟悉了这个流程就好理解了。后面马上就到最最最重要的 refresh() 方法了。

e. 刷新应用上下文(IoC 容器的初始化过程)

首先我们要知道到 IoC 容器的初始化过程,主要分下面三步:

  1. BeanDefinition 的 Resource 定位
  2. BeanDefinition 的载入
  3. 向 IoC 容器注册 BeanDefinition

在上一小节介绍了 prepareContext() 方法,在准备刷新阶段做了什么工作。接下来我们主要从 refresh() 方法中总结 IoC 容器的初始化过程。 从 run 方法的, refreshContext() 方法一路跟下去,最终来到 AbstractApplicationContext 类的 refresh() 方法。

@Override
public void refresh() throws BeansException, IllegalStateException {synchronized (this.startupShutdownMonitor) {// 1. 刷新上下文环境prepareRefresh();// 2. 这里是在子类中启动 refreshBeanFactory() 的地方ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();// 3. 准备bean工厂,以便在此上下文中使用prepareBeanFactory(beanFactory);try {// 4. 设置 beanFactory 的后置处理postProcessBeanFactory(beanFactory);// 5. 调用 BeanFactory 的后置处理器(这些处理器是在Bean定义中向容器注册的)invokeBeanFactoryPostProcessors(beanFactory);// 6. 注册Bean的后处理器,在Bean创建过程中调用registerBeanPostProcessors(beanFactory);// 7. 对上下文中的消息源进行初始化initMessageSource();// 8. 初始化上下文中的事件机制(多播器)initApplicationEventMulticaster();// 9. 初始化其他特殊的BeanonRefresh();// 10. 检查监听Bean并且将这些监听Bean向容器注册registerListeners();// 11. 实例化所有的(non-lazy-init)单件finishBeanFactoryInitialization(beanFactory);// 12. 发布容器事件,结束Refresh过程finishRefresh();} catch (BeansException ex) {if (logger.isWarnEnabled()) {logger.warn("Exception encountered during context initialization - cancelling refresh attempt: " + ex);}// Destroy already created singletons to avoid dangling resources.destroyBeans();// Reset 'active' flag.cancelRefresh(ex);// Propagate exception to caller.throw ex;} finally {resetCommonCaches();}}
}

从以上代码中我们可以看到,refresh() 方法中所作的工作也挺多,我们没办法面面俱到,主要根据 IoC 容器的初始化步骤进行分析,所以我们主要介绍重要的方法,其他的请看注释。

- 2. obtainFreshBeanFactory()

在启动流程的第三步:初始化应用上下文中我们创建了应用的上下文,并触发了 GenericApplicationContext 类的构造方法,创建了 beanFactory,也就是创建了 DefaultListableBeanFactory 类。

public GenericApplicationContext() {this.beanFactory = new DefaultListableBeanFactory();
}

关于 obtainFreshBeanFactory() 方法,其实就是拿到我们之前创建的 beanFactory。

protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {// 刷新BeanFactoryrefreshBeanFactory();// 获取beanFactoryConfigurableListableBeanFactory beanFactory = getBeanFactory();if (logger.isDebugEnabled()) {logger.debug("Bean factory for " + getDisplayName() + ": " + beanFactory);}return beanFactory;
}

从上面代码可知,在该方法中主要做了三个工作,刷新 beanFactory、获取 beanFactory、返回 beanFactory。

(1)首先看一下 refreshBeanFactory() 方法,跟下去来到 GenericApplicationContext 类的 refreshBeanFactory() 发现也没做什么。

@Override
protected final void refreshBeanFactory() throws IllegalStateException {if (!this.refreshed.compareAndSet(false, true)) {throw new IllegalStateException("GenericApplicationContext does not support multiple refresh attempts: just call 'refresh' once");}this.beanFactory.setSerializationId(getId());
}
  1. AbstractApplicationContext 类有两个子类实现了 refreshBeanFactory(),但是在前面第三步初始化上下文的时候,实例化了 GenericApplicationContext 类,所以没有进入 AbstractRefreshableApplicationContext 中的 refreshBeanFactory() 方法。
  2. this.refreshed.compareAndSet(false, true) 这行代码在这里表示:GenericApplicationContext 只允许刷新一次。首先看一下 this.refreshed 属性:private final AtomicBoolean refreshed = new AtomicBoolean(); 通过该类的 compareAndSet() 可以实现一段代码绝对只实现一次的功能。

- 3. prepareBeanFactory(beanFactory)

从字面意思上可以看出准备 BeanFactory。

protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) {// Tell the internal bean factory to use the context's class loader etc.// 配置类加载器:默认使用当前上下文的类加载器beanFactory.setBeanClassLoader(getClassLoader());// 配置EL表达式:在Bean初始化完成,填充属性的时候会用到beanFactory.setBeanExpressionResolver(new StandardBeanExpressionResolver(beanFactory.getBeanClassLoader()));// 添加属性编辑器 PropertyEditorbeanFactory.addPropertyEditorRegistrar(new ResourceEditorRegistrar(this, getEnvironment()));// Configure the bean factory with context callbacks.// 添加Bean的后置处理器beanFactory.addBeanPostProcessor(new ApplicationContextAwareProcessor(this));// 忽略装配以下指定的类beanFactory.ignoreDependencyInterface(EnvironmentAware.class);beanFactory.ignoreDependencyInterface(EmbeddedValueResolverAware.class);beanFactory.ignoreDependencyInterface(ResourceLoaderAware.class);beanFactory.ignoreDependencyInterface(ApplicationEventPublisherAware.class);beanFactory.ignoreDependencyInterface(MessageSourceAware.class);beanFactory.ignoreDependencyInterface(ApplicationContextAware.class);// BeanFactory interface not registered as resolvable type in a plain factory.// MessageSource registered (and found for autowiring) as a bean.// 将以下类注册到 beanFactory(DefaultListableBeanFactory) 的resolvableDependencies属性中beanFactory.registerResolvableDependency(BeanFactory.class, beanFactory);beanFactory.registerResolvableDependency(ResourceLoader.class, this);beanFactory.registerResolvableDependency(ApplicationEventPublisher.class, this);beanFactory.registerResolvableDependency(ApplicationContext.class, this);// Register early post-processor for detecting inner beans as ApplicationListeners.// 将早期后处理器注册为application监听器,用于检测内部beanbeanFactory.addBeanPostProcessor(new ApplicationListenerDetector(this));// Detect a LoadTimeWeaver and prepare for weaving, if found.// 如果当前BeanFactory包含loadTimeWeaver Bean,说明存在类加载期织入AspectJ,// 则把当前BeanFactory交给类加载期BeanPostProcessor实现类LoadTimeWeaverAwareProcessor来处理,// 从而实现类加载期织入AspectJ的目的。if (beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));// Set a temporary ClassLoader for type matching.beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));}// Register default environment beans.// 将当前环境变量(environment) 注册为单例beanif (!beanFactory.containsLocalBean(ENVIRONMENT_BEAN_NAME)) {beanFactory.registerSingleton(ENVIRONMENT_BEAN_NAME, getEnvironment());}// 将当前系统配置(systemProperties) 注册为单例Beanif (!beanFactory.containsLocalBean(SYSTEM_PROPERTIES_BEAN_NAME)) {beanFactory.registerSingleton(SYSTEM_PROPERTIES_BEAN_NAME, getEnvironment().getSystemProperties());}// 将当前系统环境 (systemEnvironment) 注册为单例Beanif (!beanFactory.containsLocalBean(SYSTEM_ENVIRONMENT_BEAN_NAME)) {beanFactory.registerSingleton(SYSTEM_ENVIRONMENT_BEAN_NAME, getEnvironment().getSystemEnvironment());}
}

- 4. postProcessBeanFactory(beanFactory)

该方法向上下文中添加了一系列的 Bean 的后置处理器。

后置处理器工作的时机是在所有的 BeanDenifition 加载完成之后,bean 实例化之前执行。简单来说 Bean 的后置处理器可以修改 BeanDefinition 的属性信息。

* 5. invokeBeanFactoryPostProcessors(beanFactory)

IoC 容器的初始化过程包括三个步骤,在 invokeBeanFactoryPostProcessors() 方法中完成了 IoC 容器初始化过程的三个步骤。

第一步:Resource 定位

在 SpringBoot 中,我们都知道他的包扫描是从主类所在的包开始扫描的,prepareContext() 方法中,会先将主类解析成 BeanDefinition,然后在 refresh() 方法的 invokeBeanFactoryPostProcessors() 方法中解析主类的 BeanDefinition 获取 basePackage 的路径。这样就完成了定位的过程。

其次 SpringBoot 的各种 starter 是通过 SPI 扩展机制实现的自动装配,SpringBoot 的自动装配同样也是在 invokeBeanFactoryPostProcessors() 方法中实现的。还有一种情况,在 SpringBoot 中有很多的 @EnableXXX 注解,细心点进去看的应该就知道其底层是 @Import 注解,在 invokeBeanFactoryPostProcessors() 方法中也实现了对该注解指定的配置类的定位加载。

常规的在 SpringBoot 中有三种实现定位(非常规的不说了),第一个是主类所在包的,第二个是 SPI 扩展机制实现的自动装配(比如各种 starter),第三种就是 @Import 注解指定的类。

第二步:BeanDefinition 的载入

在第一步中说了三种 Resource 的定位情况,定位后紧接着就是 BeanDefinition 的分别载入。所谓的载入就是通过上面的定位得到的 basePackage,SpringBoot 会将该路径拼接成:classpath*:com/itheima/**/*.class 这样的形式,然后一个叫做 xPathMatchingResourcePatternResolver 的类会将该路径下所有的 .class 文件都加载进来,然后遍历判断是不是有 @Component 注解,如果有的话,就是我们要装载的 BeanDefinition。大致过程就是这样的了。

@Configuration、@Controller、@Service 等注解底层都是 @Component 注解,只不过包装了一层罢了。

第三步:注册 BeanDefinition

这个过程通过调用上文提到的 BeanDefinitionRegister 接口的实现来完成。这个注册过程把载入过程中解析得到的 BeanDefinition 向 IoC 容器进行注册。通过上文的分析,我们可以看到,在 IoC 容器中将 BeanDefinition 注入到一个 ConcurrentHashMap 中,IoC 容器就是通过这个 HashMap 来持有这些 BeanDefinition 数据的。比如 DefaultListableBeanFactory 中的 beanDefinitionMap 属性。

接下来我们通过代码看看具体是怎么实现的。

/* AbstractApplicationContext.java */
protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());...
}/* PostProcessorRegistrationDelegate.java */
public static void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory, List<BeanFactoryPostProcessor> beanFactoryPostProcessors) {...invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry);...
}private static void invokeBeanDefinitionRegistryPostProcessors(Collection<? extends BeanDefinitionRegistryPostProcessor> postProcessors, BeanDefinitionRegistry registry) {for (BeanDefinitionRegistryPostProcessor postProcessor : postProcessors) {postProcessor.postProcessBeanDefinitionRegistry(registry);}
}/* ConfigurationClassPostProcessor.java */
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {...processConfigBeanDefinitions(registry);
}public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {...do {parser.parse(candidates);parser.validate();Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());configClasses.removeAll(alreadyParsed);this.reader.loadBeanDefinitions(configClasses);alreadyParsed.addAll(configClasses);...candidates.clear();} while (!candidates.isEmpty());
}

再继续往下看之前来补充说下 ConfigurationClassPostProcessor 是什么时候注册进来的?在第 3 步构造应用上下文环境时,上下文对象的构造方法内注册的。

一路跟踪调用栈,来到 ConfigurationClassParser 类的 parse() 方法。

public void parse(Set<BeanDefinitionHolder> configCandidates) {for (BeanDefinitionHolder holder : configCandidates) {BeanDefinition bd = holder.getBeanDefinition();try {// => 如果是SpringBoot项目进来的,bd其实就是前面「主类」封装成的//    AnnotatedGenericBeanDefinition(AnnotatedBeanDefinition接口的实现类)if (bd instanceof AnnotatedBeanDefinition) {parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());}else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());}else {parse(bd.getBeanClassName(), holder.getBeanName());}}catch (BeanDefinitionStoreException ex) {throw ex;}catch (Throwable ex) {throw new BeanDefinitionStoreException("Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex);}}// 去执行组件类(@Import注解处理相关)this.deferredImportSelectorHandler.process();
}

在前面的 prepareContext() 中我们详细介绍了我们的主类是如何一步步的封装成 AnnotatedGenericBeanDefinition,并注册进 IoC 容器的 beanDefinitionMap 的。

/* ConfigurationClassParser.java */
protected final void parse(AnnotationMetadata metadata, String beanName) throws IOException {// beanName="springBootMytestApplication"// 先给包装成ConfigurationClassprocessConfigurationClass(new ConfigurationClass(metadata, beanName), DEFAULT_EXCLUSION_FILTER);
}
protected void processConfigurationClass(ConfigurationClass configClass, Predicate<String> filter) throws IOException {...// Recursively process the configuration class and its superclass hierarchy. 递归地处理配置类及其父类层次结构SourceClass sourceClass = asSourceClass(configClass, filter);do {// => 递归处理Bean,如果有父类则递归处理,直到顶层父类sourceClass = doProcessConfigurationClass(configClass, sourceClass, filter);} while (sourceClass != null);this.configurationClasses.put(configClass, configClass);
}

看 doProcessConfigurationClass() 方法,这是 SpringBoot 包扫描的入口方法!

protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass) throws IOException {// Recursively process any member (nested) classes first// 首先递归处理内部类(SpringBoot项目的主类一般没有内部类)processMemberClasses(configClass, sourceClass);// 针对 @PropertySource 注解的属性配置处理for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(sourceClass.getMetadata(), PropertySources.class,org.springframework.context.annotation.PropertySource.class)) {if (this.environment instanceof ConfigurableEnvironment) {processPropertySource(propertySource);} else {logger.warn("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +"]. Reason: Environment must implement ConfigurableEnvironment");}}// 根据 @ComponentScan 注解,扫描项目中的Bean(SpringBoot启动类上有该注解)Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);if (!componentScans.isEmpty() &&!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {for (AnnotationAttributes componentScan : componentScans) {// The config class is annotated with @ComponentScan -> perform the scan immediately// => 立即执行扫描(SpringBoot项目为什么是从主类所在的包扫描,这就是关键了!)Set<BeanDefinitionHolder> scannedBeanDefinitions =this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());// Check the set of scanned definitions for any further config classes and parse recursively if neededfor (BeanDefinitionHolder holder : scannedBeanDefinitions) {BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();if (bdCand == null) {bdCand = holder.getBeanDefinition();}// 检查是否是ConfigurationClass(是否有@Configuration/@Component俩注解),如果是,递归查找该类相关联的配置类。// 所谓相关的配置类,比如@Configuration中的@Bean定义的bean或在有@Component注解的类上继续存在@Import注解。if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {parse(bdCand.getBeanClassName(), holder.getBeanName());}}}}// Process any @Import annotations// 【递归处理】@Import 注解(SpringBoot项目中经常用的各种@EnableXXX注解基本都是封装的@Import)processImports(configClass, sourceClass, getImports(sourceClass), true);// Process any @ImportResource annotationsAnnotationAttributes importResource =AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);if (importResource != null) {String[] resources = importResource.getStringArray("locations");Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader");for (String resource : resources) {String resolvedResource = this.environment.resolveRequiredPlaceholders(resource);configClass.addImportedResource(resolvedResource, readerClass);}}// Process individual @Bean methodsSet<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);for (MethodMetadata methodMetadata : beanMethods) {configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));}// Process default methods on interfacesprocessInterfaces(configClass, sourceClass);// Process superclass, if anyif (sourceClass.getMetadata().hasSuperClass()) {String superclass = sourceClass.getMetadata().getSuperClassName();if (superclass != null && !superclass.startsWith("java") &&!this.knownSuperclasses.containsKey(superclass)) {this.knownSuperclasses.put(superclass, configClass);// Superclass found, return its annotation metadata and recursereturn sourceClass.getSuperClass();}}// No superclass -> processing is completereturn null;
}

在以上代码的 parse(bdCand.getBeanClassName(), holder.getBeanName()); 会进行递归调用,因为当 Spring 扫描到需要加载的类会进一步判断每一个类是否满足是 @Component/@Configuration 注解的类,如果满足会递归调用 parse() 方法,查找其相关的类。同样的 processImports(configClass, sourceClass, getImports(sourceClass), true); 通过 @Import 注解查找到的类同样也会递归查找其相关的类。

两个递归在 debug 的时候会很乱,用文字叙述起来更让人难以理解,所以我们只关注对主类的解析及其类的扫描过程。


上面代码中 for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(... 获取主类上的@PropertySource注解 解析该注解并将该注解指定的 properties 配置文件中的值存储到 Spring 的 Environment 中,Environment 接口提供方法去读取配置文件中的值,参数是 properties 文件中定义的 key 值。


Set componentScans = AnnotationConfigUtils.attributesForRepeatable( sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class); 会解析主类上的 @ComponentScan 注解。Set<BeanDefinitionHolder> scannedBeanDefinitions = this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName()); 将解析该注解并进行包扫描。

public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, final String declaringClass) {ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(this.registry, componentScan.getBoolean("useDefaultFilters"), this.environment, this.resourceLoader);// 如果是SpringBoot项目则declaringClass为主类的全路径名if (basePackages.isEmpty()) {basePackages.add(ClassUtils.getPackageName(declaringClass));}...// => 根据basePackages扫描类return scanner.doScan(StringUtils.toStringArray(basePackages));
}

到这里 IoC 容器初始化三个步骤的第一步,Resource 定位就完成了,成功定位到了主类所在的包。

接着往下看 scanner.doScan 是如何进行类扫描的。进入 doScan() 方法。

protected Set<BeanDefinitionHolder> doScan(String... basePackages) {Assert.notEmpty(basePackages, "At least one base package must be specified");Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();for (String basePackage : basePackages) {// => 从basePackage中扫描类并解析成BeanDefinition,拿到所有符合条件的类Set<BeanDefinition> candidates = findCandidateComponents(basePackage);for (BeanDefinition candidate : candidates) {ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);candidate.setScope(scopeMetadata.getScopeName());String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);if (candidate instanceof AbstractBeanDefinition) {postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);}if (candidate instanceof AnnotatedBeanDefinition) {AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);}if (checkCandidate(beanName, candidate)) {BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);beanDefinitions.add(definitionHolder);// => 将该 Bean 注册进 IoC 容器(beanDefinitionMap)registerBeanDefinition(definitionHolder, this.registry);}}}return beanDefinitions;
}

也就是说在这个方法中完成了 IoC 容器初始化过程的第二三步,BeanDefinition 的载入和 BeanDefinition 的注册。

到这里 IoC 容器的初始化过程的三个步骤就梳理完了。当然这只是针对 SpringBoot 的包扫描的定位方式的 BeanDefinition 的定位、加载和注册过程。前面我们说过,还有两种方式 @Import 和 SPI 扩展实现的 starter 的自动装配。


processImports(configClass, sourceClass, getImports(sourceClass), true); 解析主类上的 @Import 注解,并加载该注解指定的配置类。

在 Spring 中好多注解都是一层一层封装的,比如 @EnableXXX,是对 @Import 注解的二次封装。

  • @SpringBootApplication = @ComponentScan + @EnableAutoConfiguration + @Import + @Configuration + @Component
  • @Controller、@Service 等是对 @Component 的二次封装

该方法调用入参中的 configClass 和 sourceClass 参数都是主类相对应的。

首先看 getImports(sourceClass) 方法:

再回到 processImports 主流程:

private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,Collection<SourceClass> importCandidates, Predicate<String> exclusionFilter,boolean checkForCircularImports) {...for (SourceClass candidate : importCandidates) {if (candidate.isAssignable(ImportSelector.class)) {// Candidate class is an ImportSelector -> delegate to it to determine importsClass<?> candidateClass = candidate.loadClass();ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class,this.environment, this.resourceLoader, this.registry);Predicate<String> selectorFilter = selector.getExclusionFilter();if (selectorFilter != null) {exclusionFilter = exclusionFilter.or(selectorFilter);}if (selector instanceof DeferredImportSelector) {this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);}else {String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames, exclusionFilter);processImports(configClass, currentSourceClass, importSourceClasses, exclusionFilter, false);}}else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {// Candidate class is an ImportBeanDefinitionRegistrar ->// delegate to it to register additional bean definitionsClass<?> candidateClass = candidate.loadClass();ImportBeanDefinitionRegistrar registrar = ParserStrategyUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class,this.environment, this.resourceLoader, this.registry);// =>configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());}else {// Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar ->// process it as an @Configuration classthis.importStack.registerImport(currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());processConfigurationClass(candidate.asConfigClass(configClass), exclusionFilter);}}....
}

再回到 ConfigurationClassParser 类的 parse() 方法:

public void parse(Set<BeanDefinitionHolder> configCandidates) {for (BeanDefinitionHolder holder : configCandidates) {BeanDefinition bd = holder.getBeanDefinition();try {// 如果是SpringBoot项目进来的,bd其实就是前面「主类」封装成的// AnnotatedGenericBeanDefinition(AnnotatedBeanDefinition接口的实现类)if (bd instanceof AnnotatedBeanDefinition) {parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());}else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());}else {parse(bd.getBeanClassName(), holder.getBeanName());}}catch (BeanDefinitionStoreException ex) {throw ex;}catch (Throwable ex) {throw new BeanDefinitionStoreException("Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex);}}// => 去执行组件类(@Import注解处理相关)this.deferredImportSelectorHandler.process();
}public void process() {List<DeferredImportSelectorHolder> deferredImports = this.deferredImportSelectors;this.deferredImportSelectors = null;try {if (deferredImports != null) {DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler();deferredImports.sort(DEFERRED_IMPORT_COMPARATOR);deferredImports.forEach(handler::register);// => Step Into ...handler.processGroupImports();}}finally {this.deferredImportSelectors = new ArrayList<>();}
}

点进 process 方法:

/* ConfigurationClassParser 内部类 DeferredImportSelectorGroupingHandler */public void processGroupImports() {for (DeferredImportSelectorGrouping grouping : this.groupings.values()) {Predicate<String> exclusionFilter = grouping.getCandidateFilter();// => Step Into ...grouping.getImports().forEach(entry -> {ConfigurationClass configurationClass = this.configurationClasses.get(entry.getMetadata());processImports(configurationClass, asSourceClass(configurationClass, exclusionFilter),Collections.singleton(asSourceClass(entry.getImportClassName(), exclusionFilter)),exclusionFilter, false);});}
}public Iterable<Group.Entry> getImports() {for (DeferredImportSelectorHolder deferredImport : this.deferredImports) {// => Step Into ...this.group.process(deferredImport.getConfigurationClass().getMetadata(), deferredImport.getImportSelector());}return this.group.selectImports();
}

和之前介绍的 process 完美衔接 => [3.2#b]!

f. 刷新应用上下文后的扩展接口

protected void afterRefresh(ConfigurableApplicationContext context, ApplicationArguments args) {
}

扩展接口,设计模式中的模板方法,默认为空实现。如果有自定义需求,可以重写该方法。比如打印一些启动结束 log 或一些其它后置处理。

5. 自定义 Starter

5.1 基本介绍

SpringBoot 中的 starter 是一种非常重要的机制,能够抛弃以前繁杂的配置,将其统一集成进 starter,应用者只需要在 pom.xml 中引入 starter 依赖,SpringBoot 就能自动扫描到要加载的信息并启动相应的默认配置。starter 让我们摆脱了各种依赖库的处理,需要配置各种信息的困扰。SpringBoot 会自动通过 classpath 路径下的类发现需要的 Bean,并注册进 IoC 容器。SpringBoot 提供了针对日常企业应用研发各种场景的 spring-boot-starter 依赖模块。所有这些依赖模块都遵循着约定成俗的默认配置,并允许我们调整这些配置,即遵循“约定大于配置”的理念。

比如我们在 SpringBoot 里面要引入 Redis,那么我们需要在 pom 中引入以下内容:

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

这其实就是一个 starter。

简而言之,starter 就是一个外部的项目,我们需要使用它的时候就可以在当前 SpringBoot 项目中引入它。

在我们的日常开发工作中,经常会有一些独立于业务之外的配置模块,我们经常将其放到一个特定的包下,然后如果另一个工程需要复用这块功能的时候,需要将代码硬拷贝到另一个工程,重新集成一遍,麻烦至极。如果我们将这些可独立于业务代码之外的功配置模块封装成一个个 starter,复用的时候只需要将其在 pom 中引用依赖即可,再由 SpringBoot 为我们完成自动装配,就非常轻松了~

以下案例是开发中遇到的部分场景:

  • 动态数据源
  • 登陆模块
  • 基于 AOP 技术实现日志切面

自定义 starter 的命名规则:

SpringBoot 提供的 starter 以 spring-boot-starter-xxx 的方式命名的。官方建议自定义的 starter 使用 xxx-spring-boot-starter 命名规则。以区分 SpringBoot 生态提供的 starter。

5.2 自定义 starter

整个过程分为两部分:

  • 自定义 starter
  • 使用 starter

a. 自定义 starter

(1)新建 maven 工程,工程名为 zdy-spring-boot-starter,导入依赖:

<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-autoconfigure</artifactId><version>2.2.9.RELEASE</version></dependency>
</dependencies>

(2)编写 JavaBean

@EnableConfigurationProperties(SimpleBean.class) 
@ConfigurationProperties(prefix = "simplebean") 
public class SimpleBean {private int id;private String name;public int getId() {return id;}public void setId(int id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}@Overridepublic String toString() {return "SimpleBean{" +"id=" + id +", name='" + name + '\'' +'}';}
}

(3)编写配置类 MyAutoConfiguration

@Configuration
public class MyAutoConfiguration {static {System.out.println("MyAutoConfiguration init...");}@Beanpublic SimpleBean simpleBean(){return new SimpleBean();}}

(4)resources 下创建 /META-INF/spring.factories

注意:META-INF 是自己手动创建的目录,spring.factories 也是手动创建的文件,在该文件中配置自己的自动配置类。

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.itheima.config.MyAutoConfiguration

上面这句话的意思就是 SpringBoot 启动的时候会去加载我们的 SimpleBean 到 IoC 容器中。这其实是一种变形的 SPI 机制。

b. 使用自定义 starter

(1)导入自定义 starter 的依赖

<dependency><groupId>com.itheima</groupId><artifactId>zdy-spring-boot-starter</artifactId><version>1.0-SNAPSHOT</version>
</dependency>

(2)在全局配置文件中配置属性值

simplebean.id=1
simplebean.name=自定义starter

(3)编写测试方法

@Autowired
private SimpleBean simpleBean;@Test
public void zdyStarterTest(){System.out.println(simpleBean);
}

但此处还有一个问题,如果有一天我们不想要启动工程的时候自动装配 SimpleBean 呢?可能有的同学会想,那简单啊,我们去 pom 中把依赖注释掉,的确,这是一种方案,但未免有点 low~

c. 热插拔技术

还记得我们经常会在启动类 Application 上面加 @EnableXXX 注解吗?

其实这个 @Enablexxx 注解就是一种热拔插技术,加了这个注解就可以启动对应的 starter,当不需要对应的 starter 的时候只需要把这个注解注释掉就行,是不是很优雅呢?那么这是如何实现的呢?

改造自定义 starter 工程新增热插拔支持类:

(1)新增标记类 ConfigMarker

public class ConfigMarker {
}

(2)新增 @EnableRegisterServer 注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)@Import(ConfigMarker.class)
public @interface EnableRegisterServer {
}

(3)改造 MyAutoConfiguration 新增条件注解 @ConditionalOnBean(ConfigMarker.class),@ConditionalOnBean 是条件注解,代表只有当上下文中含有 ConfigMarker 对象,被标注的类才会被实例化。

@Configuration
@ConditionalOnBean(ConfigMarker.class)
public class MyAutoConfiguration {@Beanpublic SimpleBean simpleBean(){return new SimpleBean();}}

(4)在使用自定义 starter 的工程的启动类上增加 @EnableImRegisterServer 注解。

当加了 @EnableImRegisterServer 注解的时候,由于这个注解使用了 @Import({ConfigMarker.class}),所以会导致 Spring 去加载 ConfigMarker 到上下文中,而又因为条件注解 @ConditionalOnBean(ConfigMarker.class) 的存在,所以 MyAutoConfiguration 类就会被实例化。

补充说明条件注解:

  • @ConditionalOnBean:仅仅在当前容器给i中存在某个对象时,才会实例化一个Bean。
  • @ConditionalOnClass:某个class位于类路径上,才会实例化一个Bean。
  • @ConditionalOnExpression:当表达式为true的时候,才会实例化一个Bean。基于SpEL表达式的条件判断。
  • @ConditionalOnMissingBean:仅仅在当前上下文中不存在某个对象时,才会实例化一个Bean。
  • @ConditionalOnMissingClass:某个class类路径上不存在的时候,才会实例化一个Bean。
  • @ConditionalOnNotWebApplication:不是web应用,才会实例化一个Bean。
  • @ConditionalOnWebApplication:当项目是一个Web项目时进行实例化。
  • @ConditionalOnNotWebApplication:当项目不是一个Web项目时进行实例化。
  • @ConditionalOnProperty:当指定的属性有指定的值时进行实例化。
  • @ConditionalOnJava:当JVM版本为指定的版本范围时触发实例化。
  • @ConditionalOnResource:当类路径下有指定的资源时触发实例化。
  • @ConditionalOnJndi:在JNDI存在的条件下触发实例化。
  • @ConditionalOnSingleCandidate:当指定的Bean在容器中只有一个,或者有多个但是指定了首选的Bean时触发实例化。

6. 内嵌 Tomcat

SpringBoot 默认支持 Tomcat、Jetty 和 Undertow 作为底层容器。而 SpringBoot 默认使用 Tomcat,一旦引入 spring-boot-starter-web 模块,就默认使用 Tomcat 容器。

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>

6.1 Servlet 容器使用

a. 默认 Servlet 容器

我们看看 spring-boot-starter-web 这个 starter 中有什么?

<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-json</artifactId></dependency><!-- TOMCAT --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-tomcat</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId><exclusions><exclusion><groupId>org.apache.tomcat.embed</groupId><artifactId>tomcat-embed-el</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-web</artifactId></dependency><!-- SpringMVC --><dependency><groupId>org.springframework</groupId><artifactId>spring-webmvc</artifactId></dependency>
</dependencies>

b. 切换 Servlet 容器

那如果我么想切换其他 Servlet 容器呢,只需如下两步:

  • 将 tomcat 依赖移除掉
  • 引入其他 Servlet 容器依赖

e.g. 引入 Jetty

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><exclusions><exclusion><!-- 移除 spring-boot-starter-web 中的 TOMCAT --><artifactId>spring-boot-starter-tomcat</artifactId><groupId>org.springframework.boot</groupId></exclusion></exclusions>
</dependency><dependency><groupId>org.springframework.boot</groupId><!-- 引入 Jetty --><artifactId>spring-boot-starter-jetty</artifactId>
</dependency>

6.2 内嵌 Tomcat 自动配置原理

在启动 SpringBoot 的时候可谓是相当简单,只需要执行以下代码:

@SpringBootApplication
public class SpringBootMytestApplication {public static void main(String[] args) {SpringApplication.run(SpringBootMytestApplication.class, args);}
}

那些看似简单的事物,其实并不简单。我们之所以觉得他简单,是因为复杂性都被隐藏了。通过上诉代码,大概率可以提出以下两个疑问:

  • SpringBoot 是如何启动内置 Tomcat 的?
  • SpringBoot 为什么可以响应请求,他是如何配置的SpringMVC?

a. 启动内置 Tomcat 流程

启动类上的 @EnableAutoConfiguration 注解之前已经说过了,该注解使用 @Import 注解对 AutoConfigurationImportSelector 类进行了引入。

AutoConfigurationImportSelector#selectImport() 方法中会去拿 META-INFO/spring.factories 文件配置的所有自动配置类。

这个 spring.factories 配置文件是 spring-boot-autoconfigure 下的:

打开 spring.factories 配置文件,找到 tomcat 所在的类,tomcat 加载在 ServletWebServerFactoryAutoConfiguration 配置类中:

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration,\
# ...

进入该类,里面也通过 @Import 注解将 EmbeddedTomcat、EmbeddedJetty、EmbeddedUndertow 等嵌入式容器类加载进来了,SpringBoot 默认是启动嵌入式 tomcat 容器,如果要改变启动 Jetty 或 undertow 容器,需在 pom 文件中去设置。

@Configuration(proxyBeanMethods = false)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@ConditionalOnClass(ServletRequest.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(ServerProperties.class)
@Import({ ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,ServletWebServerFactoryConfiguration.EmbeddedTomcat.class,ServletWebServerFactoryConfiguration.EmbeddedJetty.class,ServletWebServerFactoryConfiguration.EmbeddedUndertow.class })
public class ServletWebServerFactoryAutoConfiguration {

继续进入 EmbeddedTomcat 类中,见下图:

进入 TomcatServletWebServerFactory 类,里面的 getWebServer() 是关键方法:

继续进入 getTomcatWebServer() 方法,一直往下跟到 Tomcat 初始化方法,调用 tomcat.start() 方法,Tomcat 就正式开启运行:

protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {return new TomcatWebServer(tomcat, getPort() >= 0);
}public TomcatWebServer(Tomcat tomcat, boolean autoStart) {Assert.notNull(tomcat, "Tomcat Server must not be null");this.tomcat = tomcat;this.autoStart = autoStart;initialize();
}private void initialize() throws WebServerException {...// Start the server to trigger initialization listenersthis.tomcat.start();...
}

走到这里,Tomcat 在 SpringBoot 中的配置以及最终启动的流程就走完了,相信大家肯定有一个疑问,上面的 getWebServer() 方法是在哪里调用的呢?上面的代码流程并没有发现 getWebServer() 被调用的地方。

因为 getWebServer() 方法的调用根本就不在上面的代码流程中,它是在另外一个流程中被调用的。

b. getWebServer() 调用分析

上文 #4 的 run 方法大概做了以下几件事:

  1. 获取并启动监听器(通过加载 META-INF/spring.factories 完成了 SpringApplicationRunListener 实例化工作)
  2. 构造容器环境(简而言之就是加载系统变量、环境变量、配置文件)
  3. 创建容器
  4. 实例化 SpringBootExceptionReporter.class 用来支持报告关于启动的错误
  5. 准备容器
  6. 刷新容器
  7. 刷新容器后的扩展接口

那么内置 Tomcat 启动源码,就是隐藏在上诉第六步 - refreshContext 方法里面,该方法最终会调用到 AbstractApplicationContext 类的 refresh() 方法。

@Override
public void refresh() throws BeansException, IllegalStateException {// Prepare this context for refreshing.prepareRefresh();// Tell the subclass to refresh the internal bean factory.ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();// Prepare the bean factory for use in this context.prepareBeanFactory(beanFactory);// Allows post-processing of the bean factory in context subclasses.postProcessBeanFactory(beanFactory);// Invoke factory processors registered as beans in the context.invokeBeanFactoryPostProcessors(beanFactory);// Register bean processors that intercept bean creation.registerBeanPostProcessors(beanFactory);// Initialize message source for this context.initMessageSource();// Initialize event multicaster for this context.initApplicationEventMulticaster();// => Initialize other special beans in specific context subclasses.onRefresh();// Check for listener beans and register them.registerListeners();// Instantiate all remaining (non-lazy-init) singletons.finishBeanFactoryInitialization(beanFactory);// Last step: publish corresponding event.finishRefresh();}

进入 onRefresh() 会发现调用到 ServletWebServerApplicationContext 中的 createWebServer(),其内部的 getWebServer 过程见上一小节:

完整流程图示:

7. 自动配置 SpringMVC

在上一小节,我们介绍了 SpringBoot 是如何启动一个内置 Tomcat 的。

但 SpringBoot 又是如何装配的SpringMVC 呢?

其实仅仅引入 starter 是不够的,回忆一下在一个普通的WEB项目中如何去使用 SpringMVC,我们首先就是要在 web.xml 中配置如下配置:

<servlet><description>spring mvc servlet</description><servlet-name>springMvc</servlet-name><servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class><load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping><servlet-name>springMvc</servlet-name><url-pattern>*.do</url-pattern>
</servlet-mapping>

但是在 SpringBoot 中,我们没有了 web.xml 文件,我们如何去配置一个 Dispatcherservlet 呢?其实 Servlet3.0 规范中规定,要添加一个 Servlet,除了采用 xml 配置或注解的方式,还有一种通过代码的方式,伪代码如下:

servletContext.addServlet(name, this.servlet);

那么也就是说,如果我们能动态往 web 容器中添加一个我们构造好的 DispatcherServlet 对象,是不是就实现自动装配 SpringMVC 了。

7.1 自动配置 DispatcherServlet

SpringBoot 的自动配置基于 SPI 机制,实现自动配置的核心要点就是添加一个自动配置的类,SpringBoot MVC 的自动配置自然也是相同原理。

所以,先找到 SpringMVC 对应的自动配置类。

org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration

a. DispatcherServletAutoConfiguration 自动配置类

@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass(DispatcherServlet.class)
@AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class)
public class DispatcherServletAutoConfiguration {
  1. 首先注意到 @Configuration 表明这是一个配置类,将会被 Spring 给解析;
  2. @ConditionalOnWebApplication 意味着当是一个 web 项目且是 Servlet 项目的时候才会被解析;
  3. @ConditionalOnClass 表明 DispatcherServlet 这个核心类必须存在才解析该类;
  4. @AutoConfigureAfter 表明要在 ServletWebServerFactoryAutoConfiguration 这个类之后再解析,设定了一个顺序。

总的来说,这些注解表明了该自动配置类的会解析的前置条件需要满足。

其次,DispatcherServletAutoConfiguration 类主要包含了两个内部类,分别是:

  1. DispatcherServletConfiguration
  2. DispatcherServletRegistrationConfiguration

顾名思义,前者是配置 DispatcherServlet,后者是配置 DispatcherServlet 的注册类。

什么是注册类?我们知道 Servlet 实例是要被添加(注册)到如 Tomcat 这样的 ServletContext 里的,这样才能够提供请求服务。所以,DispatcherServletRegistrationConfiguration 将生成一个 Bean,负责将 DispatcherServlet 给注册到 ServletContext 中。

b. DispatcherServletConfiguration

先看看 DispatcherServletConfiguration 这个内部配置类的声明:

  • @Conditional 指明了一个前置条件判断,由 DefaultDispatcherServletCondition 实现。主要是判断了是否已经存在 DispatcherServlet,如果没有才会触发解析。
  • @ConditionalOnClass 指明了当 ServletRegistration 这个类存在的时候才会触发解析,生成的 DispatcherServlet 才能注册到 ServletContext 中。
  • @EnableConfigrationProperties 将会从 application.properties 这样的配置文件中读取 spring.http 和 spring.mvc 前缀的属性生成配置对象 HttpProperties 和 WebMvcProperties。

再看 DispatcherServletConfiguration 的内部代码,这个两个方法我们比较熟悉了,就是生成了 Bean:

  • dispatcherServlet() 方法将生成一个 DispatcherServlet的Bean 对象。比较简单,就是获取一个实例,然后添加一些属性设置。
  • multipartResolver() 方法主要是把你配置的 MultipartResolver 的 Bean 给重命名一下,防止你不是用 multipartResolver 这个名字作为 Bean 的名字。

c. DispatcherServletRegistrationConfiguration

先来看内部配置类的声明:

  • @Conditional 有一个前置判断,DispatcherServletRegistrationCondition 主要判断了该注册类的 Bean 是否存在。
  • @ConditionOnClass 也判断了 ServletRegistration 是否存在。
  • @EnableConfigurationProperties 生成了 WebMvcProperties 的属性对象。
  • @Import 导入了 DispatcherServletConfiguration,也就是我们上面的配置对象。

再看 DispatcherServletRegistrationConfiguration 的内部实现:

  • 内部只有一个方法,生成了 DispatcherServletRegistrationBean。核心逻辑就是实例化了一个 Bean,设置了一些参数,如 dispatcherServlet、loadOnStartup 等。

【小结】SpringBoot MVC 的自动配置类是 DispatcherServletAutoConfigration,主要做了两件事:

  1. 配置 DispatcherServlet
  2. 配置 DispatcherServlet 的注册 Bean(DispatcherServletRegistrationBean)

7.2 注册 DispatcherServlet 到 ServletContext

在上一小节的源码翻阅中,我们看到了 DispatcherServlet 和 DispatcherServletRegistrationBean 这两个 Bean 的自动配置。DispatcherServlet 我们很熟悉,DispatcherServletRegistrationBean 负责将 DispatcherServlet 注册到 ServletContext 当中。

既然 DispatcherServletRegistrationBean 类的职责是负责注册 DispatcherServlet,那么我们得知道什么时候触发注册操作。为此,我们先看看 DispatcherServletRegistrationBean 这个类的类图:

a. 注册 DispatcherServlet 流程

(1)我们可以看到最上面是一个 ServletContextInitializer 接口。见名知意,实现该接口意味着是用来初始化 ServletContext 的。

public interface ServletContextInitializer {void onStartup(ServletContext servletContext) throws ServletException;
}

(2)看看 RegistrationBean 是怎么实现 onStartup 方法的

@Override
public final void onStartup(ServletContext servletContext) throws ServletException {// 获取当前到底是一个filter / servlet / listenerString description = getDescription();if (!isEnabled()) {logger.info(StringUtils.capitalize(description) + " was not registered (disabled)");return;}// => 这是一个抽象方法// protected abstract void register(String description, ServletContext servletContext);register(description, servletContext);
}

(3)再看 DynamicRegistrationBean 是怎么实现 register 方法的

@Override
protected final void register(String description, ServletContext servletContext) {// => 这是一个抽象方法// protected abstract D addRegistration(String description, ServletContext servletContext);D registration = addRegistration(description, servletContext);if (registration == null) {logger.info(StringUtils.capitalize(description) + " was not registered (possibly already registered?)");return;}configure(registration);
}

(4)再看 ServletRegistrationBean 是怎么实现 addRegistration 方法的

@Override
protected ServletRegistration.Dynamic addRegistration(String description, ServletContext servletContext) {String name = getServletName();return servletContext.addServlet(name, this.servlet);
}

到这可以看到,是直接将 DispatcherServlet 给 add 到了 servletContext 当中。

b. SpringBoot 启动流程中具体体现

在创建 Servlet 容器过程中就去加载 SpringMVC,这是如何做到的呢?

我们通过调试知道 getServletContextInitializerBeans() 返回的是一个 ServletContextInitializer 集合,集合中有以下几个对象:

然后依次去调用对象的 onStartup 方法,那么对于上图标红的对象来说,就是会调用到 DispatcherServletRegistrationBean 的 onStartup 方法,这个类并没有这个方法,所以最终会调用到父类 RegistrationBean 的 onStartup 方法,也就是上一小节的代码流程。

【小结】SpringBoot 自动装配 SpringMVC 其实就是往 ServletContext 中加入了一个 Dispatcherservlet。 Servlet3.0 规范中有这个说明,除了可以动态加 Servlet,还可以动态加 Listener、Filter。

  • addServlet
  • addListener
  • addFilter

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/708163.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

免费将人像转动漫,多种效果可选,分享下自己开发的微信小程序

先上一张效果图: 支持多种风格: 支持历史记录: 历史记录详情: 支持将历史记录分享给好友: 小程序接口用的阿里云的两个接口,分别是: 第一排特效是同步请求,用的人物动漫化接口:https://help.aliyun.com/zh/viapi/developer-reference/api-animation-of-characters?sp…

Ubuntu下darknet yolo4的编译

Ubuntu下darknet yolo4的编译 首次编辑:24/5/17/23:16 最后编辑:24/5/18/12:47 参考链接YOLOv4-darknet installation and usage on your system (Windows & Linux) Installing and Building Darknet 官网0 说明 此博客只记录darknet yolo4 + opencv的编译安装,不涉及cu…

使用django_celery_beat在admin后台配置计划任务

使用步骤 安装包 pip install django-celery-beatapp注册 app注册INSTALLED_APPS = [....django_celery_beat,]配置文件:屏蔽原来的调度器 CELERY_BEAT_SCHEDULER = django_celery_beat.schedulers.DatabaseScheduler设置时区 LANGUAGE_CODE = zh-hans TIME_ZONE = Asia/Shang…

Celery admin监视任务

在控制台监控任务执行情况,还不是很方便,最好是能够通过web界面看到任务的执行情况,如有多少任务在执行,有多少任务执行失败了等 这个Celery也是可以做到了,就是将任务执行结果写到数据库中,通过web界面显示出来。 这里要用到django-celery-results插件。 通过插件可以使…

4、单行函数

最近项目要用到Oracle,奈何之前没有使用过,所以在B站上面找了一个学习视频,用于记录学习过程以及自己的思考。 视频链接: 【尚硅谷】Oracle数据库全套教程,oracle从安装到实战应用 如果有侵权,请联系删除,谢谢。本文主要讲解以下几点:SQL中不同类型的函数在 SELECT 语句…

CF-945(已更A,B)

CF-945(A,B) A 分析模拟合法情况下三个数的和只能是偶数:题中的两种操作显然都不会改变和的奇偶性这点我的代码中没有用到要使平局数最多,一定是最大的两个数减一,重复这个过程,直到两个较小的数都为零,且最大数一定是偶数,否则不合法:可以由题意和样例想到 代码 int…

VSCode安装vue3插件

1.以前的volar已经弃用了。 2.最近vue插件

python计算机视觉学习笔记——PIL库的用法

如果需要处理的原图及代码,请移步小编的GitHub地址传送门:请点击我如果点击有误:https://github.com/LeBron-Jian/ComputerVisionPractice这个是之前的笔记,自己看到了就顺带发出来,也是温习一下,内容可能不太全,算是入门贴吧。 前言:PIL 图像处理是计算机视觉领域中不…

Angular-测试驱动开发-全-

Angular 测试驱动开发(全)原文:zh.annas-archive.org/md5/60F96C36D64CD0F22F8885CC69A834D2 译者:飞龙 协议:CC BY-NC-SA 4.0前言 本书将为读者提供一个关于 JavaScript 测试驱动开发(TDD)的完整指南,然后深入探讨 Angular 的方法。它将提供清晰的、逐步的示例,不断强…

python中的装饰器,迭代器,生成器之间的关系

一、装饰器 装饰即修饰,意指为其他函数添加新功能; 装饰器的本质就是函数 作用是为其他函数添加新功能,如计算该函数运行时长 装饰器遵循原则: 1.不修改被装饰函数的源代码(开放封闭原则) 2.为被装饰函数添加新功能后,不能修改被修饰函数的调用方式 装饰器的实现 = 高阶…

RepVGG-GELAN | 融合 VGG、ShuffleNet 与 YOLO 图像检测的准确性及效率再上一层!

前言 基于YOLO的目标检测算法在速度和准确性之间取得了显著的平衡。然而,它们在脑肿瘤检测中的应用仍然未被充分探索。本研究提出了RepVGG-GELAN,这是一种新型的YOLO架构,通过集成RepVGG,一种重新参数化的卷积方法,特别关注于医学图像中的脑肿瘤检测。RepVGG-GELAN利用Rep…

有关字符串的函数接口

目录strstr函数,用于从一个字符串中查找子串strtok函数,用于分割字符串 strstr函数,用于从一个字符串中查找子串strtok函数,用于分割字符串