Spring Boot 从零到生产级掌握
Spring Boot 不能只理解成“少写 XML”或“快速启动项目”。真正的商业后端项目里,Spring Boot 负责把 Spring IOC、Web 容器、配置体系、自动配置、日志、监控、健康检查、打包部署和扩展点组织成一个可运行、可维护、可排查的应用。
一句话建立主线:
Spring Boot = Spring 应用的工程化启动器。它用 Starter 管依赖,用自动配置装配默认 Bean,用配置体系适配环境,用内嵌容器承载 Web 请求,用 Actuator 和日志支撑生产排查。
学习目标
学完这一页,你要能做到:
- 解释 Spring Boot 和 Spring 的关系。
- 解释
@SpringBootApplication为什么能启动应用。 - 解释
SpringApplication.run每个关键步骤做什么。 - 解释 Starter、自动配置、条件注解、BeanDefinition 的关系。
- 区分 Boot 2.6及更早、Boot 2.7过渡期和 Boot 3 的自动配置候选注册机制。
- 解释配置文件、环境变量、命令行参数、Profile 的优先级思路。
- 解释内嵌 Tomcat 什么时候创建,如何接入 DispatcherServlet。
- 解释一个 HTTP 请求在 Boot 应用里的完整链路。
- 说清常见扩展点分别在什么阶段使用。
- 能设计统一异常、参数校验、日志 traceId、健康检查和业务指标。
- 能排查自动配置不生效、端口占用、启动慢、连接池耗尽、接口 404/500。
如果你已经读完主线,但还不知道怎么把自动配置、Starter、配置属性、条件注解、健康检查、指标和失败诊断落到项目里,继续做:Spring Boot 商业场景训练营。
学习路线
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 项目通常需要:
- 自己管理 Spring、Spring MVC、Jackson、Tomcat、日志、数据库驱动等依赖版本。
- 配置 DispatcherServlet、组件扫描、MVC 映射、消息转换器。
- 手动部署到外部 Tomcat。
- 自己处理多环境配置。
- 自己补健康检查和指标。
Spring Boot 把这些常见问题收敛成约定:
| 能力 | Spring Boot 做什么 | 不这样会怎样 |
|---|---|---|
| Starter | 聚合一类场景的依赖 | 版本冲突、缺包、配置分散 |
| 自动配置 | 按 classpath 和条件注册默认 Bean | 每个项目重复配置 |
| 内嵌容器 | jar 直接启动 Web 服务 | 依赖外部 Tomcat 部署 |
| 配置体系 | 支持 yml、Profile、环境变量 | 多环境靠改代码 |
| Actuator | 暴露健康、指标、线程等信息 | 上线后不可观测 |
| 约定优于配置 | 常见场景有默认值 | 新项目启动成本高 |
Spring Boot 不是替代 Spring,而是把 Spring 的能力做成生产友好的应用框架。
第二步:最小应用为什么能启动
最小代码:
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。正确做法通常是把启动类放在根包:
com.company.project
DemoApplication.java
controller
service
mapper
config第三步:SpringApplication.run 做了什么
简化流程:
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 都依赖它 |
| 创建 ApplicationContext | Boot 最终仍然是 Spring 容器 |
| 加载自动配置 | 默认 Web、JSON、数据源、事务等能力来自这里 |
| refresh | BeanDefinition 变成真正 Bean |
| 启动 WebServer | Tomcat/Jetty/Undertow 开始监听端口 |
| Runner | 容器可用后做启动检查或轻量预热 |
第四步:Starter 为什么能开箱即用
Starter 本身通常不写大量业务代码,它的价值是“把某类场景依赖聚合起来”。
例如 spring-boot-starter-web 通常会带来:
- Spring MVC。
- Jackson JSON。
- 校验相关依赖。
- 内嵌 Tomcat。
- Web 自动配置所需依赖。
flowchart TD
A["业务项目引入 starter"] --> B["Maven/Gradle 拉取依赖"]
B --> C["classpath 出现相关类"]
C --> D["自动配置条件匹配"]
D --> E["注册默认 Bean"]
E --> F["业务可以直接使用 Web 能力"]Starter + 自动配置才是完整开箱即用。如果只有依赖没有自动配置,业务仍然要手动声明大量 Bean。
第五步:自动配置不是魔法
自动配置的核心公式:
候选自动配置类 + 条件注解 + 默认 Bean + 用户 Bean 优先 = 自动配置简化流程:
flowchart TD
A["@EnableAutoConfiguration"] --> B["AutoConfigurationImportSelector"]
B --> C["读取自动配置候选类"]
C --> D["排除、去重、排序"]
D --> E["解析条件注解"]
E --> F{"条件是否匹配"}
F -- "匹配" --> G["注册 BeanDefinition"]
F -- "不匹配" --> H["跳过该配置"]
G --> I["refresh 阶段创建 Bean"]常见条件注解:
| 注解 | 作用 |
|---|---|
@ConditionalOnClass | classpath 有某个类才生效 |
@ConditionalOnMissingClass | classpath 没有某个类才生效 |
@ConditionalOnBean | 容器里已有某个 Bean 才生效 |
@ConditionalOnMissingBean | 容器里没有某个 Bean 才创建默认 Bean |
@ConditionalOnProperty | 配置项满足条件才生效 |
@ConditionalOnWebApplication | Web 应用才生效 |
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 示例:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.demo.DemoAutoConfigurationBoot 3 示例:
com.example.demo.DemoAutoConfiguration面试时不要说“spring.factories 一定错了”。老项目、老版本和部分兼容场景仍可能看到它。
第六步:自定义 Starter 的最小 Demo
目标:引入一个 starter 后,自动注册一个 TraceIdGenerator。
自动配置类:
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());
}
}配置属性:
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:
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 自动配置声明文件:
# META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.example.trace.autoconfigure.TraceAutoConfiguration业务项目配置:
demo:
trace:
enabled: true
prefix: order为什么要加 @ConditionalOnMissingBean:
- 公共 starter 提供默认实现。
- 业务项目如果自己声明
TraceIdGenerator,默认实现自动让位。 - 否则会出现 Bean 冲突,或者业务无法替换默认逻辑。
第七步:配置体系怎么工作
Spring Boot 配置来源很多:
flowchart TD
A["默认配置"] --> B["application.yml"]
B --> C["application-{profile}.yml"]
C --> D["环境变量"]
D --> E["JVM 参数"]
E --> F["命令行参数"]
F --> G["配置中心"]不同版本和场景优先级细节很多,零基础先抓住原则:
- 越外部的配置通常越适合覆盖内部默认值。
- 越靠近本次启动命令的配置通常优先级越高。
- Profile 用来区分环境,不要把 dev、test、prod 混在一起。
- 密码、密钥不要提交到仓库。
@Value 和 @ConfigurationProperties
| 方式 | 适合 |
|---|---|
@Value("${demo.timeout}") | 少量简单配置 |
@ConfigurationProperties | 一组配置、嵌套配置、类型绑定、校验 |
推荐写法:
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。
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 请求"]如果端口被占用:
Web server failed to start. Port 8080 was already in use.排查:
netstat -ano | findstr 8080然后处理占用进程,或修改:
server:
port: 8081第九步:一次 HTTP 请求怎么走
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 响应"]各环节常见问题:
| 环节 | 常见问题 |
|---|---|
| Filter | traceId 丢失、跨域、编码、认证异常 |
| HandlerMapping | 路径不匹配导致 404 |
| 参数解析 | JSON 格式错、日期格式错、枚举转换失败 |
| 参数校验 | 必填缺失、范围不合法 |
| Controller | 把业务和 SQL 全写在接口层 |
| Service | 事务边界不清,自调用导致事务失效 |
| Mapper | 慢 SQL、N+1、连接池耗尽 |
| JSON 序列化 | 循环引用、时间格式不一致、字段泄露 |
第十步:Web 工程必须补齐的基础能力
统一返回
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);
}
}参数校验
import jakarta.validation.constraints.NotBlank;
public record CreateAssetRequest(
@NotBlank(message = "资产编码不能为空")
String assetCode,
@NotBlank(message = "资产名称不能为空")
String name
) {
}Controller:
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());
}
}统一异常
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:
- 响应格式会不统一。
- 日志会分散。
- 错误码不可治理。
- 安全信息可能被直接返回给前端。
第十一步:常见扩展点怎么选
| 扩展点 | 阶段 | 适合做什么 |
|---|---|---|
EnvironmentPostProcessor | Environment 准备阶段 | 配置解密、额外配置源 |
ApplicationContextInitializer | Context refresh 前 | 初始化上下文属性 |
BeanDefinitionRegistryPostProcessor | BeanDefinition 阶段 | 动态注册 BeanDefinition |
BeanFactoryPostProcessor | Bean 创建前 | 修改 BeanDefinition 属性 |
BeanPostProcessor | Bean 初始化前后 | 代理、增强、注入特殊逻辑 |
ApplicationRunner | 启动完成后 | 启动检查、轻量预热 |
Filter | Servlet 链路最前 | traceId、CORS、认证前置处理 |
HandlerInterceptor | Spring MVC 链路 | 权限、审计、接口日志 |
HandlerMethodArgumentResolver | Controller 参数解析 | 注入当前用户、租户信息 |
HealthIndicator | Actuator 健康检查 | 检查下游依赖状态 |
MeterBinder | 指标注册 | 暴露业务指标 |
FailureAnalyzer | 启动失败诊断 | 输出更友好的失败原因 |
扩展点选择原则:
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/beans | Bean 列表 |
/actuator/conditions | 自动配置条件报告 |
/actuator/threaddump | 线程栈 |
/actuator/loggers | 日志级别 |
生产环境不要把所有端点裸露出来。建议:
- 只暴露必要端点。
- 管理端口和业务端口分离。
- 接入 Spring Security 或网关权限。
- 禁止对公网暴露敏感端点。
- 指标进入 Prometheus、日志进入 ELK 或 Loki。
自定义健康检查:
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 启动
java -jar app.jar --spring.profiles.active=prod --server.port=8080JVM 参数
java -Xms512m -Xmx512m -jar app.jarDockerfile 示例
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY target/app.jar /app/app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]生产注意:
- 配置不要打死在镜像里。
- 日志输出到标准输出或统一目录。
- 健康检查要接入探针。
- JVM 内存要结合容器限制设置。
- 启动参数、Profile、密钥由环境注入。
线上排查总流程
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["扩容、降级或重启恢复"]自动配置不生效
排查顺序:
- 是否引入了正确 starter。
- 自动配置类是否在候选列表。
- 是否被 exclude。
- 条件注解是否匹配。
- 配置项是否写错。
- 是否已有用户 Bean 导致默认 Bean 不创建。
- Bean 创建是否失败。
打开调试:
debug: true或者看:
/actuator/conditions启动慢
常见原因:
| 原因 | 排查 |
|---|---|
| Bean 初始化慢 | 看启动日志、临时增加耗时统计 |
| 外部连接慢 | 数据库、Redis、MQ、注册中心超时 |
| 扫描范围太大 | 检查启动类包路径和组件扫描 |
| Runner 做重活 | 把长任务异步化或延后执行 |
| 日志阻塞 | 检查日志输出和磁盘 |
接口 404
检查:
- Controller 是否被扫描。
- 路径和 HTTP 方法是否正确。
context-path是否配置。- 网关是否转发到正确路径。
- 是否被 Security 拦截后表现为 404。
接口 500
检查:
- 全局异常日志有没有完整 traceId。
- 请求参数是否符合格式。
- Service 是否抛业务异常。
- 数据库是否唯一键冲突、锁等待、连接超时。
- 下游接口是否超时。
商业场景落地
医疗数据采集平台
Spring Boot 可以这样落地:
| 能力 | 用法 |
|---|---|
| 配置体系 | 医院接口地址、采集线程数、超时时间按环境配置 |
| 自动配置 | 数据源、Redis、MQ、Web、JSON 默认装配 |
| 扩展点 | HandlerInterceptor 做租户和操作审计 |
| Actuator | 暴露健康检查给 K8s 或网关 |
| 统一异常 | 采集失败返回统一错误码,日志保留 traceId |
| 配置属性 | CollectProperties 管采集批次、并发、重试 |
| Runner | 启动后做轻量依赖检查,不做耗时采集 |
订单系统
Spring Boot 可以这样落地:
- Controller 只做参数校验和接口编排入口。
- Service 管事务边界。
- Mapper 只负责 SQL。
- 支付、库存、消息客户端做成配置化 Bean。
- 订单状态流转失败时统一异常和日志。
- Actuator 指标暴露订单创建 TPS、失败数、下游耗时。
面试标准回答
Spring Boot 是什么
Spring Boot 是基于 Spring 的应用工程化框架。Spring 提供 IOC、AOP、事务、MVC 等基础能力,Spring Boot 通过 Starter、自动配置、配置体系、内嵌 Web 容器和 Actuator,把这些能力组织成开箱即用、可部署、可观测的应用。它不是替代 Spring,而是降低 Spring 项目的配置、依赖和生产治理成本。自动配置原理
自动配置不是魔法。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 启动流程
Spring Boot 启动大致是:创建 SpringApplication,推断应用类型,加载监听器和初始化器,准备 Environment,创建 ApplicationContext,加载启动类和自动配置,refresh 容器创建 Bean,Servlet Web 应用再启动内嵌 Tomcat,最后执行 Runner 并发布启动完成事件。自定义 Starter 怎么设计
自定义 Starter 通常拆成 starter 和 autoconfigure。starter 负责聚合依赖,autoconfigure 提供自动配置类、配置属性类和默认 Bean。自动配置要用条件注解控制生效,用 @ConfigurationProperties 承载外部配置,用 @ConditionalOnMissingBean 尊重业务自定义,并通过 AutoConfiguration.imports 或 spring.factories 声明。关联知识点
| 知识点 | 继续学习 |
|---|---|
| 自动配置与扩展点全过程 | 自动配置深度页 |
| 自动配置原理 | 自动配置原理 |
| Starter 机制 | Starter机制 |
| 配置体系 | 配置体系 |
| 启动原理 | 启动原理 |
| 扩展点 | Spring Boot扩展点 |
| Actuator | Actuator监控 |
| 生产监控与线上排查 | 线上问题排查Runbook |
| Spring Boot Admin | 多实例集中监控与管理 |
| Spring IOC | Spring IOC |
| Spring 事务 | Spring事务 |
| Spring Boot 面试 | 面试题 |
本章小结
Spring Boot 要学到生产级,必须把启动流程、自动配置、Starter、配置绑定、内嵌 Web 容器、请求链路、扩展点、Actuator、部署和排查串起来。只会写 Controller 不是掌握 Boot;能解释“为什么引入一个 starter 就有默认能力、为什么自定义 Bean 后默认配置让位、为什么配置能绑定、为什么 Tomcat 能随 jar 启动、为什么 Actuator 能定位问题”,才是真正理解。
