红茶的个人站点

  • 首页
  • 专栏
  • 开发工具
  • 其它
  • 隐私政策
Awalon
Talk is cheap,show me the code.
  1. 首页
  2. 专栏
  3. Spring Boot 学习笔记
  4. 正文

Spring 源码学习 15:Spring Boot

2025年7月7日 11点热度 0人点赞 0条评论

除了直接通过浏览器从 Spring 官网下载框架代码以及通过 Idea 创建外,还可以通过 Linux 下的命令行 Web 客户端下载,优点是可以结合 Bash 命令或脚本实现一些自动化功能,此外也可以在 Idea 无法创建项目框架时作为一种替代方案。

curl -G https://start.spring.io/starter.tgz -d dependencies=web,lombok                    -d type=maven-project -d java-version=21 -d baseDir=my-test -d packaging=jar | tar -xzvf -

可以通过虚拟机挂载 Windows 工作目录的方式,在虚拟机中直接通过命令将框架代码下载解压到 WIndows 的工作目录。

war 项目

创建一个 war 包 Spring Boot 项目:

image-20250705105029838

添加 JSP:

image-20250706142408938

war 包项目的 JSP 固定添加在src/main/webapp目录。

hello.jsp:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <h1>Hello World!</h1>
</body>
</html>

添加访问这个 JSP 的控制器类:

@Controller
public class HelloController {
    @GetMapping("/hello")
    public String hello(){
        return "hello";
    }
}

就像前文说的,这里控制器方法hello返回 String 类型,实际上会被返回值处理器当做 View 名称进行处理,会查找一个 JSP 当做视图返回给浏览器。

需要在配置类中指定在哪个目录下查找 JSP 文件:

spring.mvc.view.prefix=/
spring.mvc.view.suffix=.jsp

这里的spring.mvc.view.prefix指webapp目录,因此这里使用/。这样在查找视图的时候,就可以根据视图名(hello)查找到视图文件(/hello.jsp)。

通过外部 Tomcat 启动

我们知道 war 包是要在 Servlet 服务器上运行的,在 Idea 中,可以通过配置启动项将项目托管给外部 Tomcat 服务器运行:

image-20250706143113097

添加一个运行配置,选择Tomcat 服务器> 本地:

image-20250706143242236

配置内容如下:

image-20250706143353831

应用程序服务器要指定一个本地安装的 Tomcat 目录。

image-20250706143751616

部署分为两种方式,这里的war exploded指将项目目录挂载到 Tomcat 中运行,省去了打包环节。另一种就是打包成 war 包后放在 Tomcat 中运行。

需要注意的是下面的应用程序上下文,这里指将目录挂载到 Tomcat 的哪个路径下,通常设置为/,这样访问localhost:8081/就能对应到项目请求,否则如果设置为/war_exploded,就需要访问localhost:8081/war_exploded/。

启动配置好的 Tomcat 启动项:

image-20250706145217406

这样就说明启动成功。

在 Spring 的 war 项目框架代码中,包含一个:

image-20250706145338136

这个类的用途是让外部的 Tomcat 服务器能找到并执行 Spring 框架的入口类。

通过内部 Tomcat 启动

除了通过外部 Tomcat 启动,Spring war 项目也可以通过入口类的 main 方法启动:

image-20250706150041606

此时使用的是 Spring 自带的 Tomcat 运行项目。

需要注意的是,在这种情况下通过页面访问 JSP 文件,比如http://localhost:8080/hello,浏览器并不会加载和渲染返回的 JSP 页面,而是将其作为一个文件下载。这是因为 Spring 内置的 Tomcat 服务器缺少 JSP 解析器。

通过依赖添加:

<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-jasper</artifactId>
    <version>11.0.7</version>
</dependency>

现在访问就能看到 JSP 页面被正常加载。

如果依然下载文件,是因为浏览器缓存的问题,可以使用开发者工具禁用缓存后加载页面。

Spring 应用启动过程

Spring 应用实际启动代码为:

image-20250706152945162

分为两部分:

  • 构造器new SpringApplication(...),执行一些准备工作

  • SpringApplication对象的run方法,创建并初始化容器

构造器

加载 Bean 定义来源

构造器需要准备好创建 Bean 定义的来源,对于通常基于注解的 Spring Boot 项目来说,主要来源是入口类:

image-20250706153302840

用以下测试用例模拟类定义来源加载:

SpringApplication application = new SpringApplication(Config.class);
// 创建容器,打印 bean 定义
ConfigurableApplicationContext context = application.run();
String[] beanDefinitionNames = context.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
    System.out.println(beanDefinitionName);
}

run方法调用后容器中才会生成 bean 定义。

当做入口类的配置类:

@Configuration
public static class Config{
    @Bean
    public User user(){
        return new User("Tom", 20);
    }
}

运行代码会报错:

image-20250706153544529

因为应用被当做 Spring web 应用启动,缺少 ServletWebServerFactory。

添加:

@Bean
public ServletWebServerFactory servletWebServerFactory(){
    return new TomcatServletWebServerFactory();
}

可以在 SpringApplication 在构造阶段为其添加其他 bean 定义来源:

SpringApplication application = new SpringApplication(Config.class);
application.setSources(Set.of("classpath:applicationContext.xml"));

applicationContext.xml:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean name="student" class="cn.icexmoon.demo1.entity.Student"/>
</beans>

可以获取 bean 定义的来源信息:

String resourceDescription = context.getBeanFactory().getBeanDefinition(beanDefinitionName).getResourceDescription();
System.out.printf("{bean 名称:%s,来源:%s%n}", beanDefinitionName, resourceDescription);

输出:

image-20250706154851371

Spring 框架内置的 Bean 定义来源为 null。

判断容器类型

构造器的另一个工作是判断应用类型,即判断当前应用是一个非 Web 应用、Reactive Web 应用还是 Spring Web 应用,以便在之后run方法调用时创建对应类型的容器。

image-20250706155256926

判断依据是类路径中是否存或不存在某个关键的类:

image-20250706155400369

比如如果存在org.springframework.web.reactive.DispatcherHandler且不存在org.springframework.web.servlet.DispatcherServlet,就认为这是一个 Reactive Web 应用。

在测试用例中调用deduceFromClasspath方法获取应用类型:

// 判断应用类型
Method deduceFromClasspath = WebApplicationType.class.getDeclaredMethod("deduceFromClasspath");
deduceFromClasspath.setAccessible(true);
WebApplicationType applicationType = (WebApplicationType)deduceFromClasspath.invoke(null);
System.out.println(applicationType);

deduceFromClasspath方法是受保护的,所以要通过反射调用。

结果是SERVLET,表示这是一个 Spring Web 应用。

初始化器

在调用run方法创建容器后,执行容器的refresh方法前,会执行容器的初始化器以对容器做一些增强(通常是追加 Bean 定义)。因此,SpringApplication 的构造器中会准备一些容器的初始化器(ApplicationContextInitializer):

image-20250706161928456

通过测试用例模拟为SpringApplication添加容器的初始化器:

// 添加初始化器
application.addInitializers(new ApplicationContextInitializer<ConfigurableApplicationContext>() {
    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        if (applicationContext instanceof GenericApplicationContext genericApplicationContext) {
            genericApplicationContext.registerBean("teacher", Teacher.class);;
        }
    }
});

这里通过初始化器,为容器添加了一个 Bean 定义teacher。

示例中对ConfigurableApplicationContext向下转型是为了能调用registerBean方法。

事件监听器

在 run 方法调用时,会产生一些事件,因此 SpringApplication 构造器中会添加相应的事件监听器,对这些事件进行监听和处理:

image-20250706162528453

添加事件监听器:

// 添加监听器
application.addListeners(new ApplicationListener<ApplicationEvent>() {
    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        System.out.println("产生事件:"+event.getClass());
    }
});

输出:

产生事件:class org.springframework.boot.context.event.ApplicationStartingEvent
产生事件:class org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent
产生事件:class org.springframework.boot.context.event.ApplicationContextInitializedEvent
产生事件:class org.springframework.boot.context.event.ApplicationPreparedEvent
产生事件:class org.springframework.boot.web.servlet.context.ServletWebServerInitializedEvent
// ...

获取主类

SpringApplication 的构造器最后还会记录主类(Main Class):

image-20250706163034618

用测试用例模拟调用:

SpringApplication application = new SpringApplication(Config.class);
Method deduceMainApplicationClass = SpringApplication.class.getDeclaredMethod("deduceMainApplicationClass");
deduceMainApplicationClass.setAccessible(true);
Class mainClass = (Class)deduceMainApplicationClass.invoke(application);
System.out.println(mainClass);

需要反射调用,该方法是私有的。

输出:

class com.intellij.rt.junit.JUnitStarter

因为是测试用例,所以主类(入口类)是 JUnit 的启动类。

run 方法

事件发布

SpringApplication 对象的 run 方法执行后,会在不同阶段发布事件,执行事件发布的是SpringApplicationRunListeners,这是一个组合类(使用了复合模式),包含多个SpringApplicationRunListener:

image-20250707110620222

SpringApplicationRunListener是一个接口,有多个实现:

image-20250707110915781

Spring 在配置文件spring.factories中配置了 run 方法执行时使用的SpringApplicationRunListener实现类:

image-20250707111240692

使用工具类方法SpringFactoriesLoader.loadFactoryNames可以读取配置文件中的实现类列表:

List<String> factoryNames = SpringFactoriesLoader.loadFactoryNames(SpringApplicationRunListener.class, springApplication.getClassLoader());

然后就可以通过反射调用的方式创建消息发布对象:

Constructor<?> constructor = Class.forName(factoryName).getDeclaredConstructor(SpringApplication.class, String[].class);
constructor.setAccessible(true);
SpringApplicationRunListener runListener = (SpringApplicationRunListener)constructor.newInstance(springApplication, new String[0]);

模拟 run 方法启动后事件发布过程:

DefaultBootstrapContext bootstrapContext = new DefaultBootstrapContext();
// 开始执行 run 方法
runListener.starting(bootstrapContext);
// 准备环境信息
runListener.environmentPrepared(bootstrapContext, new StandardEnvironment());
// 创建容器,调用容器初始化器对容器初始化
GenericApplicationContext context = new GenericApplicationContext();
runListener.contextPrepared(context);
// 加载 bean 定义
runListener.contextLoaded(context);
// 刷新容器,创建单例 bean(如果需要),执行 bean 工厂的后处理器
context.refresh();
runListener.started(context, null);
// 执行 CommandLineRunner 和 ApplicationRunner
runListener.ready(context, null);
​
// RUN 方法执行过程中如果有异常产生
runListener.failed(context, new RuntimeException("test"));

这些事件发布方法与实际产生的事件对象的对应关系:

image-20250707114139134

准备环境信息

容器创建需要提供环境信息,这些环境信息的来源是多种多样的:

ApplicationEnvironment env = new ApplicationEnvironment();
for (PropertySource<?> propertySource : env.getPropertySources()) {
    System.out.println(propertySource);
}

ApplicationEnvironment 默认的来源有:

PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}

其中systemEnvironment指系统环境变量,比如:

System.out.println(env.getProperty("JAVA_HOME"));

来源的排序是有意义的,在这里systemProperties比systemEnvironment的优先级高。

可以添加.properties或.yaml配置文件作为源:

env.getPropertySources().addLast(new ResourcePropertySource(new ClassPathResource("application.properties")));

通过addLast添加的源优先级是最低的,通过配置文件设置的属性的优先级最低。

这样就可以从配置文件获取属性值:

System.out.println(env.getProperty("spring.application.name"));

还可以将命令行参数作为源加入环境信息:

String[] args = new String[]{
        "--spring.profiles.active=dev",
};
env.getPropertySources().addFirst(new SimpleCommandLinePropertySource(args));

显然,命令行参数的优先级是最高的,通过命令行参数修改应用的spring.profiles.active属性相当常见。

这里是测试用例,用本地变量模拟命令行参数。

获取属性:

System.out.println(env.getProperty("spring.profiles.active"));

特殊属性源

假设配置文件中存在这么几个属性:

user.person-one=Jack
user.person_two=Tom
user.personThree=Bruce

单词拼接形式分别是连字符、下划线和驼峰形式。获取属性值的时候统一使用连字符:

System.out.println(env.getProperty("user.person-one"));
System.out.println(env.getProperty("user.person-two"));
System.out.println(env.getProperty("user.person-three"));

输出:

Jack
null
null

只有严格匹配的属性正常获取到值。实际上 Spring 会为环境添加一个特殊的源,以提供宽松的属性名称匹配功能:

ConfigurationPropertySources.attach(env);

该方法会将一个特殊的源加入到第一位源:

image-20250707151954332

后处理器

Spring 并不需要一个个添加属性源,而是通过一些列环境后处理器(Enviroment Post Processor)进行添加:

image-20250707152951277

环境后处理器ConfigDataEnvironmentPostProcessor可以扫描类路径下的配置文件,将其添加为源:

ConfigDataEnvironmentPostProcessor configDataEnvironmentPostProcessor = new ConfigDataEnvironmentPostProcessor(new DeferredLogs(), new DefaultBootstrapContext());
printPropertySources(env, "执行前");
configDataEnvironmentPostProcessor.postProcessEnvironment(env, springApplication);
printPropertySources(env, "执行后");

结果:

image-20250707153819420

环境后处理器可以添加一个特殊的随机(Random)源:

RandomValuePropertySourceEnvironmentPostProcessor postProcessor = new RandomValuePropertySourceEnvironmentPostProcessor(new DeferredLogs());
printPropertySources(env, "执行前");
postProcessor.postProcessEnvironment(env, springApplication);
printPropertySources(env, "执行后");

可以利用这个源通过属性获取的方式产生随机数:

System.out.println(env.getProperty("random.int"));
System.out.println(env.getProperty("random.int"));
System.out.println(env.getProperty("random.int"));

输出:

-533867907
404823549
1757535839

甚至是获取 UUID:

System.out.println(env.getProperty("random.uuid"));
System.out.println(env.getProperty("random.uuid"));
System.out.println(env.getProperty("random.uuid"));

输出:

0ee15ff9-fb64-4cf4-b3d2-b31ccaa5d7c1
ece1424b-798a-4df2-88f0-82fbbb8f2a2c
cd26bf05-cfec-41c5-b970-308767fced11

事件驱动

Spring 会使用哪些环境后处理器的实现取决于配置文件spring.factories:

# Environment Post Processors
org.springframework.boot.env.EnvironmentPostProcessor=\
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\
org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor,\
org.springframework.boot.env.RandomValuePropertySourceEnvironmentPostProcessor,\
org.springframework.boot.env.SpringApplicationJsonEnvironmentPostProcessor,\
org.springframework.boot.env.SystemEnvironmentPropertySourceEnvironmentPostProcessor,\
org.springframework.boot.reactor.ReactorEnvironmentPostProcessor

从这个文件读取以及创建和执行环境后处理器的时机由事件进行触发和驱动。

SpringApplication springApplication = new SpringApplication();
// 添加环境后处理器的监听器
springApplication.addListeners(new EnvironmentPostProcessorApplicationListener());
ApplicationEnvironment env = new ApplicationEnvironment();
SpringApplicationRunListener publisher = getPublisher(springApplication);
// 发布事件
printPropertySources(env,"事件发布前");
publisher.environmentPrepared(new DefaultBootstrapContext(), env);
printPropertySources(env,"事件发布后");

输出:

image-20250707161428833

environmentPrepared事件发布后,多出了两个属性源。

属性绑定

环境信息准备好后,需要将 SpringApplication 需要从环境中读取的属性进行绑定。

Binder

配置文件:

user.first-name=Tom
user.middle_name=Jerry
user.lastName=Bruce

创建环境信息并添加配置文件为属性源:

ApplicationEnvironment env = new ApplicationEnvironment();
env.getPropertySources().addLast(new ResourcePropertySource("user", new ClassPathResource("user.properties")));

实现数据绑定:

User user = Binder.get(env).bind("user", User.class).get();
System.out.println(user);

输出:

EnvironmentTests.User(firstName=Tom, middleName=Jerry, lastName=Bruce)

Binder 对属性名的匹配也很宽松。

Binder 除了可以利用环境信息中的属性创建一个新的对象外,也可以对现有对象实现数据绑定(属性改写):

User user = new User();
Binder.get(env).bind("user", Bindable.ofInstance(user));

需要使用Bindable.ofInstance包装被绑定对象。

SpringApplication 属性绑定

属性配置文件:

spring.main.banner-mode=off
spring.main.lazy-initialization=true

加载并进行绑定:

SpringApplication springApplication = new SpringApplication();
ApplicationEnvironment env = new ApplicationEnvironment();
env.getPropertySources().addLast(new ResourcePropertySource("app", new ClassPathResource("app.properties")));
System.out.println(springApplication);
Binder.get(env).bind("spring.main", Bindable.ofInstance(springApplication));
System.out.println(springApplication);

可以在 debug 模式下观察 SpringApplication 对象的属性变化:

image-20250707165020490

创建容器

之前说了,在 SpringApplication 的构造器中需要判断容器类型,在 run 方法中,可以根据容器类型创建相应的容器:

switch (applicationType){
    case NONE -> context = new AnnotationConfigApplicationContext();
    case SERVLET -> context = new AnnotationConfigServletWebApplicationContext();
    case REACTIVE -> context = new AnnotationConfigReactiveWebApplicationContext();
}

执行容器的初始化器

容器在创建后,需要执行初始化器,这些初始化器是在 SpringApplication 的构造器中添加的:

// 模拟 run 方法中调用初始化器
for (ApplicationContextInitializer initializer : springApplication.getInitializers()) {
    initializer.initialize(context);
}

添加 Bean 定义

容器在初始化好后,会从资源中添加 Bean 定义,同样的,这些 Bean 定义来源在构造器中指定。

添加 Bean 定义分为三种方式:

  • 以注解的方式从配置类添加

  • 从 XML 文件添加

  • 通过包扫描添加

AnnotationConfigServletWebApplicationContext context = new AnnotationConfigServletWebApplicationContext();
// 从配置类添加
DefaultListableBeanFactory defaultListableBeanFactory = context.getDefaultListableBeanFactory();
AnnotatedBeanDefinitionReader configReader = new AnnotatedBeanDefinitionReader(defaultListableBeanFactory);
configReader.register(Config.class);
// 从 XML 文件添加
XmlBeanDefinitionReader xmlReader = new XmlBeanDefinitionReader(defaultListableBeanFactory);
xmlReader.loadBeanDefinitions("classpath:applicationContext.xml");
// 通过包扫描添加
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(defaultListableBeanFactory);
scanner.scan("cn.icexmoon.demo1.entity");
context.refresh();
String[] beanDefinitionNames = context.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
    log.debug("beanDefinitionName: {}", beanDefinitionName);
}

完整示例见这里。

Runner

当容器初始化完毕后(执行过refresh方法),会执行 CommandLineRunner 和 ApplicationRunner,通常我们会利用实现了这两个接口的 Bean 添加需要在应用启动后执行的代码。

比如:

@Configuration
static class WebConfig{
    @Bean
    public CommandLineRunner commandLineRunner(){
        return new CommandLineRunner() {
            @Override
            public void run(String... args) throws Exception {
                System.out.println("CommandLineRunner is called.");
            }
        };
    }
​
    @Bean
    public ApplicationRunner applicationRunner(){
        return new ApplicationRunner() {
            @Override
            public void run(ApplicationArguments args) throws Exception {
                System.out.println("ApplicationRunner is called.");
            }
        };
    }
}

模拟 run 方法中执行 runner:

String[] args = new String[0];
Map<String, CommandLineRunner> commandLineRunnerMap = context.getBeansOfType(CommandLineRunner.class);
for (CommandLineRunner commandLineRunner : commandLineRunnerMap.values()) {
    commandLineRunner.run(args);
}
Map<String, ApplicationRunner> applicationRunnerMap = context.getBeansOfType(ApplicationRunner.class);
for (ApplicationRunner applicationRunner : applicationRunnerMap.values()) {
    applicationRunner.run(new DefaultApplicationArguments(args));
}

CommandLineRunner与ApplicationRunner的区别在于它们接收的参数不同,前者接收的是String[]类型的参数,也就是入口类的 main 方法接收的命令行参数的原始类型。后者对命令行参数进行了封装,接收ApplicationArguments类型的命令行参数。

如果 runer 中的代码需要使用命令行参数,推荐使用ApplicationRunner,因为ApplicationArguments提供一些更方便处理命令行参数的 API。

假设命令行参数是:

String[] args = new String[]{
        "--spring.profiles.active=dev",
        "--spring.main.web-application-type=servlet",
        "test"
};

ApplicationRunner:

@Bean
public ApplicationRunner applicationRunner() {
    return new ApplicationRunner() {
        @Override
        public void run(ApplicationArguments args) throws Exception {
            System.out.println("ApplicationRunner is called.");
            Set<String> optionNames = args.getOptionNames();
            for (String optionName : optionNames) {
                List<String> optionValues = args.getOptionValues(optionName);
                System.out.println("optionName: " + optionName + ", optionValues: " + optionValues);
            }
            List<String> nonOptionArgs = args.getNonOptionArgs();
            System.out.println("nonOptionArgs: " + nonOptionArgs);
        }
    };
}

带双连字符--的参数被称作可选参数(Option Argument),通过getOptionNames方法可以获取命令行参数中的可选参数名称列表,getOptionValues方法可以获取其参数值(可能有多个)。不带连字符的参数被称作非可选参数(Non Option Argument),这些参数只有名称,没有值。使用getNonOptionArgs方法可以获取这些参数。

输出:

optionName: spring.main.web-application-type, optionValues: [servlet]
optionName: spring.profiles.active, optionValues: [dev]
nonOptionArgs: [test]

Banner

打印 Banner 到标准输出:

SpringApplicationBannerPrinter printer = new SpringApplicationBannerPrinter(
    new DefaultResourceLoader(), new SpringBootBanner());
ApplicationEnvironment environment = new ApplicationEnvironment();
printer.print(environment, BannerTests.class, System.out);

可以通过环境属性spring.banner.location,让 Spring 使用指定的文本文件 banner:

environment.getPropertySources().addLast(new MapPropertySource("custom", Map.of("spring.banner.location", "banner1.txt")));
printer.print(environment, BannerTests.class, System.out);

新版本 Spring Boot 已不再支持图片作为 Banner,详情请阅读官方文档。

默认 Banner 中有一个 Spring Boot 版本信息,可以通过以下方式获取:

String version = SpringBootVersion.getVersion();

总结

run 方法的 Spring 源码执行过程分析可以看这个视频。

本文的完整示例代码可以从这里获取。

The End.

参考资料

  • 黑马程序员Spring视频教程,深度讲解spring5底层原理

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: spring
最后更新:2025年7月7日

魔芋红茶

加一点PHP,加一点Go,加一点Python......

点赞
< 上一篇

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复

COPYRIGHT © 2021 icexmoon.cn. ALL RIGHTS RESERVED.
本网站由提供CDN加速/云存储服务

Theme Kratos Made By Seaton Jiang

宁ICP备2021001508号

宁公网安备64040202000141号