Skip to content

Spring Boot 从零到生产级掌握 ​

Spring Boot 不能只理解成“少写 XML”或“快速启动项目”。真正的商业后端项目里,Spring Boot 负责把 Spring IOC、Web 容器、配置体系、自动配置、日志、监控、健康检查、打包部署和扩展点组织成一个可运行、可维护、可排查的应用。

一句话建立主线:

Spring Boot = Spring 应用的工程化启动器。它用 Starter 管依赖,用自动配置装配默认 Bean,用配置体系适配环境,用内嵌容器承载 Web 请求,用 Actuator 和日志支撑生产排查。

学习目标 ​

学完这一页,你要能做到:

  1. 解释 Spring Boot 和 Spring 的关系。
  2. 解释 @SpringBootApplication 为什么能启动应用。
  3. 解释 SpringApplication.run 每个关键步骤做什么。
  4. 解释 Starter、自动配置、条件注解、BeanDefinition 的关系。
  5. 区分 Boot 2.6及更早、Boot 2.7过渡期和 Boot 3 的自动配置候选注册机制。
  6. 解释配置文件、环境变量、命令行参数、Profile 的优先级思路。
  7. 解释内嵌 Tomcat 什么时候创建,如何接入 DispatcherServlet。
  8. 解释一个 HTTP 请求在 Boot 应用里的完整链路。
  9. 说清常见扩展点分别在什么阶段使用。
  10. 能设计统一异常、参数校验、日志 traceId、健康检查和业务指标。
  11. 能排查自动配置不生效、端口占用、启动慢、连接池耗尽、接口 404/500。

如果你已经读完主线,但还不知道怎么把自动配置、Starter、配置属性、条件注解、健康检查、指标和失败诊断落到项目里,继续做:Spring Boot 商业场景训练营。

学习路线 ​

mermaid
flowchart TD
    A["Spring 基础<br/>IOC、AOP、事务"] --> B["Boot 启动类<br/>@SpringBootApplication"]
    B --> C["Starter<br/>依赖和版本管理"]
    C --> D["自动配置<br/>条件注解和默认 Bean"]
    D --> E["配置体系<br/>yml、Profile、环境变量"]
    E --> F["Web 应用<br/>Tomcat、MVC、JSON"]
    F --> G["工程治理<br/>日志、异常、校验、Actuator"]
    G --> H["扩展点<br/>启动、Bean、Web、监控"]
    H --> I["部署排查<br/>jar、Docker、K8s、线上问题"]

这条路线不能乱。还不懂 Spring IOC 就直接背自动配置,会觉得“Boot 很玄”;还不懂配置体系就写 Starter,会把参数写死;还不懂 Actuator 和日志,上线后就只能看报错猜问题。

第一步:Spring Boot 解决了什么问题 ​

传统 Spring Web 项目通常需要:

  1. 自己管理 Spring、Spring MVC、Jackson、Tomcat、日志、数据库驱动等依赖版本。
  2. 配置 DispatcherServlet、组件扫描、MVC 映射、消息转换器。
  3. 手动部署到外部 Tomcat。
  4. 自己处理多环境配置。
  5. 自己补健康检查和指标。

Spring Boot 把这些常见问题收敛成约定:

能力Spring Boot 做什么不这样会怎样
Starter聚合一类场景的依赖版本冲突、缺包、配置分散
自动配置按 classpath 和条件注册默认 Bean每个项目重复配置
内嵌容器jar 直接启动 Web 服务依赖外部 Tomcat 部署
配置体系支持 yml、Profile、环境变量多环境靠改代码
Actuator暴露健康、指标、线程等信息上线后不可观测
约定优于配置常见场景有默认值新项目启动成本高

Spring Boot 不是替代 Spring,而是把 Spring 的能力做成生产友好的应用框架。

第二步:最小应用为什么能启动 ​

最小代码:

java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

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

@SpringBootApplication 是组合注解,可以拆成三层理解:

注解作用
@SpringBootConfiguration说明当前类是配置类,本质上也是 Spring 配置
@EnableAutoConfiguration开启自动配置,导入自动配置候选类
@ComponentScan从启动类所在包向下扫描组件

如果启动类放错包,比如放在 com.demo.web,而 Service 在 com.demo.service 的兄弟包下,默认扫描不到 Service。正确做法通常是把启动类放在根包:

text
com.company.project
  DemoApplication.java
  controller
  service
  mapper
  config

第三步:SpringApplication.run 做了什么 ​

简化流程:

mermaid
flowchart TD
    A["创建 SpringApplication"] --> B["推断应用类型<br/>Servlet / Reactive / 非 Web"]
    B --> C["加载监听器和初始化器"]
    C --> D["准备 Environment<br/>配置、Profile、参数"]
    D --> E["创建 ApplicationContext"]
    E --> F["加载启动类和自动配置"]
    F --> G["refresh 容器<br/>创建 Bean"]
    G --> H["启动内嵌 WebServer"]
    H --> I["执行 Runner"]
    I --> J["发布启动完成事件"]

关键点:

步骤为什么重要
准备 Environment条件注解、配置绑定、Profile 都依赖它
创建 ApplicationContextBoot 最终仍然是 Spring 容器
加载自动配置默认 Web、JSON、数据源、事务等能力来自这里
refreshBeanDefinition 变成真正 Bean
启动 WebServerTomcat/Jetty/Undertow 开始监听端口
Runner容器可用后做启动检查或轻量预热

第四步:Starter 为什么能开箱即用 ​

Starter 本身通常不写大量业务代码,它的价值是“把某类场景依赖聚合起来”。

例如 spring-boot-starter-web 通常会带来:

  1. Spring MVC。
  2. Jackson JSON。
  3. 校验相关依赖。
  4. 内嵌 Tomcat。
  5. Web 自动配置所需依赖。
mermaid
flowchart TD
    A["业务项目引入 starter"] --> B["Maven/Gradle 拉取依赖"]
    B --> C["classpath 出现相关类"]
    C --> D["自动配置条件匹配"]
    D --> E["注册默认 Bean"]
    E --> F["业务可以直接使用 Web 能力"]

Starter + 自动配置才是完整开箱即用。如果只有依赖没有自动配置,业务仍然要手动声明大量 Bean。

第五步:自动配置不是魔法 ​

自动配置的核心公式:

text
候选自动配置类 + 条件注解 + 默认 Bean + 用户 Bean 优先 = 自动配置

简化流程:

mermaid
flowchart TD
    A["@EnableAutoConfiguration"] --> B["AutoConfigurationImportSelector"]
    B --> C["读取自动配置候选类"]
    C --> D["排除、去重、排序"]
    D --> E["解析条件注解"]
    E --> F{"条件是否匹配"}
    F -- "匹配" --> G["注册 BeanDefinition"]
    F -- "不匹配" --> H["跳过该配置"]
    G --> I["refresh 阶段创建 Bean"]

常见条件注解:

注解作用
@ConditionalOnClassclasspath 有某个类才生效
@ConditionalOnMissingClassclasspath 没有某个类才生效
@ConditionalOnBean容器里已有某个 Bean 才生效
@ConditionalOnMissingBean容器里没有某个 Bean 才创建默认 Bean
@ConditionalOnProperty配置项满足条件才生效
@ConditionalOnWebApplicationWeb 应用才生效

Boot 2 和 Boot 3 的区别 ​

版本自动配置声明方式
Spring Boot 2.6及更早常见 META-INF/spring.factories
Spring Boot 2.7已支持并推荐 AutoConfiguration.imports,仍兼容旧候选注册方式
Spring Boot 3使用 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

Boot 2 示例:

properties
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.demo.DemoAutoConfiguration

Boot 3 示例:

text
com.example.demo.DemoAutoConfiguration

面试时不要说“spring.factories 一定错了”。老项目、老版本和部分兼容场景仍可能看到它。

第六步:自定义 Starter 的最小 Demo ​

目标:引入一个 starter 后,自动注册一个 TraceIdGenerator。

自动配置类:

java
package com.example.trace.autoconfigure;

import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;

@AutoConfiguration
@EnableConfigurationProperties(TraceProperties.class)
@ConditionalOnProperty(prefix = "demo.trace", name = "enabled", havingValue = "true", matchIfMissing = true)
public class TraceAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public TraceIdGenerator traceIdGenerator(TraceProperties properties) {
        return new TraceIdGenerator(properties.getPrefix());
    }
}

配置属性:

java
package com.example.trace.autoconfigure;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "demo.trace")
public class TraceProperties {
    private boolean enabled = true;
    private String prefix = "trace";

    public boolean isEnabled() {
        return enabled;
    }

    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }

    public String getPrefix() {
        return prefix;
    }

    public void setPrefix(String prefix) {
        this.prefix = prefix;
    }
}

业务 Bean:

java
package com.example.trace.autoconfigure;

import java.util.UUID;

public class TraceIdGenerator {
    private final String prefix;

    public TraceIdGenerator(String prefix) {
        this.prefix = prefix;
    }

    public String nextId() {
        return prefix + "-" + UUID.randomUUID();
    }
}

Boot 3 自动配置声明文件:

text
# META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.example.trace.autoconfigure.TraceAutoConfiguration

业务项目配置:

yaml
demo:
  trace:
    enabled: true
    prefix: order

为什么要加 @ConditionalOnMissingBean:

  1. 公共 starter 提供默认实现。
  2. 业务项目如果自己声明 TraceIdGenerator,默认实现自动让位。
  3. 否则会出现 Bean 冲突,或者业务无法替换默认逻辑。

第七步:配置体系怎么工作 ​

Spring Boot 配置来源很多:

mermaid
flowchart TD
    A["默认配置"] --> B["application.yml"]
    B --> C["application-{profile}.yml"]
    C --> D["环境变量"]
    D --> E["JVM 参数"]
    E --> F["命令行参数"]
    F --> G["配置中心"]

不同版本和场景优先级细节很多,零基础先抓住原则:

  1. 越外部的配置通常越适合覆盖内部默认值。
  2. 越靠近本次启动命令的配置通常优先级越高。
  3. Profile 用来区分环境,不要把 dev、test、prod 混在一起。
  4. 密码、密钥不要提交到仓库。

@Value 和 @ConfigurationProperties ​

方式适合
@Value("${demo.timeout}")少量简单配置
@ConfigurationProperties一组配置、嵌套配置、类型绑定、校验

推荐写法:

java
import jakarta.validation.constraints.Min;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;

@Validated
@ConfigurationProperties(prefix = "collect")
public class CollectProperties {
    @Min(1)
    private int threadSize = 8;
    private int timeoutSeconds = 10;

    public int getThreadSize() {
        return threadSize;
    }

    public void setThreadSize(int threadSize) {
        this.threadSize = threadSize;
    }

    public int getTimeoutSeconds() {
        return timeoutSeconds;
    }

    public void setTimeoutSeconds(int timeoutSeconds) {
        this.timeoutSeconds = timeoutSeconds;
    }
}

配置绑定失败要尽早失败,而不是上线后线程池参数变成默认值才发现。

第八步:内嵌 Tomcat 如何启动 ​

Servlet Web 应用中,Boot 会创建 ServletWebServerApplicationContext。在容器刷新阶段,它会通过 ServletWebServerFactory 创建并启动 Tomcat。

mermaid
flowchart TD
    A["refresh ApplicationContext"] --> B["创建 ServletWebServerFactory"]
    B --> C["创建 Tomcat WebServer"]
    C --> D["注册 DispatcherServlet"]
    D --> E["注册 Filter、Listener、Servlet"]
    E --> F["Tomcat 监听端口"]
    F --> G["接收 HTTP 请求"]

如果端口被占用:

text
Web server failed to start. Port 8080 was already in use.

排查:

bash
netstat -ano | findstr 8080

然后处理占用进程,或修改:

yaml
server:
  port: 8081

第九步:一次 HTTP 请求怎么走 ​

mermaid
flowchart TD
    A["浏览器 / 前端 / 调用方"] --> B["Tomcat Connector"]
    B --> C["Filter 链"]
    C --> D["DispatcherServlet"]
    D --> E["HandlerMapping 找 Controller"]
    E --> F["参数解析和校验"]
    F --> G["Controller 方法"]
    G --> H["Service 事务和业务"]
    H --> I["Mapper / Repository"]
    I --> J["数据库 / Redis / MQ"]
    J --> I
    I --> H
    H --> K["返回 DTO"]
    K --> L["JSON 序列化"]
    L --> M["HTTP 响应"]

各环节常见问题:

环节常见问题
FiltertraceId 丢失、跨域、编码、认证异常
HandlerMapping路径不匹配导致 404
参数解析JSON 格式错、日期格式错、枚举转换失败
参数校验必填缺失、范围不合法
Controller把业务和 SQL 全写在接口层
Service事务边界不清,自调用导致事务失效
Mapper慢 SQL、N+1、连接池耗尽
JSON 序列化循环引用、时间格式不一致、字段泄露

第十步:Web 工程必须补齐的基础能力 ​

统一返回 ​

java
public record ApiResult<T>(int code, String message, T data) {
    public static <T> ApiResult<T> ok(T data) {
        return new ApiResult<>(0, "ok", data);
    }

    public static <T> ApiResult<T> fail(int code, String message) {
        return new ApiResult<>(code, message, null);
    }
}

参数校验 ​

java
import jakarta.validation.constraints.NotBlank;

public record CreateAssetRequest(
    @NotBlank(message = "资产编码不能为空")
    String assetCode,
    @NotBlank(message = "资产名称不能为空")
    String name
) {
}

Controller:

java
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/assets")
public class AssetController {

    @PostMapping
    public ApiResult<String> create(@Valid @RequestBody CreateAssetRequest request) {
        return ApiResult.ok("created:" + request.assetCode());
    }
}

统一异常 ​

java
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ApiResult<Void> handleValidException(MethodArgumentNotValidException ex) {
        String message = ex.getBindingResult().getFieldErrors().get(0).getDefaultMessage();
        return ApiResult.fail(400, message);
    }

    @ExceptionHandler(Exception.class)
    public ApiResult<Void> handleException(Exception ex) {
        return ApiResult.fail(500, "系统异常");
    }
}

为什么不能每个 Controller 自己 try-catch:

  1. 响应格式会不统一。
  2. 日志会分散。
  3. 错误码不可治理。
  4. 安全信息可能被直接返回给前端。

第十一步:常见扩展点怎么选 ​

扩展点阶段适合做什么
EnvironmentPostProcessorEnvironment 准备阶段配置解密、额外配置源
ApplicationContextInitializerContext refresh 前初始化上下文属性
BeanDefinitionRegistryPostProcessorBeanDefinition 阶段动态注册 BeanDefinition
BeanFactoryPostProcessorBean 创建前修改 BeanDefinition 属性
BeanPostProcessorBean 初始化前后代理、增强、注入特殊逻辑
ApplicationRunner启动完成后启动检查、轻量预热
FilterServlet 链路最前traceId、CORS、认证前置处理
HandlerInterceptorSpring MVC 链路权限、审计、接口日志
HandlerMethodArgumentResolverController 参数解析注入当前用户、租户信息
HealthIndicatorActuator 健康检查检查下游依赖状态
MeterBinder指标注册暴露业务指标
FailureAnalyzer启动失败诊断输出更友好的失败原因

扩展点选择原则:

mermaid
flowchart TD
    A["想扩展 Boot"] --> B{"影响配置读取吗"}
    B -- "是" --> C["EnvironmentPostProcessor"]
    B -- "否" --> D{"影响 Bean 定义吗"}
    D -- "是" --> E["BeanFactory / Registry 后置处理器"]
    D -- "否" --> F{"影响请求链路吗"}
    F -- "是" --> G["Filter / Interceptor / ArgumentResolver"]
    F -- "否" --> H{"影响监控诊断吗"}
    H -- "是" --> I["HealthIndicator / MeterBinder / FailureAnalyzer"]
    H -- "否" --> J["Runner 或普通 Bean"]

不要为了“高级”乱用扩展点。能用普通 Bean 解决,就不要上早期生命周期扩展点;否则排查成本会很高。

第十二步:Actuator 和生产可观测 ​

Actuator 常用能力:

端点作用
/actuator/health健康检查
/actuator/metrics指标
/actuator/env环境属性
/actuator/beansBean 列表
/actuator/conditions自动配置条件报告
/actuator/threaddump线程栈
/actuator/loggers日志级别

生产环境不要把所有端点裸露出来。建议:

  1. 只暴露必要端点。
  2. 管理端口和业务端口分离。
  3. 接入 Spring Security 或网关权限。
  4. 禁止对公网暴露敏感端点。
  5. 指标进入 Prometheus、日志进入 ELK 或 Loki。

自定义健康检查:

java
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;

@Component
public class CollectHealthIndicator implements HealthIndicator {
    @Override
    public Health health() {
        boolean ok = true;
        if (ok) {
            return Health.up().withDetail("collect", "ready").build();
        }
        return Health.down().withDetail("collect", "not ready").build();
    }
}

第十三步:部署和运行 ​

jar 启动 ​

bash
java -jar app.jar --spring.profiles.active=prod --server.port=8080

JVM 参数 ​

bash
java -Xms512m -Xmx512m -jar app.jar

Dockerfile 示例 ​

dockerfile
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY target/app.jar /app/app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

生产注意:

  1. 配置不要打死在镜像里。
  2. 日志输出到标准输出或统一目录。
  3. 健康检查要接入探针。
  4. JVM 内存要结合容器限制设置。
  5. 启动参数、Profile、密钥由环境注入。

线上排查总流程 ​

mermaid
flowchart TD
    A["Spring Boot 应用异常"] --> B{"启动失败还是运行异常"}
    B -- "启动失败" --> C["看启动日志和 FailureAnalyzer"]
    C --> D["检查端口、配置、Bean、自动配置条件"]
    B -- "运行异常" --> E{"接口慢、报错还是不可用"}
    E -- "接口慢" --> F["看日志 traceId、线程、DB、Redis、下游"]
    E -- "接口报错" --> G["看异常栈、参数、业务状态"]
    E -- "不可用" --> H["看 health、连接池、线程池、GC"]
    D --> I["修复配置或依赖"]
    F --> J["定位瓶颈并限流/优化"]
    G --> K["修复代码或数据"]
    H --> L["扩容、降级或重启恢复"]

自动配置不生效 ​

排查顺序:

  1. 是否引入了正确 starter。
  2. 自动配置类是否在候选列表。
  3. 是否被 exclude。
  4. 条件注解是否匹配。
  5. 配置项是否写错。
  6. 是否已有用户 Bean 导致默认 Bean 不创建。
  7. Bean 创建是否失败。

打开调试:

yaml
debug: true

或者看:

text
/actuator/conditions

启动慢 ​

常见原因:

原因排查
Bean 初始化慢看启动日志、临时增加耗时统计
外部连接慢数据库、Redis、MQ、注册中心超时
扫描范围太大检查启动类包路径和组件扫描
Runner 做重活把长任务异步化或延后执行
日志阻塞检查日志输出和磁盘

接口 404 ​

检查:

  1. Controller 是否被扫描。
  2. 路径和 HTTP 方法是否正确。
  3. context-path 是否配置。
  4. 网关是否转发到正确路径。
  5. 是否被 Security 拦截后表现为 404。

接口 500 ​

检查:

  1. 全局异常日志有没有完整 traceId。
  2. 请求参数是否符合格式。
  3. Service 是否抛业务异常。
  4. 数据库是否唯一键冲突、锁等待、连接超时。
  5. 下游接口是否超时。

商业场景落地 ​

医疗数据采集平台 ​

Spring Boot 可以这样落地:

能力用法
配置体系医院接口地址、采集线程数、超时时间按环境配置
自动配置数据源、Redis、MQ、Web、JSON 默认装配
扩展点HandlerInterceptor 做租户和操作审计
Actuator暴露健康检查给 K8s 或网关
统一异常采集失败返回统一错误码,日志保留 traceId
配置属性CollectProperties 管采集批次、并发、重试
Runner启动后做轻量依赖检查,不做耗时采集

订单系统 ​

Spring Boot 可以这样落地:

  1. Controller 只做参数校验和接口编排入口。
  2. Service 管事务边界。
  3. Mapper 只负责 SQL。
  4. 支付、库存、消息客户端做成配置化 Bean。
  5. 订单状态流转失败时统一异常和日志。
  6. Actuator 指标暴露订单创建 TPS、失败数、下游耗时。

面试标准回答 ​

Spring Boot 是什么 ​

text
Spring Boot 是基于 Spring 的应用工程化框架。Spring 提供 IOC、AOP、事务、MVC 等基础能力,Spring Boot 通过 Starter、自动配置、配置体系、内嵌 Web 容器和 Actuator,把这些能力组织成开箱即用、可部署、可观测的应用。它不是替代 Spring,而是降低 Spring 项目的配置、依赖和生产治理成本。

自动配置原理 ​

text
自动配置不是魔法。Spring Boot 通过 @EnableAutoConfiguration 导入 AutoConfigurationImportSelector,读取自动配置候选类。Boot 2.6及更早常见 spring.factories,Boot 2.7 已支持并推荐 AutoConfiguration.imports,Boot 3 使用 imports 机制。候选配置类再通过 @ConditionalOnClass、@ConditionalOnMissingBean、@ConditionalOnProperty 等条件注解决定是否注册 BeanDefinition,最后仍然由 Spring IOC 在 refresh 阶段创建 Bean。

Spring Boot 启动流程 ​

text
Spring Boot 启动大致是:创建 SpringApplication,推断应用类型,加载监听器和初始化器,准备 Environment,创建 ApplicationContext,加载启动类和自动配置,refresh 容器创建 Bean,Servlet Web 应用再启动内嵌 Tomcat,最后执行 Runner 并发布启动完成事件。

自定义 Starter 怎么设计 ​

text
自定义 Starter 通常拆成 starter 和 autoconfigure。starter 负责聚合依赖,autoconfigure 提供自动配置类、配置属性类和默认 Bean。自动配置要用条件注解控制生效,用 @ConfigurationProperties 承载外部配置,用 @ConditionalOnMissingBean 尊重业务自定义,并通过 AutoConfiguration.imports 或 spring.factories 声明。

关联知识点 ​

知识点继续学习
自动配置与扩展点全过程自动配置深度页
自动配置原理自动配置原理
Starter 机制Starter机制
配置体系配置体系
启动原理启动原理
扩展点Spring Boot扩展点
ActuatorActuator监控
生产监控与线上排查线上问题排查Runbook
Spring Boot Admin多实例集中监控与管理
Spring IOCSpring IOC
Spring 事务Spring事务
Spring Boot 面试面试题

本章小结 ​

Spring Boot 要学到生产级,必须把启动流程、自动配置、Starter、配置绑定、内嵌 Web 容器、请求链路、扩展点、Actuator、部署和排查串起来。只会写 Controller 不是掌握 Boot;能解释“为什么引入一个 starter 就有默认能力、为什么自定义 Bean 后默认配置让位、为什么配置能绑定、为什么 Tomcat 能随 jar 启动、为什么 Actuator 能定位问题”,才是真正理解。