Spring Boot 启动主链:main 方法如何成为可接流量的应用
Spring Boot 应用由启动类、Spring 配置、运行依赖和构建插件组成。执行 java -jar 后,JVM 调用入口,Boot 准备配置环境并刷新 ApplicationContext;Web 应用还要启动服务器,随后执行启动任务,最后进入可接收流量状态。
需要实现带参数校验、分页和数据库事务的功能时,可以从Spring Boot 业务接口开发建立工程,再结合启动阶段定位装配与运行问题。
建立一个能响应 HTTP 的应用
依赖、入口与包结构
实验使用 Spring Boot 4.1.1、Java 25 和 Maven 3.9.12。Boot 4.1.1 的最低 Java 版本是 17,支持至 Java 26,要求 Spring Framework 7.0.9 或更新的兼容版本;Servlet 应用支持 Tomcat 11 和 Jetty 12.1,均为 Servlet 6.1。应用应采用 Boot 管理的依赖组合,避免单独提升某个 Spring JAR 后形成混合版本。系统要求
下载完整启动实验,解压为 boot-startup。主要文件的作用如下:
boot-startup/
├── pom.xml 依赖版本、编译目标、可执行 JAR 插件
├── src/main/java/example/startup/
│ └── StartupApplication.java main、Runner、HTTP 接口
├── src/main/resources/
│ └── application.properties 应用默认配置
└── src/test/java/example/startup/
└── StartupTests.java 实际 Context 的成功与失败测试spring-boot-starter-webmvc 引入 MVC 与默认嵌入式 Tomcat;spring-boot-starter-actuator 提供健康与启动步骤端点。父 POM 管理依赖和插件的兼容版本,spring-boot-maven-plugin 把应用重新打包成可执行 JAR。Starter 不负责运行 main,构建插件也不会替代业务配置。构建第一个应用
最小应用可以写成下面这样;实验包在此基础上增加事件输出和 Runner:
package example.startup;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
@RestController
public class StartupApplication {
public static void main(String[] args) {
SpringApplication.run(StartupApplication.class, args);
}
@GetMapping("/hello")
String hello() {
return "hello";
}
}@SpringBootApplication 合并了配置类、自动配置和组件扫描。扫描从声明它的包向下展开,因此入口放在 example.startup,Controller 放在同包或子包。位于相邻包的组件需要明确导入或调整扫描范围;放在默认包会扩大扫描并引入无关类型。应用代码结构
用普通身份构建和启动
下面在 Linux Bash、解压后的目录中执行,需要 Docker Engine、unzip、curl 7.76+,以及有 Docker 使用权限的普通宿主用户。Docker socket 权限可控制宿主容器与挂载,不能当成普通应用权限。
BUILD_IMAGE=maven:3.9.12-eclipse-temurin-25
RUN_IMAGE=eclipse-temurin:25.0.4_7-jdk
PROJECT_DIR="$PWD"
CACHE_DIR="$HOME/.cache/boot-08-maven"
mkdir -p "$CACHE_DIR"
docker pull "$BUILD_IMAGE"
docker pull "$RUN_IMAGE"
docker run --rm --user "$(id -u):$(id -g)" \
-e MAVEN_CONFIG=/tmp/maven \
-v "$PROJECT_DIR:/work" -v "$CACHE_DIR:/cache" -w /work \
"$BUILD_IMAGE" mvn -B -Dmaven.repo.local=/cache clean verifyMaven 应显示测试成功和 BUILD SUCCESS,产物为 target/boot-startup-lab-1.0.0.jar。构建容器显式使用宿主 UID/GID;项目目录、target 和缓存须由该身份可写,Docker daemon 的身份并没有因此改变。Maven 3.9.12 是固定实验版本,可从发行说明核对。
依赖下载失败时先区分 DNS/代理、仓库认证和坐标不存在。企业内网通过只读挂载的 Maven settings.xml 配置受信仓库,再用 -s /config/settings.xml 选择它;离线运行需事先缓存完整依赖和构建插件,加入 -o 验证缓存完整性。镜像可在允许联网的机器通过 docker save 导出、校验传输后 docker load 导入,不在示例中替换为来源不明的加速站。Maven 仓库镜像配置
CONTAINER=boot-startup-lab
docker run -d --name "$CONTAINER" --user 10001:10001 \
--read-only --tmpfs /tmp:rw,nosuid,nodev,size=128m \
--cap-drop ALL --security-opt no-new-privileges \
-p 127.0.0.1:18080:8080 \
-v "$PROJECT_DIR/target/boot-startup-lab-1.0.0.jar:/app.jar:ro" \
"$RUN_IMAGE" java -jar /app.jar
docker logs "$CONTAINER"应用 UID 10001 只需要读取 JAR,临时文件写入 /tmp。等待日志出现 AVAILABILITY ACCEPTING_TRAFFIC 后,在同一宿主终端请求:
curl -q --noproxy '*' --fail-with-body http://127.0.0.1:18080/hello
curl -q --noproxy '*' --fail-with-body http://127.0.0.1:18080/state分别得到 hello、ACCEPTING_TRAFFIC。连接拒绝先看容器是否仍在运行及启动异常;若 Docker 报宿主 18080 被占用,仅更换映射左侧端口,同时修改请求地址。这里使用 HTTP 回环实验,未配置公网鉴权或 TLS。
SpringApplication 怎样组织启动
构造阶段与运行阶段
new SpringApplication(StartupApplication.class) 保存主配置来源,并从 classpath 推断初始 WebApplicationType、发现启动初始化器和监听器。调用 run(args) 后才建立本次运行的 BootstrapContext、ApplicationArguments 与 Environment。环境中的 spring.main.* 可进一步影响启动配置;不能把应用类型推断机械放在 ConfigData 完成之后。SpringApplication 实现
SpringApplication 构造
└─ primarySources、初始 WebApplicationType、initializers、listeners
run(args)
├─ BootstrapContext:只在早期装配需要的对象
├─ Environment:参数、系统环境、ConfigData 等配置来源
├─ ApplicationContext:按应用类型创建,应用初始化器和配置来源
├─ refresh:配置处理、Bean 创建、生命周期;Web 模式启动服务器
├─ ApplicationStartedEvent → LivenessState.CORRECT
├─ ApplicationRunner / CommandLineRunner
└─ ApplicationReadyEvent → ReadinessState.ACCEPTING_TRAFFICEnvironment 必须先形成,因为自动配置条件和 Bean 的构造参数会使用它。ApplicationContextInitializer 得到的是刷新前的 Context,可以调整容器设置或注册定义;此时不应假设普通业务 Bean 已完成初始化。BootstrapContext 支持早期配置基础设施共享对象,它和业务 Context 的关闭及转交过程不同,不适合放置未定义关闭责任的业务连接。
SpringApplicationRunListener 处理 Boot 启动阶段回调;ApplicationListener 接收应用事件。事件未必都经过已经刷新的 Context:ApplicationStartingEvent 和环境准备事件发生时,普通 Bean 监听器尚不可用。实验用 app.addListeners(...) 在 run 前注册,才能观察这些早期事件。事件与监听器
Web 类型改变 Context 与服务器
| 应用类型 | 典型输入 | 容器与监听行为 |
|---|---|---|
| SERVLET | MVC 与 Servlet 运行依赖 | Servlet Web Context,查找 ServletWebServerFactory |
| REACTIVE | WebFlux 运行依赖,且没有 MVC 优先选择条件 | Reactive Web Context,使用反应式服务器 |
| NONE | 非 Web 应用或显式指定 | 普通 Context,不因 Controller 注解而创建监听端口 |
MVC 与 WebFlux 同时存在时,默认选择 MVC;仅仅使用 WebClient 不会把整个应用切换成反应式服务。批处理程序可以使用 --spring.main.web-application-type=none,避免为了客户端依赖而意外启动服务器。显式选 SERVLET 却没有服务器工厂时,Context 会在刷新阶段失败。Web 应用类型
refresh 内部由 Spring Framework 处理 BeanFactory 后处理器、BeanPostProcessor、单例和生命周期。Web Context 在相应阶段创建和启动服务器;它不能被画成全部 Bean 创建完成后才统一绑定端口的外部动作。服务器启动事件只能提供服务器和实际端口,Runner 仍在后面。
区分端口监听、Started 与 Ready
把 Runner 窗口保持到能够观察
实验包的 Runner 在打印 begin 后等待指定毫秒数,允许范围为 0–30000;随后可以成功或抛出固定异常。等待属于演示,不是生产预热实现。
@Bean
ApplicationRunner preparation(Environment environment) {
return args -> {
long delay = environment.getProperty("lab.runner-delay-ms", Long.class, 0L);
if (delay < 0 || delay > 30000) {
throw new IllegalArgumentException("lab.runner-delay-ms must be 0..30000");
}
System.out.println("RUNNER begin");
Thread.sleep(delay);
if (environment.getProperty("lab.fail-runner", Boolean.class, false)) {
throw new IllegalStateException("LAB_RUNNER_FAILED");
}
System.out.println("RUNNER complete");
};
}先关闭上一实例,再以相同 JAR 和端口启动;仅增加 Runner 参数,避免把镜像或配置差异混进比较。
docker stop -t 20 "$CONTAINER"
docker rm "$CONTAINER"
docker run -d --name "$CONTAINER" --user 10001:10001 \
--read-only --tmpfs /tmp:rw,nosuid,nodev,size=128m \
--cap-drop ALL --security-opt no-new-privileges \
-p 127.0.0.1:18080:8080 \
-v "$PROJECT_DIR/target/boot-startup-lab-1.0.0.jar:/app.jar:ro" \
"$RUN_IMAGE" java -jar /app.jar --lab.runner-delay-ms=20000
docker logs -f "$CONTAINER"另开宿主 Bash,在看到 RUNNER begin 后、20 秒窗口结束前执行:
curl -q --noproxy '*' --fail-with-body http://127.0.0.1:18080/state
curl -q --noproxy '*' --fail-with-body http://127.0.0.1:18080/hello如果 /state 已返回 ACCEPTING_TRAFFIC,说明请求落在窗口之后;先停止并删除这个实验容器,再重复本节的 20 秒启动命令。不要把超窗结果解释为 readiness 在启动任务前已经切换。
此时 /state 为 REFUSING_TRAFFIC,但 /hello 仍可返回 hello。readiness 是给平台的状态信号,Boot 不会自动在每个 Controller 前拒绝请求。只有入口或编排平台消费 readiness,并停止路由,新流量才会被摘除。
正常事件的关键相对次序为:
EVENT ApplicationStartedEvent
AVAILABILITY CORRECT
RUNNER begin
RUNNER complete
EVENT ApplicationReadyEvent
AVAILABILITY ACCEPTING_TRAFFICApplicationReadyEvent 后才发送 accepting 状态事件,因此在 ready 监听器内部立即读取状态仍需注意这一顺序。监听器默认同步执行;耗时监听器还会延后后续阶段。若只需要请求处理前的有界准备,ApplicationRunner 比在构造器中进行大量工作更容易区分失败阶段。可用性与 Runner
启动任务需要终点和失败策略
ApplicationRunner 接收已解析的 ApplicationArguments,CommandLineRunner 接收原始字符串数组。多个 Runner 按 Ordered 或 @Order 排序,依赖另一个任务的结果时应显式组织依赖;相同顺序值无法代替业务先后关系。上传文件名等位置参数用 getNonOptionArgs(),--key=value 用 option API 获取,不必再自己切字符串。
数据库迁移、加载小型本地索引等必要准备可以阻止 ready;每个远程操作仍需连接和读取超时。无限重试会让实例长期卡在半启动状态,容器重启也可能重复执行副作用。迁移应由支持并发协调和幂等的迁移工具执行,大型缓存预热可以分批进行;能安全降级的能力不必阻塞整个实例。
日志里的 Started ... 在 Runner 前输出。平台启动预算应覆盖冷启动、Runner 与 readiness 探测的耗时,不以这一行作为放流量条件。启用全局 lazy initialization 则会把部分 Bean 创建推迟到首次调用,启动时间下降可能伴随首次请求变慢或首次请求才失败。延迟初始化
从失败阶段定位到具体原因
Runner 失败与早期配置失败
先停止当前容器,失败实例不使用 --rm,保留退出码和日志:
docker stop -t 20 "$CONTAINER"
docker rm "$CONTAINER"
docker run -d --name "$CONTAINER" --user 10001:10001 \
--read-only --tmpfs /tmp:rw,nosuid,nodev,size=128m \
-v "$PROJECT_DIR/target/boot-startup-lab-1.0.0.jar:/app.jar:ro" \
"$RUN_IMAGE" java -jar /app.jar --lab.fail-runner=true
docker wait "$CONTAINER"
docker logs "$CONTAINER"进程以非零码退出,日志包含 RUNNER begin、LAB_RUNNER_FAILED 和 ApplicationFailedEvent,不会进入 ApplicationReadyEvent。这里外层 main 没有捕获异常并改成成功退出,调度器能识别启动失败。
docker rm "$CONTAINER"
docker run -d --name "$CONTAINER" --user 10001:10001 \
--read-only --tmpfs /tmp:rw,nosuid,nodev,size=128m \
-v "$PROJECT_DIR/target/boot-startup-lab-1.0.0.jar:/app.jar:ro" \
"$RUN_IMAGE" java -jar /app.jar --spring.config.location=file:/missing.properties
docker wait "$CONTAINER"
docker logs "$CONTAINER"这次失败发生在配置环境准备阶段,报告指向不存在的配置资源,Runner 尚未执行。恢复时修正部署挂载和路径,再运行;只有配置确实允许缺失时才使用 optional:,不应为了启动成功把必要配置都改成可选。
FailureAnalyzer 为匹配的异常生成说明和修复提示,完整 cause 仍是定位依据。端口冲突、绑定失败与 Bean 初始化异常的处理方法不同。早期日志尚未完整初始化时保留 stderr;不要在诊断输出里打印密码、完整环境变量或带凭证的连接串。启动失败诊断
启动慢时观测正在执行的工作
先删除已退出的失败容器,再按第一节完整启动命令运行不带失败参数的正常实例,等待 accepting;PROJECT_DIR、RUN_IMAGE 和端口映射沿用第一节。实验的 BufferingApplicationStartup 保存最多 512 条 startup steps。读取启动端点:
curl -q --noproxy '*' --fail-with-body \
http://127.0.0.1:18080/actuator/startup响应含步骤名称、起止信息和父步骤标识,可查看配置处理和 Bean 实例化耗时。该端点只适用于已配置记录器的应用;通常需要等待能接收 HTTP 后读取,因此进程仍卡在早期阶段时,应先取线程栈。JAR 内应用线程全部等待 DNS 或远程连接时,增加 Bean 扫描优化没有作用。启动步骤端点
| 首个明显现象 | 首查对象 | 修复与复测 |
|---|---|---|
| 找不到配置资源 | 启动参数、挂载路径与文件权限 | 改正确路径,原命令重启;应进入环境准备后的事件 |
| Bean 创建异常 | 最深 cause、注入类型、条件报告 | 修正定义或依赖;用相同 Profile 重跑 Context 测试 |
| 端口占用 | Docker 发布端口或容器内 BindException | 确认端口所有者,再修改映射/监听;真实 curl 复测 |
| Started 后一直无 ready | Runner 和同步事件监听器线程栈 | 去掉无界等待,给外部调用设置超时;观察 complete 和 accepting |
| 首次请求才出现创建异常 | lazy Bean 与请求触发的依赖 | 对关键 Bean 禁用 lazy 或提前验证;请求路径复跑 |
在正在运行的实验容器中执行 docker exec --user 10001:10001 "$CONTAINER" jcmd 1 Thread.print -l 可查看线程;它要求目标仍存活、相同 UID 和可访问的临时目录。这个命令不能在失败进程已经退出后再执行。
SpringApplication.exit(context) 会关闭 Context 并汇总 ExitCodeGenerator 的结果,但 JVM 进程退出由最外层 System.exit(code) 决定。Web 服务平时通过 SIGTERM 触发 shutdown hook,关闭服务器及 Spring 管理资源;库和 Starter 不应擅自终止整个 JVM。应用退出
实验结束后只删除本次容器,源码、JAR 和 Maven 缓存保留,方便重复运行:
docker stop -t 20 "$CONTAINER"
docker rm "$CONTAINER"权威资料与规范地址
运行要求、启动扩展和诊断 API 可从以下入口查阅。
