一句话定位:
SpringApplication是 Spring Boot 应用的启动引擎,run()方法 orchestrates 整个应用生命周期——从推断应用类型、加载环境、创建容器到刷新上下文,最终将一个普通的main()方法转化为一个可对外提供服务的 Spring 应用。
定义与作用
SpringApplication 是 org.springframework.boot 包下的核心类,负责引导(bootstrap)整个 Spring Boot 应用的启动过程。它的 run() 方法不是简单的 "new 一个容器",而是一个经过精心编排的多阶段启动流水线。
它解决的痛点:
| 传统 Spring Framework 启动 | SpringApplication.run() 的改进 |
|---|---|
手动创建 ApplicationContext 子类(如 AnnotationConfigApplicationContext) | 自动推断应用类型(Servlet / Reactive / None),创建最合适的上下文实现 |
手动加载 applicationContext.xml 或扫描 @Configuration 类 | 自动推断主类,自动加载组件扫描和自动配置 |
手动注册 ServletContextListener 和 DispatcherServlet | 自动创建并配置内嵌 Web 服务器,注册 DispatcherServlet |
| 手动处理命令行参数和配置文件 | 统一准备 Environment,合并命令行参数、系统属性、环境变量、application.yml |
| 无统一的启动生命周期扩展点 | 提供 ApplicationContextInitializer、ApplicationListener、ApplicationRunner 等标准扩展点 |
适用位置与常用属性
SpringApplication 是一个类,不是注解。它在项目中唯一出现的位置是主启动类的 main() 方法。
适用位置
package com.feixiang.student;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class StudentManagementApplication {
public static void main(String[] args) {
// 唯一、标准的调用方式
SpringApplication.run(StudentManagementApplication.class, args);
}
}
进阶用法:自定义 SpringApplication 实例
package com.feixiang.student;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class StudentManagementApplication {
public static void main(String[] args) {
// 操作前:标准写法,无法定制启动行为
// SpringApplication.run(StudentManagementApplication.class, args);
// 操作后:自定义 SpringApplication,控制 Banner、懒初始化等
SpringApplication application = new SpringApplication(StudentManagementApplication.class);
application.setBannerMode(Banner.Mode.OFF); // 关闭 Banner
application.setLazyInitialization(true); // 开启全局懒初始化
application.setAdditionalProfiles("dev"); // 预设激活的 Profile
application.run(args);
}
}
| 属性/方法 | 说明 | 典型场景 |
|---|---|---|
setBannerMode(Banner.Mode.OFF) | 关闭启动 Banner | CI/CD 环境减少日志噪音 |
setLazyInitialization(true) | 全局懒初始化 Bean | 开发环境加速启动,非所有 Bean 立即创建 |
setAdditionalProfiles(String...) | 预设激活的 Profile | 测试环境强制激活 dev Profile |
setDefaultProperties(Properties) | 设置默认属性 | 为未配置项提供应用级默认值 |
setApplicationContextClass(Class) | 强制指定上下文类型 | 极罕见,通常由自动推断处理 |
addListeners(ApplicationListener...) | 添加应用监听器 | 注册自定义的 ApplicationReadyEvent 监听器 |
addInitializers(ApplicationContextInitializer...) | 添加上下文初始化器 | 在容器刷新前修改 ConfigurableEnvironment |
核心原理
SpringApplication.run() 的 12 阶段启动流程
阶段详解:
| 阶段 | 核心动作 | 对开发者的意义 |
|---|---|---|
| 1. 创建实例 | 调用 new SpringApplication(primarySources) | 可在此之前通过构造方法定制属性 |
| 2. 推断应用类型 | 检测 classpath 是否存在 spring-webmvc、spring-webflux | 决定创建哪种 ApplicationContext 实现 |
| 3-4. 加载扩展 | 从 spring.factories 读取 ApplicationContextInitializer 和 ApplicationListener | 框架级扩展点,开发者可自定义 |
| 5. 推断主类 | 通过堆栈反推哪个类调用了 run() | 决定 @ComponentScan 的默认根包 |
| 6. 准备环境 | 加载 application.yml、命令行参数、系统属性、环境变量 | 外部化配置在此阶段合并到 Environment |
| 7. 打印 Banner | 输出 :: Spring Boot :: (v2.7.18) | 可自定义 banner.txt 或关闭 |
| 8-9. 创建与预处理 | 实例化 ApplicationContext,执行初始化器 | 初始化器可在 Bean 加载前修改上下文状态 |
| 10. 刷新上下文 | 调用 refresh(),执行自动配置、条件评估、Bean 实例化 | 最核心的阶段,自动配置在此发生 |
| 11-12. 后处理与 Runner | 调用 afterRefresh,执行 ApplicationRunner | 启动后初始化逻辑(如预加载缓存)放在此处 |
| 13. 发布就绪事件 | ApplicationReadyEvent | 监听器可在此执行启动后的异步初始化 |
启动流程时序图(简化版)
完整示例
场景说明
飞翔科技的学生成绩管理系统启动后,架构师白歌希望小崔在控制台输出一条自定义的启动完成消息,并记录启动耗时。小崔通过自定义 SpringApplication 和 ApplicationRunner 来实现。
操作前:标准启动方式(无法定制)
package com.feixiang.student;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class StudentManagementApplication {
public static void main(String[] args) {
// 标准写法:无法关闭 Banner、无法预设 Profile、无法自定义启动后逻辑
SpringApplication.run(StudentManagementApplication.class, args);
}
}
启动日志(默认 Banner 输出):
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.7.18)
... 正常启动日志 ...
小崔的需求:关闭 Banner 减少 CI 日志噪音;启动后输出 "学生成绩管理系统已就绪";记录启动耗时。
使用自定义 SpringApplication 的完整代码
主启动类:
package com.feixiang.student;
import org.springframework.boot.Banner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class StudentManagementApplication {
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
SpringApplication application = new SpringApplication(StudentManagementApplication.class);
application.setBannerMode(Banner.Mode.OFF); // 关闭 Banner
application.setAdditionalProfiles("dev"); // 预设 dev 环境
application.run(args);
long cost = System.currentTimeMillis() - startTime;
System.out.println("[飞翔科技] 学生成绩管理系统启动完成,耗时:" + cost + "ms");
}
}
启动后逻辑:使用 ApplicationRunner(替代 main 中手动计时,更标准):
package com.feixiang.student.config;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
@Component
public class StartupReportRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("========================================");
System.out.println(" 广州飞翔科技 - 学生成绩管理系统");
System.out.println(" 系统状态:已就绪");
System.out.println(" 监听端口:8080");
System.out.println(" 数据库 :student_db");
System.out.println("========================================");
}
}
启动日志(关闭 Banner 后):
2024-05-20 09:25:10.123 INFO 12345 --- [main] c.f.s.StudentManagementApplication : Starting StudentManagementApplication using Java 1.8
2024-05-20 09:25:10.456 INFO 12345 --- [main] c.f.s.StudentManagementApplication : The following profiles are active: dev
2024-05-20 09:25:11.789 INFO 12345 --- [main] o.s.b.w.e.t.TomcatWebServer : Tomcat started on port(s): 8080 (http)
2024-05-20 09:25:11.890 INFO 12345 --- [main] c.f.s.StudentManagementApplication : Started in 1.876 seconds
========================================
广州飞翔科技 - 学生成绩管理系统
系统状态:已就绪
监听端口:8080
数据库 :student_db
========================================
操作后运行结果及分析
分析:
- Banner 关闭:
setBannerMode(Banner.Mode.OFF)生效,Spring Boot 的 ASCII 艺术 Banner 不再输出,CI 日志更干净。 - Profile 预设:
setAdditionalProfiles("dev")使application-dev.yml的配置被激活,开发环境数据库连接自动生效。 - 启动后逻辑:
ApplicationRunner的run()方法在容器刷新完成后、应用就绪前执行,适合执行预加载缓存、初始化定时任务等启动后初始化操作。它与CommandLineRunner的区别在于参数封装——ApplicationRunner接收封装后的ApplicationArguments,而CommandLineRunner接收原始String[]。 - 启动事件:
ApplicationReadyEvent在ApplicationRunner执行后发布,如果需要在启动完成后执行异步操作(如发送启动通知到钉钉),应监听此事件。
易错场景与面试考点
易错场景:在 main() 中写业务逻辑,期望容器已就绪
小崔曾犯过这样的错误:他想在 main() 中直接调用 Service 方法查询学生成绩。
错误示范:
package com.feixiang.student;
import com.feixiang.student.service.StudentService;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
@SpringBootApplication
public class StudentManagementApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context =
SpringApplication.run(StudentManagementApplication.class, args);
// 错误:虽然此时容器已创建,但启动流程尚未完成
// 如果此时调用 Bean 执行复杂操作,可能遇到未完全初始化的组件
StudentService service = context.getBean(StudentService.class);
System.out.println(service.queryScore("2024001"));
// 更严重:获取 Bean 后直接关闭上下文,Runner 和 ReadyEvent 都没执行
context.close();
}
}
后果:
- 虽然
SpringApplication.run()返回的ApplicationContext已经刷新,但ApplicationRunner和ApplicationReadyEvent尚未执行。如果业务逻辑依赖这些启动后初始化(如缓存预热),会直接失败。 - 手动
context.close()导致应用启动后立即关闭,Tomcat 尚未接受请求就终止。
正确做法:所有启动后业务初始化逻辑应放在 ApplicationRunner 或 ApplicationReadyEvent 监听器中,而非 main() 中直接写业务代码。
面试考点
Q:SpringApplication.run() 内部做了哪些关键步骤?
① 创建
SpringApplication实例;② 推断应用类型(Servlet / Reactive / None);③ 加载ApplicationContextInitializer和ApplicationListener;④ 推断主类;⑤ 准备Environment(合并命令行参数、系统属性、环境变量、application.yml);⑥ 打印 Banner;⑦ 创建ApplicationContext;⑧ 预处理上下文;⑨ 刷新上下文(核心:自动配置 + Bean 加载);⑩ 后处理;⑪ 调用ApplicationRunner/CommandLineRunner;⑫ 发布ApplicationReadyEvent。
Q:ApplicationRunner 和 CommandLineRunner 有什么区别?
两者都在容器刷新完成后、应用就绪前执行。区别:
ApplicationRunner接收ApplicationArguments(封装后的参数,支持--key=value和key=value解析);CommandLineRunner接收原始String[] args。如果项目中同时存在两者,可通过@Order或实现Ordered接口控制执行顺序。
Q:如何关闭 Spring Boot 的 Banner?
方式一:
SpringApplication.setBannerMode(Banner.Mode.OFF);方式二:application.yml中配置spring.main.banner-mode=off;方式三:在resources目录下放置自定义banner.txt替换默认图案(支持${spring-boot.version}等占位符)。
Q:Spring Boot 推断应用类型的依据是什么?
检测 classpath 中是否存在
spring-webmvc(存在则为 SERVLET)、spring-webflux(存在则为 REACTIVE)。如果两者都存在,SERVLET 优先。如果两者都不存在,则为 NONE(非 Web 应用,如批处理或命令行工具)。