版本:Spring Boot 3.2.x / JDK 17 / Maven 3.8+ 前置阶段:阶段
1(Spring Core) 预计篇幅:2200–3000 行
1. 导语
在阶段 1 里,你学会了 Spring 最核心的两件事:IoC
容器负责创建和管理对象(Bean),AOP
负责横切逻辑(事务、日志、权限)。但你也一定发现了一个让人抓狂的问题:写一个能跑的
Web 项目,光 XML 配置就要写几百行——DispatcherServlet
要配、视图解析器要配、数据源要配、事务管理器要配……这些配置既重复又容易抄错。
Spring Boot 要解决的,就是「配置地狱」这个问题。
它的核心武器是一个叫「自动配置(Auto-configuration)」的机制:你只要引入一个
starter 依赖,Spring Boot 就会根据你 classpath 里有什么类、容器里缺什么
Bean,自动帮你把该配的东西配好。你写一个「Hello
World」接口,从建项目到跑起来,可以不超过 10 分钟。
但「能跑起来」和「懂它为什么能跑起来」是两回事。真实生产中,你一定遇到过这些情况:改了个配置项却不生效、引入的
starter 没按预期工作、线上日志乱成一团没法排查、打包后的 jar
换环境跑就报错。这些问题的根子,都在于你对「自动配置」和「配置管理」的理解停在表面。
学完本篇,你将能够:
- 说清
@SpringBootApplication
这个注解背后到底做了什么,以及为什么启动类必须放在包根目录; - 用
--debug
启动参数,像医生看化验单一样读懂「自动配置报告」,独立定位「配置不生效」的根因; - 用
@ConfigurationProperties+@Validated
写出类型安全、可校验的配置类,而不是到处散落@Value; - 配置一套生产级日志(滚动文件、按包隔离级别、traceId
链路追踪); - 从零写一个自己的 starter,把通用能力沉淀成「引入即用」的模块;
- 用 Maven 打出可执行 jar,并做到多环境切换与优雅停机。
本篇是后续所有阶段的地基:阶段 3 的 Web 开发、阶段 4 的数据访问、阶段
10 的分布式基础组件,全都建立在「懂自动配置 +
懂配置管理」之上。读完这一篇,你才算真正从「Spring 使用者」跨入「Spring
Boot 开发者」。
2. 学习目标与前置要求
学完本章,你能:
- 独立用 Spring Initializr 创建一个 Spring Boot 3.2.x 项目并跑通第一个
REST 接口; - 完整拆解
@SpringBootApplication
的三个子注解,并能解释为什么启动类要放在根包; - 说清自动配置的完整链路(
@EnableAutoConfiguration→
AutoConfigurationImportSelector→
AutoConfiguration.imports→@Conditional
家族),并能用--debug排查「配置不生效」; - 写出带
@Validated校验的
@ConfigurationProperties配置类,并理解配置优先级、Profile
切换、敏感信息外部化; - 配置一份生产级
logback-spring.xml(滚动文件 + 按包隔离
+ MDC traceId); - 独立创建一个自定义
starter,并被打包部署用到多环境运行与优雅停机。
前置依赖:
- 必须完成阶段 1(Spring Core),理解 IoC 容器、Bean
生命周期、@Component/@Configuration/@Bean
注解; - 若对「Bean 的注册方式」或「
@Conditional
条件装配」不熟,建议先回看阶段 1 中「注解驱动的 Bean
注册」与「条件化装配」两节; - 环境方面需要 JDK 17 与 Maven 3.8+(下一节给出精确版本)。
3. 环境准备
3.1 精确版本
| 组件 | 版本 | 说明 |
|---|---|---|
| JDK | 17(建议 17.0.10+) | Spring Boot 3.x 最低要求 Java 17 |
| Spring Boot | 3.2.x(本文用 3.2.5) | 3.x 使用 Jakarta EE 9+,包名从 javax.* 变为jakarta.* |
| Maven | 3.8.x / 3.9.x | 用于构建与打包 |
| IDE | IntelliJ IDEA 2023.2+ 或 VS Code + Spring Boot Extension | 二选一 |
3.2 初始化项目(两种方式)
方式 A:Spring Initializr 网页(推荐新手)
打开 start.spring.io,按下表填写:
| 项 | 值 |
|---|---|
| Project | Maven |
| Language | Java |
| Spring Boot | 3.2.5 |
| Group | com.example |
| Artifact | demo |
| Dependencies | Spring Web、Spring Boot DevTools(可选)、Validation |
点击 GENERATE 下载压缩包,解压后用 IDE 打开。
方式 B:命令行创建(可复现)
# 确认 JDK 版本
java -version
# openjdk version "17.0.10" 2024-01-16
# 确认 Maven 版本
mvn -version
# Apache Maven 3.9.6
若没有网络访问 start.spring.io 的环境,可以用下面的
pom.xml直接手建项目,效果完全一致。
3.3 项目结构(本篇统一使用)
demo
├── pom.xml
├── src
│ ├── main
│ │ ├── java
│ │ │ └── com
│ │ │ └── example
│ │ │ └── demo
│ │ │ ├── DemoApplication.java # 启动类(放根包)
│ │ │ ├── controller/ # 接口层
│ │ │ ├── service/ # 业务层
│ │ │ ├── config/ # 配置类
│ │ │ ├── properties/ # @ConfigurationProperties 类
│ │ │ ├── exception/ # BizException / GlobalExceptionHandler
│ │ │ └── common/ # ApiResult / ErrorCode
│ │ └── resources
│ │ ├── application.yml
│ │ ├── application-dev.yml
│ │ ├── application-prod.yml
│ │ └── logback-spring.xml
│ └── test
│ └── java/.../DemoApplicationTests.java
3.4 基础 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- 关键:继承 Spring Boot 父 POM,统一管理所有依赖版本 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Spring Boot basics tutorial</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- Web:内嵌 Tomcat + Spring MVC + Jackson -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 校验:@Validated / @NotBlank 等 JSR-380 注解需要它 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- 测试:JUnit 5 + MockMvc -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 打可执行 fat jar 的关键插件(第 6 章详解) -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
为什么继承父 POM 如此重要?
spring-boot-starter-parent里通过
dependencyManagement锁定了上千个依赖的版本,你引入
spring-boot-starter-web
时不需要写版本号,Spring Boot
会自动选一个经过兼容性测试的版本。一旦你不继承它、自己乱写版本,就容易出现
NoSuchMethodError这类运行时冲突(第 6
章「常见坑」会再讲)。
4. 正文章节
第 1 章 快速开始与
@SpringBootApplication
1.1 第一个 REST 接口
先建立「能跑起来」的体感,再回头拆原理。在
com.example.demo.controller 包下新建
HelloController:
package com.example.demo.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController // = @Controller + @ResponseBody,方法返回值直接写进响应体
@RequestMapping("/api/hello")
public class HelloController {
@GetMapping
public String hello() {
// 先返回最简字符串,下一节再换成统一响应体 ApiResult
return "Hello, Spring Boot!";
}
}
启动类(Initializr 生成,位于 com.example.demo
包根):
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
// run 方法做了三件事:创建容器 → 触发自动配置 → 启动内嵌 Tomcat
SpringApplication.run(DemoApplication.class, args);
}
}
运行并验证:
# 在项目根目录
mvn spring-boot:run
# 另开一个终端验证
curl http://localhost:8080/api/hello
# 输出:Hello, Spring Boot!
启动日志里最关键的几行:
Tomcat started on port 8080 (http) with context path ''
Started DemoApplication in 1.834 seconds (process running for 2.2)
看到 Started DemoApplication 就说明容器启动完成、内嵌
Tomcat 已监听 8080 端口。
1.2 @SpringBootApplication
三注解拆解
@SpringBootApplication
是一个组合注解,它等价于同时标注了下面三个注解(点进源码就能看到):
@SpringBootConfiguration // ① 本质是 @Configuration,把启动类标记为配置类
@EnableAutoConfiguration // ② 开启自动配置(本篇核心,第 2 章详解)
@ComponentScan // ③ 组件扫描
public @interface SpringBootApplication {
// ...
}
逐个拆解:
① @SpringBootConfiguration
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration // 注意:它本身就是被 @Configuration 标注的
@Indexed
public @interface SpringBootConfiguration {
}
它的作用很单纯:把启动类本身变成一个配置类。所以你可以直接在启动类上写
@Bean 方法注册 Bean。Spring Boot 约定「一个应用只允许有一个
@SpringBootConfiguration」,多写会报错。
② @EnableAutoConfiguration ——
本篇的灵魂,第 2 章用一整章讲。它通过
@Import(AutoConfigurationImportSelector.class) 把成百上千个
xxxAutoConfiguration 类加载进来,再由
@Conditional 家族决定谁生效。
③ @ComponentScan —— 阶段 1
学过的组件扫描。它默认扫描启动类所在包及其所有子包,把
@Component/@Service/@Controller/@Repository
标注的类注册成 Bean。
1.3
为什么启动类必须放在包根目录
这是 Spring Boot
新手最容易踩、也最容易在面试里被问的坑。看下面的错误结构:
com.example.demo
├── web
│ └── DemoApplication.java # ❌ 启动类放在 web 子包里
├── controller
│ └── UserController.java # 在 com.example.demo.controller 包
启动类在 com.example.demo.web 包,那
@ComponentScan 只会扫描 com.example.demo.web
及其子包,而 com.example.demo.controller
不在这个范围内。结果:UserController 根本没被注册成
Bean,启动时不会报错,但访问接口时抛:
Field userController in com.example.demo.web.DemoApplication required a bean of type
'com.example.demo.controller.UserController' that could not be found.
修复方式有三种(按推荐度排序):
- 把启动类上移到根包
com.example.demo(最推荐,一劳永逸); - 在
@SpringBootApplication上补
@ComponentScan(basePackages = "com.example.demo")
显式指定扫描范围; - 用
@Import(UserController.class)
显式导入单个类(适合个别跨包的第三方类)。
关键点提示:@SpringBootApplication 里
@ComponentScan 是没有指定 basePackages
的,它使用「标注类的包」作为默认扫描根。所以「启动类放根包」这个约定,本质是让默认扫描范围覆盖你的整个业务代码。
1.4 换成统一响应体 ApiResult
生产接口不会直接返回裸字符串。按系列统一约定,把返回值包成
ApiResult:
package com.example.demo.common;
import lombok.Data;
import org.slf4j.MDC;
@Data
public class ApiResult<T> {
private int code; // 0=成功,非 0=错误码
private String message; // 提示信息
private T data; // 业务数据
private String traceId; // 链路追踪 id
public static <T> ApiResult<T> ok(T data) {
ApiResult<T> r = new ApiResult<>();
r.setCode(ErrorCode.SUCCESS.getCode());
r.setMessage(ErrorCode.SUCCESS.getMessage());
r.setData(data);
r.setTraceId(MDC.get("traceId"));
return r;
}
public static <T> ApiResult<T> ok() {
return ok(null);
}
public static <T> ApiResult<T> fail(int code, String message) {
ApiResult<T> r = new ApiResult<>();
r.setCode(code);
r.setMessage(message);
r.setTraceId(MDC.get("traceId"));
return r;
}
public static <T> ApiResult<T> fail(ErrorCode ec) {
return fail(ec.getCode(), ec.getMessage());
}
}
配套的错误码枚举与业务异常(同样为系列公共类):
package com.example.demo.common;
/** 统一错误码(基础码数值全系列固定不变,各篇可扩展) */
public enum ErrorCode {
SUCCESS(0, "success"),
PARAM_ERROR(40001, "参数错误"),
UNAUTHORIZED(40101, "未登录或登录已过期"),
FORBIDDEN(40301, "无权限访问"),
USER_NOT_FOUND(40401, "用户不存在"),
SYSTEM_ERROR(50000, "系统繁忙,请稍后重试"),
;
private final int code;
private final String message;
ErrorCode(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() { return code; }
public String getMessage() { return message; }
}
package com.example.demo.exception;
import com.example.demo.common.ErrorCode;
/** 统一业务异常:业务代码主动抛出,由 GlobalExceptionHandler 统一转成 ApiResult */
public class BizException extends RuntimeException {
private final int code;
public BizException(ErrorCode ec) {
super(ec.getMessage());
this.code = ec.getCode();
}
public BizException(int code, String message) {
super(message);
this.code = code;
}
public int getCode() { return code; }
}
package com.example.demo.exception;
import com.example.demo.common.ApiResult;
import com.example.demo.common.ErrorCode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/** 全局异常处理器:统一把异常转成 ApiResult,避免堆栈直接暴露给前端 */
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// 业务异常:code/message 透传给前端
@ExceptionHandler(BizException.class)
public ApiResult<Void> handleBiz(BizException e) {
log.warn("业务异常 code={}, message={}", e.getCode(), e.getMessage());
return ApiResult.fail(e.getCode(), e.getMessage());
}
// 参数校验异常:取第一个校验失败的字段提示
@ExceptionHandler(MethodArgumentNotValidException.class)
public ApiResult<Void> handleValid(MethodArgumentNotValidException e) {
String msg = e.getBindingResult().getFieldErrors().stream()
.findFirst()
.map(f -> f.getField() + " " + f.getDefaultMessage())
.orElse(ErrorCode.PARAM_ERROR.getMessage());
return ApiResult.fail(ErrorCode.PARAM_ERROR.getCode(), msg);
}
// 兜底异常:对外返回模糊提示,详细堆栈只进日志,防止泄露内部结构
@ExceptionHandler(Exception.class)
public ApiResult<Void> handleOther(Exception e) {
log.error("系统异常", e);
return ApiResult.fail(ErrorCode.SYSTEM_ERROR);
}
}
改造后的 HelloController:
@RestController
@RequestMapping("/api/hello")
public class HelloController {
@GetMapping
public ApiResult<String> hello() {
return ApiResult.ok("Hello, Spring Boot!");
}
}
再次访问,返回:
{"code":0,"message":"success","data":"Hello, Spring Boot!","traceId":null}
traceId 现在还是 null,等第 4 章配好 MDC
过滤器后就有值了。
本章小结:@SpringBootApplication =
@SpringBootConfiguration +
@EnableAutoConfiguration +
@ComponentScan,启动类放根包是为了让默认组件扫描覆盖全部业务代码;接口返回统一
ApiResult 是贯穿全系列的规范。
第 2 章
自动配置原理(面试必考,务必吃透)
这一章是整篇的重头戏。先记住一句话:自动配置不是「魔法」,它是一套可预测的、基于
classpath 和已有 Bean 的条件判断机制。
你要做的不是背每个配置类,而是掌握它的判断流程,从而能读懂它的「体检报告」。
2.1 完整链路
自动配置的触发链路由四步组成:
启动类上的 @EnableAutoConfiguration
│
▼
@Import(AutoConfigurationImportSelector.class) // ① 通过 @Import 引入一个"选择器"
│
▼
AutoConfigurationImportSelector.selectImports()
│ ② 读取 classpath 下的文件
▼
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
│ ③ 拿到一长串 xxxAutoConfiguration 类名列表(Spring Boot 3.2 约 140+ 个)
▼
对每个 AutoConfiguration 类逐个判断其上的 @Conditional 注解
│ ④ 满足全部条件 → 生效(Positive match);任一不满足 → 跳过(Negative match)
▼
生效的配置类向容器注册默认 Bean(如 DispatcherServlet、ObjectMapper、DataSource)
第 ② 步是 Spring Boot 3.x 的关键变化:老版本(2.7
之前)用的是 META-INF/spring.factories 文件,而 3.x
改成了
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports。如果你在排障时翻旧博客看到
spring.factories,注意那是过时写法,自定义 starter 时(第 5
章)也一定要用新文件名。
你可以在本地依赖里看到这个文件:
# 找到 spring-boot-autoconfigure 的 jar,解压后看这个文件
jar xf spring-boot-autoconfigure-3.2.5.jar
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
cat META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
# 输出片段:
# org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
# org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
# org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration
# ...(约 140+ 行)
2.2 @Conditional
家族:生效判断的规则
每个 xxxAutoConfiguration
类上,都会叠一堆条件注解。判断流程(面试常让画这个图):
某个 AutoConfiguration 类被加载
│
▼
@ConditionalOnClass ── 不满足 ──▶ 跳过(类不在 classpath,如没引 starter-web 就没有 DispatcherServlet)
│ 满足
▼
@ConditionalOnBean ── 不满足 ──▶ 跳过(它依赖的 Bean 不存在)
│ 满足
▼
@ConditionalOnMissingBean ── 已存在 ──▶ 跳过(用户已自定义同类 Bean,自动配置尊重用户、让位)
│ 不存在
▼
@ConditionalOnProperty ── 不满足 ──▶ 跳过(配置开关为 false / 未满足 havingValue)
│ 满足
▼
@ConditionalOnWebApplication ─ 不满足 ─▶ 跳过(当前不是目标 Web 环境,如缺 Servlet)
│ 满足
▼
✅ 该配置类生效,向容器注册默认 Bean
常用的条件注解一览:
| 注解 | 生效条件 | 典型用途 |
|---|---|---|
@ConditionalOnClass |
classpath 存在指定类 | 有没有引对应依赖 |
@ConditionalOnMissingClass |
classpath 不存在指定类 | 排除冲突场景 |
@ConditionalOnBean |
容器存在指定 Bean | 依赖别的自动配置先就绪 |
@ConditionalOnMissingBean |
容器不存在指定 Bean | 用户自定义了就让位 |
@ConditionalOnProperty |
配置项存在且满足 havingValue |
功能开关 |
@ConditionalOnWebApplication |
是 Web 应用(Servlet / Reactive) | 区分 Web 与纯后台 |
@ConditionalOnExpression |
SpEL 表达式为 true | 复杂组合判断 |
两个关键细节:
-
@ConditionalOnClass
是通过字节码(ASM)判断的,不是反射加载。也就是说它只看「这个类的字节码文件在不在
classpath」,不会真的去初始化那个类,避免类加载的副作用(比如静态初始化抛异常)。所以
@ConditionalOnClass(name = "com.xxx.SomeClass")和
@ConditionalOnClass(SomeClass.class)
的区别只是写法,前者可以避开编译期必须能解析到该类的问题。 -
@ConditionalOnMissingBean
是「自动配置让位用户」的核心机制。它保证:只要你自己定义了一个同类型的
Bean(比如自定义
ObjectMapper),自动配置就不会再覆盖你。这就是为什么「自定义
Bean 覆盖默认配置」能工作的原因。
2.3
实例拆解:WebMvcAutoConfiguration 为什么生效
引入 spring-boot-starter-web 后,「零配置」就能跑 Web
应用,本质是下面这条判断链全部成立。看
WebMvcAutoConfiguration 源码(节选):
@AutoConfiguration
@ConditionalOnWebApplication(type = Type.SERVLET) // ① 是 Servlet Web 环境
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class }) // ② 相关类在 classpath
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class) // ③ 用户没自己定义 WebMvcConfigurationSupport
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
public class WebMvcAutoConfiguration {
// ...
@Bean
@ConditionalOnMissingBean
public ViewResolver mvcViewResolver(...) { /* 默认视图解析器 */ }
@Bean
@ConditionalOnMissingBean
public ObjectMapper jacksonObjectMapper(...) { /* 默认 JSON 序列化器 */ }
}
逐条对号入座:
- ① 成立:你跑的是内嵌 Tomcat 的 Servlet 应用;
- ② 成立:
spring-boot-starter-web
传递依赖了spring-webmvc和
spring-boot-starter-tomcat,所以
DispatcherServlet、Servlet都在
classpath; - ③ 成立:你一般不会自己去定义
WebMvcConfigurationSupport(定义了就表示「我要完全接管
WebMvc 配置」,自动配置会整体让位)。
于是 WebMvcAutoConfiguration 生效,向容器注册了默认的
DispatcherServlet、ViewResolver、ObjectMapper
等 Bean——这就是「零配置可用」的全部秘密。
反过来想:假如你没有引入
spring-boot-starter-web,②
不成立,WebMvcAutoConfiguration就是一个 Negative
match,容器里就不会有
DispatcherServlet,自然也就没有 Web 能力。「引入
starter = 凑齐 classpath 条件 =
触发对应自动配置」,这是贯穿全篇的核心心智模型。
2.4 用 –debug
排查「配置不生效」(核心实操)
当「某个功能没按预期生效」时,不要靠猜,直接看自动配置报告。启动时加
--debug:
# 方式一:命令行参数
java -jar app.jar --debug
# 方式二:mvn 启动时
mvn spring-boot:run -Dspring-boot.run.arguments=--debug
# 方式三:写进 application.yml(会一直打印,生产不建议常开)
debug: true
加了 --debug 后,启动日志里会多出一段 CONDITIONS
EVALUATION REPORT,核心是两部分:
============================
CONDITIONS EVALUATION REPORT
============================
Positive matches: # ✅ 生效的自动配置(及它为什么生效)
-----------------
WebMvcAutoConfiguration matched:
- @ConditionalOnWebApplication (required) found 'session' scope
- @ConditionalOnClass found required classes
'org.springframework.web.servlet.DispatcherServlet',
'jakarta.servlet.Servlet'
- @ConditionalOnMissingBean (types: WebMvcConfigurationSupport; ...)
did not find any beans
Negative matches: # ❌ 被跳过的自动配置(及它为什么没生效)
-----------------
DataSourceAutoConfiguration:
Did not match:
- @ConditionalOnClass did not find required class 'javax.sql.DataSource'
(你没引 spring-boot-starter-jdbc,所以数据源不自动配)
RedisAutoConfiguration:
Did not match:
- @ConditionalOnClass did not find required class
'org.springframework.data.redis.core.RedisOperations'
读报告的三步法:
- 先想「我期望生效的功能对应哪个
AutoConfiguration」——不确定类名就在报告里按关键词搜(如
Web、Jdbc、Redis); - 它出现在 Positive 还是 Negative? 在 Negative
就看它Did not match里列的是哪一条条件; - 对症下药:
@ConditionalOnClass did not find required class xxx→
少依赖,补上对应 starter;@ConditionalOnMissingBean ... found beans of type xxx→
你自己定义的 Bean 把默认的顶掉了(多半是误写);@ConditionalOnProperty ... did not find property→
配置项缺失或值不对,检查 yml。
关键点提示:这份报告是排查「自动配置不生效」的第一现场证据,比问人、比搜博客都快且准确。养成「先看报告、再动手」的习惯,能省掉大量无效试错。
2.5 自己写一个条件
Bean,验证条件注解
读懂了原理,动手验证一下。写一个「只有存在 redis
相关类才注册」的演示 Bean(只是演示,别真当生产用):
package com.example.demo.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisOperations;
@Configuration
@Slf4j
public class DemoConditionConfig {
// 只有当 classpath 存在 RedisOperations(即引了 redis starter)、
// 且容器里还没有同名 Bean、且 demo.redis.enabled 为 true 时,才注册
@Bean
@ConditionalOnClass(RedisOperations.class)
@ConditionalOnMissingBean(name = "demoRedisMarker")
@ConditionalOnProperty(prefix = "demo.redis", name = "enabled", havingValue = "true", matchIfMissing = true)
public String demoRedisMarker() {
log.info("demoRedisMarker 已注册:说明 redis 条件满足");
return "redis-present";
}
}
分别做两个实验:
- 不引 redis 依赖:用
--debug启动,在
Negative matches 里能看到demoRedisMarker因
@ConditionalOnClass不满足而被跳过; - 引入
spring-boot-starter-data-redis
后:同样的启动,demoRedisMarker进入 Positive
matches。
2.6
排除与排序:进一步控制自动配置
(1)AutoConfigurationImportSelector
内部到底做了什么
2.1 节的链路里,selectImports()
只是一句话,展开后它内部的 getAutoConfigurationEntry()
大致做了五步:
① getCandidateConfigurations()
读所有 jar 里 META-INF/spring/...AutoConfiguration.imports,收集候选类名
│
▼
② removeDuplicates()
去重(多个 jar 可能重复注册同一个类)
│
▼
③ getExclusions()
应用排除项:@SpringBootApplication(exclude=...) 或 spring.autoconfigure.exclude 配置
│
▼
④ sortAutoConfigurations()
按 @AutoConfigureOrder / @AutoConfigureBefore / @AutoConfigureAfter 排序
│
▼
⑤ 逐个交给条件评估器(ConditionEvaluator),生成 Positive/Negative 报告
(2)三种排除自动配置的方式(当你不想让某个自动配置生效时)
// 方式一:注解排除(精确到类)
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class DemoApplication {
// ...
}
# 方式二:配置排除(不用改代码,运维可临时禁用)
spring:
autoconfigure:
exclude:
- org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
- org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration
// 方式三:@EnableAutoConfiguration 上排除(与方式一等效,写法不同)
@EnableAutoConfiguration(exclude = DataSourceAutoConfiguration.class)
(3)控制自动配置的先后顺序
多个自动配置类之间有依赖关系(比如「数据源」要先于「JPA」配置)。Spring
Boot 用三个注解声明顺序:
@AutoConfiguration
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) // 数字越小越先加载
@AutoConfigureBefore(WebMvcAutoConfiguration.class) // 声明自己必须在某类之前
@AutoConfigureAfter(DataSourceAutoConfiguration.class) // 声明自己必须在某类之后
public class MyAutoConfiguration {
// ...
}
自定义 starter 时,如果你的自动配置依赖别的自动配置先就绪(例如要用到
DataSource),务必用
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
声明顺序,否则可能拿到未就绪的依赖。
关键点提示:排除(exclude)和排序(order)是自动配置的「两个旋钮」——默认值覆盖不了你的需求时,先用
exclude 关掉默认、再用自己的 @Bean +
排序补上,这比硬改框架源码干净得多。
本章小结:自动配置 =
@EnableAutoConfiguration 拉取
AutoConfiguration.imports 里的配置类列表,再逐个用
@Conditional 家族做「classpath 有没有类、容器缺不缺
Bean、配置开关开没开」的条件判断;--debug
报告是排查一切「配置不生效」的第一现场;用 exclude
关默认、用排序注解控制先后。
第 3 章 配置管理
自动配置解决了「默认怎么配」,但真实项目里有大量业务自有配置(对象存储地址、线程池大小、第三方密钥)。这一章讲清
Spring Boot 的配置体系:怎么写、谁优先、怎么安全地读。
3.1 YAML 语法要点
Spring Boot 同时支持 application.properties 和
application.yml,生产统一推荐
yml(层级清晰、支持多行、不易写错)。核心语法:
# ① 层级用缩进表示(不能用 Tab,必须空格,且缩进一致)
server:
port: 8080 # 冒号后必须有一个空格
# ② 三种值类型
app:
name: order-service # 字符串
thread-pool: 16 # 数字(会被绑定成 int)
feature-enabled: true # 布尔
# ③ 数组写法(两种等价)
app:
tags:
- java
- spring
# 或行内:tags: [java, spring]
# ④ 占位符引用
app:
desc: "服务 ${app.name} 的配置" # 引用同文件里的值
# ⑤ 多行字符串(保留换行)
app:
banner: |
第一行
第二行
注意:thread-pool
这种中划线命名,会通过「松散绑定」对应 Java 字段
threadPool(下文 3.3 详讲)。
3.2
配置优先级(必须记住的排序)
同一个配置项可以有多个来源,Spring Boot
按固定优先级从高到低取值(高覆盖低):
① 命令行参数 --server.port=9090
② Java 系统属性 -Dserver.port=9090
③ 操作系统环境变量 SERVER_PORT=9090
④ application-{profile}.yml 指定环境的配置文件
⑤ application.yml 主配置文件
⑥ application.properties 主配置文件(同位置下 yml 优先于 properties)
⑦ 代码里 @PropertySource 自定义属性源
⑧ 默认值 @ConfigurationProperties 里的字段默认值 / 代码硬编码
补充说明:完整优先级还有「当前目录的 config 子目录
> 当前目录 > classpath 的 config 子目录 > classpath
根」这一层「文件位置优先级」,但日常开发记住上面 8 级足以解决 99%
的问题。另外老版本用bootstrap.yml,Spring Boot 3.x
已废弃,改用
spring.config.import(如需配置中心,阶段 10 会讲)。
验证优先级(动手):
# application.yml 里写 server.port: 8080
# 用命令行参数覆盖成 9090
mvn spring-boot:run -Dspring-boot.run.arguments=--server.port=9090
# 或直接
java -jar app.jar --server.port=9090
# 观察启动日志:Tomcat started on port 9090
再用环境变量覆盖验证:
export SERVER_PORT=7070
java -jar app.jar # 此时命令行 > 环境变量,但这里没传命令行,所以是 7070
为什么优先级要设计成这样?
因为「越靠近运行时、越晚出现的值,越应该覆盖更早的默认值」——开发者在 yml
里写默认值,运维在部署时用环境变量/命令行做最后覆盖,双方不用改代码也不用改文件。
3.3 @Value vs
@ConfigurationProperties
读配置有三种方式,生产选择有明确结论:
| 方式 | 场景 | 优点 | 缺点 |
|---|---|---|---|
@Value("${key}") |
读单个配置 | 简单直接 | 类型不安全、无校验、不支持松散绑定、散落各处难维护 |
@ConfigurationProperties |
读一组相关配置 | 类型安全、可校验、支持松散绑定、可复用 | 需要建一个类 + 注册 |
Environment 接口 |
运行时动态探测 | 灵活 | 少用,容易绕过类型安全 |
先看 @Value 的局限:
@Service
public class OssService {
@Value("${app.oss.endpoint}") // 散落、无校验、类型不安全
private String endpoint;
@Value("${app.oss.max-size:1048576}") // 冒号后面是默认值
private int maxSize; // 若配置写成了 "abc",启动直接报 NumberFormatException
// 若某个 key 忘配且没默认值,启动直接 fail-fast
}
结论:@Value
只适合读零散的、单个的配置;凡是「一组有明确含义的配置」(如
app.oss),一律用
@ConfigurationProperties。这是本篇反复强调的规范,也是面试对比题的标准答案。
3.4 生产级
@ConfigurationProperties + @Validated
完整配置类(对象存储 OSS 为例):
package com.example.demo.properties;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
/**
* 绑定 application.yml 中 app.oss.* 前缀的配置。
* 用 @Validated + JSR-380 注解,启动时校验,配置错了 fail-fast 而不是运行时才炸。
*/
@Data
@Validated
@ConfigurationProperties(prefix = "app.oss")
public class OssProperties {
/** 对象存储 endpoint,必填 */
@NotBlank(message = "app.oss.endpoint 不能为空")
private String endpoint;
/** accessKey,必填 */
@NotBlank(message = "app.oss.access-key 不能为空")
private String accessKey;
/** secretKey 属于敏感信息,必填但生产从环境变量注入(见 3.6) */
@NotBlank(message = "app.oss.secret-key 不能为空")
private String secretKey;
/** bucket 名,必填 */
@NotBlank
private String bucket;
/** 上传超时(秒),给默认值并限制范围 */
@Min(1)
@Max(60)
private int timeoutSeconds = 10;
/** 是否启用(功能开关) */
private boolean enabled = true;
/** 连接池大小,必填且不小于 1 */
@NotNull
@Min(1)
private Integer poolSize = 8;
}
启用配置类(两种方式,推荐第一种):
package com.example.demo.config;
import com.example.demo.properties.OssProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* 方式一:用 @EnableConfigurationProperties 显式注册配置类。
* 好处:配置类不需要加 @Component,职责更纯粹,也便于测试时选择性启用。
*/
@Configuration
@EnableConfigurationProperties(OssProperties.class)
public class AppConfig {
}
// 方式二(等效):在配置类上加 @ConfigurationPropertiesScan,
// 会自动扫描指定包下所有 @ConfigurationProperties 类
// @Configuration
// @ConfigurationPropertiesScan("com.example.demo.properties")
// public class AppConfig { }
对应的 yml:
app:
oss:
endpoint: https://oss-cn-hangzhou.aliyuncs.com
access-key: ${OSS_ACCESS_KEY} # 从环境变量读,避免硬编码
secret-key: ${OSS_SECRET_KEY} # 敏感信息外部化
bucket: my-bucket
timeout-seconds: 20
pool-size: 16
使用方(构造器注入 + final,系列统一规范):
package com.example.demo.service;
import com.example.demo.properties.OssProperties;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor // 生成包含 final 字段的构造器,实现构造器注入
@Slf4j
public class OssService {
private final OssProperties ossProperties; // 构造器注入,保证依赖不可变
public String buildUploadUrl() {
// 使用配置拼出上传地址
return ossProperties.getEndpoint() + "/" + ossProperties.getBucket();
}
public void printConfig() {
// 日志里绝不打印 secretKey 这类敏感字段
log.info("OSS 配置:endpoint={}, bucket={}, timeout={}s, enabled={}",
ossProperties.getEndpoint(),
ossProperties.getBucket(),
ossProperties.getTimeoutSeconds(),
ossProperties.isEnabled());
}
}
关键点提示(松散绑定):yml 里的
access-key、secret-key、timeout-seconds
会自动绑定到 Java 字段
accessKey、secretKey、timeoutSeconds。这是
@ConfigurationProperties 独有的「松散绑定(Relaxed
Binding)」能力,@Value
没有——@Value("${app.oss.access-key}") 必须逐字一致,而
@ConfigurationProperties
允许中划线、下划线、大小写驼峰自由对应。
关键点提示(元数据提示):想让 IDE 在写 yml
时自动补全
app.oss.*,需引入配置处理器(只参与编译,不打进运行时包):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
校验失败的报错(故意把 endpoint
留空再启动):
Binding to target [Bindable@... type = com.example.demo.properties.OssProperties] failed:
Property: app.oss.endpoint
Value: null
Reason: app.oss.endpoint 不能为空
启动直接失败(fail-fast),而不是上线后用到才炸——这正是
@ConfigurationProperties + 校验的价值。
3.5 多环境 Profile
一套代码,跑开发/测试/生产多个环境,靠 Profile 切换:
# application.yml(所有环境共享的公共配置 + 默认激活哪个环境)
spring:
profiles:
active: dev # 默认 dev;生产用环境变量 SPRING_PROFILES_ACTIVE 覆盖
application:
name: demo
server:
port: 8080
# application-dev.yml(开发环境)
server:
port: 8080
logging:
level:
com.example: DEBUG # 开发环境业务包打 DEBUG,便于排查
# application-prod.yml(生产环境)
server:
port: 8080
logging:
level:
com.example: INFO # 生产只打 INFO,减少日志量
app:
oss:
endpoint: https://oss-prod.example.com
切换与验证:
# 方式一:yml 里 active(默认 dev)
# 方式二:命令行
java -jar app.jar --spring.profiles.active=prod
# 方式三:环境变量(生产标准做法,运维不改文件)
export SPRING_PROFILES_ACTIVE=prod
java -jar app.jar
合并规则:application.yml
是公共底座,application-{profile}.yml
是特定环境的覆盖层,两者合并而非替换,profile
文件里同名的键会覆盖公共文件里的值。
3.6
敏感信息外部化(安全意识贯穿全篇)
铁律:数据库密码、AK/SK、JWT 密钥等敏感信息,绝不硬编码进 yml
提交到 Git 仓库。 一旦进仓库,即使后来删除,Git
历史里依然可查,等于永久泄露。正确做法:
# ❌ 错误示范:硬编码
spring:
datasource:
password: "MyP@ssw0rd123"
# ✅ 正确做法一:占位符引用环境变量
spring:
datasource:
password: ${DB_PASSWORD}
# ✅ 正确做法二:引用 JVM 系统属性
spring:
datasource:
password: ${db.password}
部署时由运维注入:
# 环境变量注入
export DB_PASSWORD='MyP@ssw0rd123'
java -jar app.jar
# 或命令行参数注入(优先级最高,但会出现在进程列表里,需权衡)
java -jar app.jar --spring.datasource.password='MyP@ssw0rd123'
# 或 JVM 系统属性
java -jar -Ddb.password='MyP@ssw0rd123' app.jar
进阶:大型生产会用配置中心(Nacos/Apollo/Vault)统一管理并加密,阶段
10 会讲。现阶段先做到「敏感信息 100% 外部化」,就已超过多数团队。
3.7
进阶:构造器绑定、嵌套配置与 spring.config.import
(1)构造器绑定(Spring Boot 3
推荐的不可变写法)
前面 OssProperties 用 @Data +
setter,这是最通用、兼容性最好的写法。但 Boot 3
更推荐构造器绑定:配置对象一旦创建就不可变(final
字段),且校验注解可直接打在构造参数上。
package com.example.demo.properties;
import jakarta.validation.constraints.NotBlank;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
/**
* 构造器绑定的不可变配置类。
* Spring Boot 3.x 中:只要类只有一个构造器,就自动启用构造器绑定,无需 @ConstructorBinding。
*/
@Validated
@ConfigurationProperties(prefix = "app.oss")
public class ImmutableOssProperties {
private final String endpoint;
private final String accessKey;
private final String bucket;
private final int timeoutSeconds;
// 校验注解直接打在构造参数上,绑定 + 校验一步完成
public ImmutableOssProperties(@NotBlank String endpoint,
@NotBlank String accessKey,
@NotBlank String bucket,
int timeoutSeconds) {
this.endpoint = endpoint;
this.accessKey = accessKey;
this.bucket = bucket;
this.timeoutSeconds = timeoutSeconds <= 0 ? 10 : timeoutSeconds; // 缺省兜底
}
public String getEndpoint() { return endpoint; }
public String getAccessKey() { return accessKey; }
public String getBucket() { return bucket; }
public int getTimeoutSeconds() { return timeoutSeconds; }
}
何时用哪个?团队协作、字段多、后续要扩展的场景,
@Data+
setter
更省事;追求不可变、字段稳定、并发安全的场景,构造器绑定更优。两者都是
@ConfigurationProperties,本文后续仍以@Data
风格为主(与系列公共约定一致)。
(2)嵌套配置
真实配置常有层级(如 OSS
下面还有重试策略)。@ConfigurationProperties
天然支持嵌套对象:
app:
oss:
endpoint: https://oss.example.com
retry:
max-attempts: 3
backoff-ms: 1000
@Data
@ConfigurationProperties(prefix = "app.oss")
@Validated
public class NestedOssProperties {
@NotBlank
private String endpoint;
/** 嵌套对象:必须给默认实例,否则配置缺失时 retry 为 null */
@Valid
private Retry retry = new Retry();
@Data
public static class Retry {
@Min(1)
@Max(10)
private int maxAttempts = 3; // 默认重试 3 次
@Min(0)
private long backoffMs = 1000; // 退避 1 秒
}
}
关键点提示:嵌套对象要加 @Valid
才会触发内层校验;字段要给 = new Retry() 默认实例,否则 yml
里没配 retry 时该字段为 null,用到就 NPE。
(3)松散绑定(Relaxed Binding)完整规则
@ConfigurationProperties
绑定字段时,大小写、中划线、下划线、驼峰写法等价:
| 配置写法 | 绑定到 Java 字段 | 说明 |
|---|---|---|
app.thread-pool |
threadPool |
中划线 → 驼峰 |
app.thread_pool |
threadPool |
下划线 → 驼峰 |
app.THREAD_POOL |
threadPool |
大写 → 驼峰 |
app.threadPool |
threadPool |
完全一致 |
这也是 @Value
做不到的:@Value("${app.thread-pool}") 一旦 key
大小写/分隔符对不上就直接解析失败。
(4)spring.config.import:引入额外配置源
一个 yml 写不下时,或想把敏感配置放在仓库外的文件,用
spring.config.import 引入:
# application.yml
spring:
config:
import:
- "optional:file:/etc/app/oss-secret.yml" # optional: 文件不存在也不报错
- "classpath:common.yml" # 引入 classpath 下另一个配置文件
/etc/app/oss-secret.yml 内容(不进 Git
仓库,部署时由运维放置):
app:
oss:
access-key: AK-XXXX
secret-key: SK-XXXX
spring.config.import是 Spring Boot 2.4+
引入的新机制,取代了旧版的
bootstrap.yml(那个在 Spring Cloud
老版本里用于配置中心,3.x 已废弃)。看到老项目里还有
bootstrap.yml不要奇怪,但新项目请用
spring.config.import。
本章小结:配置管理三件事——yml
写配置、记优先级(命令行 > 系统属性 > 环境变量 > profile >
默认)、用 @ConfigurationProperties +
校验安全地读;敏感信息一律环境变量注入;进阶可掌握构造器绑定、嵌套配置、spring.config.import
引入外部配置源。
第 4 章 日志体系
日志是生产排查的命脉。Spring Boot 默认的日志方案是
SLF4J(门面接口)+ Logback(实现),你代码里只面向
SLF4J 编程,底层实现可替换。
4.1 日志级别与规范
级别从低到高:
TRACE < DEBUG < INFO < WARN < ERROR
- TRACE/DEBUG:开发排查细节;
- INFO:关键业务节点(下单成功、支付回调);
- WARN:可恢复的异常(重试、降级);
- ERROR:需要人工介入的错误(异常堆栈)。
生产日志铁律:
- 一律用 Lombok 的
@Slf4j,禁止
System.out.println(无级别、无格式、性能差); - 关键操作记入参/出参、耗时、traceId;敏感字段(密码、手机号、身份证)必须脱敏;
- 异常日志必须带堆栈:
log.error("订单处理失败", e),绝不要
log.error(e.getMessage())丢堆栈。
4.2 快速配置(写进 yml 即可)
不写任何 XML 时,Spring Boot 有一套默认日志格式,你也可以在 yml
里做简单定制:
logging:
level:
root: INFO # 全局默认级别
com.example.demo: DEBUG # 自己的包打 DEBUG
org.springframework: WARN # 第三方框架降噪,只打 WARN
file:
name: logs/demo.log # 输出到文件(简单场景够用)
但 yml 配置能力有限(不能按大小/日期滚动、不能自定义
pattern),生产必须上
logback-spring.xml。
4.3 生产级 logback-spring.xml
文件名必须是 logback-spring.xml(带 -spring
后缀),这样才能用 <springProfile> 按环境切换:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 定义两个可复用变量:日志目录和文件名 -->
<property name="LOG_DIR" value="logs"/>
<property name="APP_NAME" value="demo"/>
<!-- ① 控制台输出:开发时人眼查看 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!-- traceId 从 MDC 取;%X{traceId} 输出链路追踪 id -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- ② 滚动文件:按日期 + 大小切分,生产回溯靠它 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_DIR}/${APP_NAME}.log</file>
<!-- 按时间滚动,一天一个文件,超过 200MB 再切一个 .%i 序号文件 -->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_DIR}/${APP_NAME}-%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>200MB</maxFileSize>
<maxHistory>30</maxHistory> <!-- 保留 30 天 -->
<totalSizeCap>10GB</totalSizeCap> <!-- 总大小上限,防磁盘打满 -->
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- ③ 错误日志单独归档:方便快速定位 ERROR -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_DIR}/${APP_NAME}-error.log</file>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_DIR}/${APP_NAME}-error-%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- ④ 按包隔离级别:自己的包 DEBUG,第三方框架降噪 -->
<logger name="com.example.demo" level="DEBUG"/>
<logger name="org.springframework" level="WARN"/>
<logger name="org.apache.tomcat" level="WARN"/>
<!-- ⑤ 根 logger:默认 INFO,同时挂控制台和文件 -->
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
<appender-ref ref="ERROR_FILE"/>
</root>
<!-- ⑥ 按环境切换:开发环境控制台打详细,生产环境不打控制台(交给采集器) -->
<springProfile name="dev">
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
</springProfile>
</configuration>
逐块说明:
%X{traceId}:从 MDC(Mapped Diagnostic
Context)取traceId
变量,配合下一节的过滤器实现全链路追踪;SizeAndTimeBasedRollingPolicy:同时按时间和大小滚动,日志文件会变成
demo-2026-08-14.0.log.gz这样的归档,自动压缩省空间;<springProfile>:这是文件名带
-spring后缀才能用的特性,可以在不同环境用不同
appender,而无需维护多份 XML;- 按包隔离:自己的业务包打 DEBUG
方便排查,org.springframework等框架包降到
WARN,避免启动日志刷屏淹没关键信息。
4.4 MDC +
traceId:给每条请求一个「身份证」
ApiResult 里的 traceId 字段,以及日志里的
%X{traceId},都靠一个过滤器把 traceId 放进 MDC。实现:
package com.example.demo.common;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.MDC;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.UUID;
/**
* 给每个请求生成唯一 traceId 并放入 MDC。
* MDC 是线程本地(ThreadLocal)的,同一请求的所有日志自动带上该 id,
* 从而能把一条请求链路的所有日志串起来。
*/
@Component
@Order(Ordered.HIGHEST_PRECEDENCE) // 最高优先级,保证最先进入、最后清理
public class TraceIdFilter extends OncePerRequestFilter {
private static final String TRACE_ID = "traceId";
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
try {
// 优先沿用上游传入的 traceId(网关/调用方),没有则自己生成
String traceId = request.getHeader("X-Trace-Id");
if (traceId == null || traceId.isBlank()) {
traceId = UUID.randomUUID().toString().replace("-", "");
}
MDC.put(TRACE_ID, traceId);
// 把 traceId 回写到响应头,方便前端/调用方联调时对应
response.setHeader("X-Trace-Id", traceId);
filterChain.doFilter(request, response);
} finally {
// 关键:请求结束必须清理,否则线程复用(Tomcat 线程池)会串号
MDC.remove(TRACE_ID);
}
}
}
改造 HelloController,加一条日志验证:
@RestController
@RequestMapping("/api/hello")
@Slf4j
public class HelloController {
@GetMapping
public ApiResult<String> hello() {
log.info("收到 hello 请求"); // 这条日志会自动带上 traceId
return ApiResult.ok("Hello, Spring Boot!");
}
}
访问后看日志和响应:
2026-08-14 10:00:01.123 [http-nio-8080-exec-1] [3f2a9c...] INFO c.e.d.controller.HelloController - 收到 hello 请求
响应 JSON 里的 traceId 也不再是 null:
{"code":0,"message":"success","data":"Hello, Spring Boot!","traceId":"3f2a9c..."}
关键点提示:MDC 底层是
ThreadLocal,所以必须在 finally 里
MDC.remove(),否则 Tomcat
线程池复用线程时,下一个请求会串到上一个请求的
traceId,排查时反而制造混乱。同理,异步线程(@Async、自定义线程池)不会自动继承
MDC,需要显式传递,阶段 10 会再讲。
4.5 进阶:日志
Pattern 占位符、异步日志与结构化日志
(1)Pattern 占位符速查
前面 logback-spring.xml 里的 pattern 是
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{50} - %msg%n,逐段含义:
| 占位符 | 含义 | 示例输出 |
|---|---|---|
%d{...} |
时间(可自定义格式) | 2026-08-14 10:00:01.123 |
%thread / %t |
线程名 | http-nio-8080-exec-1 |
%-5level |
日志级别,左对齐占 5 位 | INFO / ERROR |
%logger{50} |
Logger 名,最多 50 字符 | c.e.d.controller.HelloController |
%msg / %m |
日志正文 | 收到 hello 请求 |
%X{traceId} |
从 MDC 取 traceId |
3f2a9c... |
%n |
换行 | — |
(2)异步日志:高并发下降低日志写盘对业务的拖累
日志同步写盘在 QPS 很高时可能成为瓶颈。Logback 的
AsyncAppender 用一个队列 + 后台线程异步落盘:
<!-- 在已有 FILE appender 基础上包一层异步 -->
<appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
<!-- 队列满时的策略:不丢弃 INFO 以下(discardingThreshold=0 表示不丢弃),避免丢关键日志 -->
<discardingThreshold>0</discardingThreshold>
<queueSize>512</queueSize>
<includeCallerData>false</includeCallerData>
<!-- 实际落盘仍交给同步的 FILE appender -->
<appender-ref ref="FILE"/>
</appender>
然后在 <root> 里把 FILE 换成
ASYNC_FILE 即可。
权衡:异步日志提升了吞吐,但代价是——应用崩溃/宕机时,队列里还没落盘的日志会丢失。所以关键审计日志不建议走异步,或接受「极端情况下丢少量日志」的取舍。
(3)结构化(JSON)日志
传统文本日志靠 grep/awk
人工解析,量大时吃力。上了日志采集平台(ELK/Loki)后,更推荐直接输出
JSON,方便机器解析:
<appender name="JSON_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_DIR}/${APP_NAME}.json.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_DIR}/${APP_NAME}-%d{yyyy-MM-dd}.%i.json.log.gz</fileNamePattern>
<maxFileSize>200MB</maxFileSize>
<maxHistory>7</maxHistory>
</rollingPolicy>
<encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
</appender>
使用 JSON 输出需要额外引入
logstash-logback-encoder
依赖(net.logstash.logback:logstash-logback-encoder:7.4)。没有采集平台的小项目不必上
JSON,普通文本 + traceId 已足够。
本章小结:日志 = @Slf4j 打点 +
logback-spring.xml
落盘(滚动、按包隔离、<springProfile> 按环境切换)+
MDC traceId 串链路;异常必须带堆栈,敏感字段必须脱敏;高并发用
AsyncAppender 降延迟,上采集平台可输出 JSON
结构化日志。
第 5 章 Starter
机制与自定义 Starter
5.1 Starter 是什么
Starter = 「依赖聚合 +
自动配置类」的打包。它本身不含业务代码,只做两件事:
- 聚合依赖:
spring-boot-starter-web
把你需要的
spring-webmvc、spring-boot-starter-tomcat、jackson
等十几个依赖一次性引入,且版本全部对齐; - 触发自动配置:把
xxxAutoConfiguration
类通过AutoConfiguration.imports注册进自动配置流程,配合
@Conditional判断何时生效。
命名规范(一眼区分官方/第三方):
- 官方:
spring-boot-starter-*(如
spring-boot-starter-web、-data-jpa、-data-redis、-validation、-actuator、-test); - 第三方/公司内部:
*-spring-boot-starter(如
mybatis-spring-boot-starter、id-generator-spring-boot-starter)。
5.2 为什么需要自定义 Starter
当你有一个「跨项目复用的通用能力」时,不该在每个项目里复制粘贴。典型场景:ID
生成器、统一日志切面、第三方 SDK 的封装、内部 RPC
客户端、通用工具。把这些沉淀成一个 starter,别的项目「引入依赖
+ 配几个属性」即可用,且通过 @ConditionalOnMissingBean
保留被覆盖的余地。
阶段 10 会讲分布式
ID(雪花算法)在分布式场景下的完整实现与时钟回拨处理,本章先做一个可运行的教学版,重点是讲清
starter 的结构与装配流程,不必深入雪花算法细节。
5.3 自定义「ID
生成器」Starter(完整步骤)
第 1 步:新建 Maven 模块
id-generator-spring-boot-starter,目录结构:
id-generator-spring-boot-starter
├── pom.xml
└── src/main
├── java/com/example/idgenerator
│ ├── IdGenerator.java # 对外接口
│ ├── SnowflakeIdGenerator.java # 默认实现(雪花算法简化版)
│ ├── IdGeneratorProperties.java # 配置属性类
│ └── IdGeneratorAutoConfiguration.java # 自动配置类(核心)
└── resources/META-INF/spring
└── org.springframework.boot.autoconfigure.AutoConfiguration.imports # 注册入口
第 2 步:写 pom.xml。关键点:只依赖
spring-boot-autoconfigure(轻量),不引入
spring-boot-starter-web
这种重依赖;spring-boot-configuration-processor
生成元数据:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>id-generator-spring-boot-starter</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- 自动配置核心:提供 @AutoConfiguration、@ConditionalOnXxx 等注解 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<!-- 校验注解(@Validated 等),scope provided 表示由使用方决定 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<optional>true</optional>
</dependency>
<!-- 生成 yml 自动补全元数据,optional 不打进使用者运行时 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
第 3 步:写配置属性类:
package com.example.idgenerator;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
/**
* id.generator.* 配置项。
* 全部给默认值,使用者「零配置」也能跑,想调再覆盖。
*/
@Data
@Validated
@ConfigurationProperties(prefix = "id.generator")
public class IdGeneratorProperties {
/** 是否启用(功能总开关) */
private boolean enabled = true;
/** 雪花算法的工作机器 id(0~1023),多实例部署时每台必须不同 */
@Min(0)
@Max(1023)
private long workerId = 1;
/** 数据中心 id(0~31) */
@Min(0)
@Max(31)
private long datacenterId = 1;
}
第 4 步:写对外接口 + 默认实现:
package com.example.idgenerator;
/** 对外暴露的 ID 生成接口 */
public interface IdGenerator {
/** 生成全局唯一、趋势递增的 64 位 ID */
long nextId();
}
package com.example.idgenerator;
/**
* 雪花算法(Snowflake)简化教学版。
* 64 位结构:1 位符号位 + 41 位时间戳 + 5 位数据中心 + 5 位机器 + 12 位序列号。
* 注意:生产环境的时钟回拨处理、跨机房唯一性,阶段 10 再展开。
*/
public class SnowflakeIdGenerator implements IdGenerator {
private static final long EPOCH = 1704067200000L; // 起始时间戳(2024-01-01),可自定义
private static final long WORKER_ID_BITS = 5L;
private static final long DATACENTER_ID_BITS = 5L;
private static final long SEQUENCE_BITS = 12L;
private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS); // 31
private static final long MAX_DATACENTER_ID = ~(-1L << DATACENTER_ID_BITS); // 31
private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS); // 4095
private static final long WORKER_ID_SHIFT = SEQUENCE_BITS; // 12
private static final long DATACENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS; // 17
private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTER_ID_BITS; // 22
private final long workerId;
private final long datacenterId;
private long sequence = 0L;
private long lastTimestamp = -1L;
public SnowflakeIdGenerator(long workerId, long datacenterId) {
if (workerId > MAX_WORKER_ID || workerId < 0) {
throw new IllegalArgumentException("workerId 必须在 0~" + MAX_WORKER_ID + " 之间");
}
if (datacenterId > MAX_DATACENTER_ID || datacenterId < 0) {
throw new IllegalArgumentException("datacenterId 必须在 0~" + MAX_DATACENTER_ID + " 之间");
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
@Override
public synchronized long nextId() {
long timestamp = System.currentTimeMillis();
// 时钟回拨保护:若当前时间小于上次时间,直接抛异常(生产需更完善策略,阶段 10 讲)
if (timestamp < lastTimestamp) {
throw new IllegalStateException("时钟回拨 " + (lastTimestamp - timestamp) + "ms,拒绝生成 ID");
}
if (timestamp == lastTimestamp) {
// 同一毫秒内序列号自增,超 4095 则自旋等下一毫秒
sequence = (sequence + 1) & SEQUENCE_MASK;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
// 位移拼装出 64 位 ID
return ((timestamp - EPOCH) << TIMESTAMP_SHIFT)
| (datacenterId << DATACENTER_ID_SHIFT)
| (workerId << WORKER_ID_SHIFT)
| sequence;
}
private long tilNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
}
第 5 步:写自动配置类(核心):
package com.example.idgenerator;
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;
/**
* ID 生成器的自动配置类。
* 三个条件注解层层把关:
* - @ConditionalOnProperty:配置开关 id.generator.enabled 为 true 才生效(默认 true)
* - @ConditionalOnMissingBean:使用者已自定义 IdGenerator 时让位,绝不覆盖
*/
@AutoConfiguration
@EnableConfigurationProperties(IdGeneratorProperties.class)
@ConditionalOnProperty(prefix = "id.generator", name = "enabled", havingValue = "true", matchIfMissing = true)
public class IdGeneratorAutoConfiguration {
@Bean
@ConditionalOnMissingBean(IdGenerator.class)
public IdGenerator idGenerator(IdGeneratorProperties properties) {
// 用配置里的 workerId / datacenterId 构造默认雪花实现
return new SnowflakeIdGenerator(properties.getWorkerId(), properties.getDatacenterId());
}
}
第 6 步:写注册入口文件(Spring Boot 3.x
的新写法,不是 spring.factories):
文件路径
src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports,内容只有一行:
com.example.idgenerator.IdGeneratorAutoConfiguration
第 7 步:安装到本地仓库:
cd id-generator-spring-boot-starter
mvn clean install
# 安装到本地 ~/.m2,其他项目即可引用
第 8 步:在业务项目里使用。在 demo 的
pom.xml 加依赖:
<dependency>
<groupId>com.example</groupId>
<artifactId>id-generator-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
配置(可选,不配就用默认值):
id:
generator:
worker-id: 2 # 多实例部署时每台不同
datacenter-id: 1
使用:
@Service
@RequiredArgsConstructor
@Slf4j
public class OrderService {
private final IdGenerator idGenerator; // 构造器注入 starter 提供的 Bean
public long createOrder() {
long orderId = idGenerator.nextId();
log.info("生成订单号 {}", orderId);
return orderId;
}
}
验证:用 --debug 启动,在 Positive
matches 里能看到
IdGeneratorAutoConfiguration matched;把配置改成
id.generator.enabled: false 重启,它就进入 Negative
matches。
关键点提示:
- 入口文件名一个字母都不能错:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports。写错(比如还写老的
spring.factories)会导致自动配置静默不生效,且不报错,极难排查——这也是为什么
2.4 节强调先看--debug报告。 @ConditionalOnMissingBean是自定义 starter
的「逃生门」:使用者想自己实现IdGenerator
时,只需注册一个自己的
Bean,你的默认实现会自动让位,双方都不改代码。- starter 依赖要克制:只依赖
spring-boot-autoconfigure,不要引入一堆重依赖,否则使用方会莫名其妙被传递进一坨没用的
jar。
5.4
进阶:条件注解的落点与新旧入口对比
(1)条件注解写在「类上」还是「@Bean 方法上」?
这是自定义 starter 时最容易含糊的点。规则很简单:
- 写在类上:控制整段自动配置(该类里的所有
@Bean)是否生效; - 写在
@Bean方法上:只控制这一个 Bean
是否注册,类里其他 Bean 不受影响。
@AutoConfiguration
@ConditionalOnClass(IdGenerator.class) // 类级条件:classpath 有 IdGenerator 才解析整类
@EnableConfigurationProperties(IdGeneratorProperties.class)
public class IdGeneratorAutoConfiguration {
// 方法级条件:仅控制"雪花实现"这个 Bean 是否注册
@Bean
@ConditionalOnProperty(prefix = "id.generator", name = "impl", havingValue = "snowflake", matchIfMissing = true)
@ConditionalOnMissingBean(IdGenerator.class)
public IdGenerator snowflakeIdGenerator(IdGeneratorProperties p) {
return new SnowflakeIdGenerator(p.getWorkerId(), p.getDatacenterId());
}
// 方法级条件:配置成 uuid 时,改用 UUID 实现(另一个 Bean)
@Bean
@ConditionalOnProperty(prefix = "id.generator", name = "impl", havingValue = "uuid")
@ConditionalOnMissingBean(IdGenerator.class)
public IdGenerator uuidIdGenerator() {
return () -> UUID.randomUUID().getMostSignificantBits() & Long.MAX_VALUE;
}
}
配合配置 id.generator.impl: snowflake 或
uuid,就能让使用方通过配置切换实现,而不用改代码——这是自定义
starter 保持灵活性的标准手法。
(2)新旧入口文件对比(务必记住,别被老博客带偏)
| 项 | 旧写法(Spring Boot 2.7 之前) | 新写法(Spring Boot 2.7+ / 3.x) |
|---|---|---|
| 入口文件 | META-INF/spring.factories |
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports |
| 文件格式 | key=value键值对( org.springframework.boot.autoconfigure.EnableAutoConfiguration=类1,类2) |
每行一个类名,无 key |
| 示例 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.example.idgenerator.IdGeneratorAutoConfiguration |
com.example.idgenerator.IdGeneratorAutoConfiguration |
2.7 是过渡版本,两个文件都支持;3.x 只认
AutoConfiguration.imports。你抄网上老教程时如果还写
spring.factories,starter
会静默不生效——这正是「引入 starter
没反应」的高发根因,回到 2.4 节用--debug一查便知。
本章小结:Starter = 依赖聚合 +
AutoConfiguration.imports 注册的自动配置类;自定义 starter
六步走(建模块 → 写属性类 → 写接口/实现 → 写
@AutoConfiguration → 写 imports 入口 →
安装使用),@ConditionalOnMissingBean
保证可被覆盖;条件注解可按需落在类级或方法级,3.x 入口文件必须用
AutoConfiguration.imports。
第 6 章 打包与部署
6.1
spring-boot-maven-plugin 与可执行 jar
第 3 章 pom.xml 里已经配了
spring-boot-maven-plugin,它是打「可执行 fat
jar」的关键:
mvn clean package
产物在 target/ 下,注意用 .jar
而不是 .jar.original:
target/
├── demo-0.0.1-SNAPSHOT.jar # ✅ 可执行 fat jar(内嵌 Tomcat + 所有依赖)
└── demo-0.0.1-SNAPSHOT.jar.original # ❌ 只是你的字节码,不含依赖,不能直接 java -jar 跑
fat jar 的内部结构(解压可看):
demo-0.0.1-SNAPSHOT.jar
├── BOOT-INF/
│ ├── classes/ # 你的业务字节码 + application.yml
│ └── lib/ # 所有第三方依赖 jar
├── META-INF/
│ ├── MANIFEST.MF # 声明了 Main-Class 和 Start-Class
│ └── spring/
│ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports # 各依赖的自动配置入口
└── org/springframework/boot/loader/ # Spring Boot 的类加载器,负责从嵌套 jar 加载类
关键点:MANIFEST.MF 里 Main-Class 是 Spring
Boot 的 JarLauncher,Start-Class 才是你的
DemoApplication。JarLauncher 先建好「能从嵌套
jar 加载类」的类加载器,再启动你的应用——这就是「一个 jar
就能跑」的原理。
6.2 运行与多环境切换
# 基础运行
java -jar target/demo-0.0.1-SNAPSHOT.jar
# 指定环境
java -jar target/demo-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod
# 覆盖任意配置(优先级最高)
java -jar target/demo-0.0.1-SNAPSHOT.jar
--server.port=9090
--spring.datasource.password='xxx'
# 后台运行 + 日志重定向(注意 nohup 与 &)
nohup java -jar target/demo-0.0.1-SNAPSHOT.jar > app.out.log 2>&1 &
6.3 多环境「打包」的两种策略
策略 A(推荐):一个 jar 打天下,运行时用 Profile
切。只打一个包,部署时用 SPRING_PROFILES_ACTIVE
或命令行切环境,配置不进 jar:
mvn clean package # 只打一个通用包
SPRING_PROFILES_ACTIVE=prod java -jar target/demo-0.0.1-SNAPSHOT.jar
策略 B:按环境打不同的包。用 Maven Profile
在打包时替换资源:
<!-- pom.xml 里定义 Maven profile -->
<profiles>
<profile>
<id>prod</id>
<properties>
<!-- 打包时激活 prod 环境 -->
<spring.profiles.active>prod</spring.profiles.active>
</properties>
</profile>
</profiles>
mvn clean package -Pprod # 打出的 jar 内置 prod 配置
生产共识:优先策略
A。让「环境差异」成为部署时的事,而不是打包时的事,避免「打错环境包」这类事故。
6.4 优雅停机
生产发版时直接 kill -9
会导致正在处理的请求被强杀、事务中断。Spring Boot 3.2 内置优雅停机:
# application.yml
server:
shutdown: graceful # 开启优雅停机
spring:
lifecycle:
timeout-per-shutdown-phase: 30s # 最多等 30 秒让存量请求处理完
开启后,收到停机信号(如 kill 默认的
SIGTERM)时,应用会:
- 停止接收新请求;
- 等待正在处理的请求完成(最多等 30 秒);
- 优雅关闭容器与资源(数据源、线程池等)。
# 用 kill 发 SIGTERM(不要用 kill -9,-9 无法优雅停机)
kill <pid>
# 或 systemd 管理时
systemctl stop demo
验证日志里会看到 Commencing graceful shutdown 与
Graceful shutdown complete。
6.5 一个完整的部署检查清单
上线前自检:
# 1. 确认打的包正确(不是 .jar.original)
ls -lh target/*.jar
# 2. 本地用生产配置试跑,观察启动日志无 ERROR
SPRING_PROFILES_ACTIVE=prod java -jar target/demo-0.0.1-SNAPSHOT.jar
# 3. 用 --debug 抽查关键自动配置是否按预期生效/失效
java -jar target/demo-0.0.1-SNAPSHOT.jar --debug 2>&1 | grep -A5 "Positive matches"
# 4. 验证敏感信息来自环境变量而非硬编码
grep -r "password" src/main/resources/ # 应只有 ${...} 占位符
6.6 健康检查与容器化部署
(1)用 Actuator 暴露健康检查,配合优雅停机验证
生产环境(尤其容器编排)需要一个「这个实例还活着吗、能接流量吗」的探针。引入
Actuator:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
# application.yml
management:
endpoints:
web:
exposure:
include: health,info # 只暴露 health 和 info,不要全开(安全)
endpoint:
health:
show-details: when-authorized # 详情需授权,默认不泄露内部信息
验证:
# 存活探针:容器编排用它判断要不要重启
curl http://localhost:8080/actuator/health
# {"status":"UP"}
# 就绪探针(组件齐全才 UP,优雅停机时会先变 DOWN,K8s 据此摘流量)
curl http://localhost:8080/actuator/health/readiness
# {"status":"UP"}
# 存活探针
curl http://localhost:8080/actuator/health/liveness
# {"status":"UP"}
与优雅停机的配合:
server.shutdown=graceful
触发停机时,readiness会先变成
OUT_OF_SERVICE,容器编排平台(K8s)据此把该 Pod 从 Service
摘除、不再转发新流量,等存量请求处理完再真正退出——这就是生产环境「无损发版」的标准链路。
(2)容器化部署(Dockerfile)
# 用 JRE 而非 JDK,镜像更小、攻击面更小
FROM eclipse-temurin:17-jre
# 创建非 root 用户,避免容器内以 root 运行(安全最佳实践)
RUN groupadd -r app && useradd -r -g app app
WORKDIR /app
# 只 COPY 最终 fat jar
COPY target/order-service-1.0.0.jar app.jar
USER app
# ENTRYPOINT 形式:容器内 PID 1 就是 java 进程,能正确收到 SIGTERM 触发优雅停机
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
# 构建并运行
docker build -t order-service:1.0.0 .
docker run -d -p 8080:8080
-e SPRING_PROFILES_ACTIVE=prod
-e DB_PASSWORD='xxx'
--name order-service order-service:1.0.0
# 优雅停机验证:docker stop 默认发 SIGTERM,会走优雅停机
docker stop order-service
关键点提示:Dockerfile 用 ENTRYPOINT(exec
形式)而不是 CMD + shell 形式,是为了让 java
进程成为容器 PID 1,从而能正确接收 docker stop 发来的
SIGTERM、触发优雅停机;如果套一层 sh -c,信号会被 shell
吞掉,优雅停机就失效了。
本章小结:spring-boot-maven-plugin
打可执行 fat jar(选 .jar 不选
.jar.original);部署用「一个 jar + 运行时
Profile」策略;开启 server.shutdown=graceful
实现优雅停机,发版不用 kill -9;用 Actuator
健康检查配合容器编排实现无损发版。
5.
生产级实战项目:订单服务(order-service)
把本篇 6
章的知识串成一个完整可运行的项目。业务场景:一个极简的订单服务,具备——
- 统一响应体
ApiResult/ 错误码ErrorCode/
业务异常BizException/ 全局异常处理; - 类型安全配置类
AppProperties+ 校验 + Profile 多环境 +
敏感信息外部化; - 生产级
logback-spring.xml+ MDC traceId; - 使用第 5 章自定义的
id-generator-spring-boot-starter
生成订单号; - 优雅停机。
5.1 目录结构
order-service
├── pom.xml
├── src/main/java/com/example/order
│ ├── OrderApplication.java # 启动类(根包)
│ ├── controller/OrderController.java
│ ├── service/OrderService.java
│ ├── properties/AppProperties.java # app.* 配置类
│ ├── config/AppConfig.java # 启用配置类
│ ├── exception/BizException.java
│ ├── exception/GlobalExceptionHandler.java
│ ├── common/ApiResult.java
│ ├── common/ErrorCode.java
│ └── common/TraceIdFilter.java
└── src/main/resources
├── application.yml
├── application-dev.yml
├── application-prod.yml
└── logback-spring.xml
5.2 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>order-service</artifactId>
<version>1.0.0</version>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- 自定义 starter:引入即拥有 IdGenerator Bean -->
<dependency>
<groupId>com.example</groupId>
<artifactId>id-generator-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
5.3 启动类
package com.example.order;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/** 启动类位于根包 com.example.order,保证默认组件扫描覆盖所有子包 */
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
5.4
统一响应体与错误码(系列公共类)
ApiResult、ErrorCode、BizException、GlobalExceptionHandler
与阶段 1
给出的逐字一致,此处引用不重写(跨文章一致性要求)。为让项目自包含,把它们放在
common 与 exception 包,实现见阶段 1。
5.5 类型安全配置类 + 校验
package com.example.order.properties;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
/** 订单服务的业务配置,绑定 app.* 前缀 */
@Data
@Validated
@ConfigurationProperties(prefix = "app")
public class AppProperties {
/** 服务名(会出现在日志里,便于多实例区分) */
@NotBlank
private String name;
/** 下单超时时间(秒) */
@Min(1)
private int orderTimeoutSeconds = 30;
/** 订单号生成策略 */
@NotBlank
private String idStrategy = "snowflake";
}
package com.example.order.config;
import com.example.order.properties.AppProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/** 注册配置类:配置类不碰 @Component,职责纯粹,便于测试 */
@Configuration
@EnableConfigurationProperties(AppProperties.class)
public class AppConfig {
}
5.6 业务层(用 starter
生成订单号)
package com.example.order.service;
import com.example.idgenerator.IdGenerator;
import com.example.order.properties.AppProperties;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
@Slf4j
public class OrderService {
private final IdGenerator idGenerator; // 来自自定义 starter
private final AppProperties appProperties; // 类型安全配置
/** 创建订单:生成订单号并记录关键日志 */
public long createOrder(String sku) {
long orderId = idGenerator.nextId();
// 记录关键业务节点:谁、买了什么、订单号多少
log.info("创建订单成功 orderId={}, sku={}, strategy={}",
orderId, sku, appProperties.getIdStrategy());
return orderId;
}
}
5.7 接口层(带参数校验 +
统一响应)
package com.example.order.controller;
import com.example.order.common.ApiResult;
import com.example.order.service.OrderService;
import jakarta.validation.constraints.NotBlank;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/order")
@RequiredArgsConstructor
@Validated
@Slf4j
public class OrderController {
private final OrderService orderService;
@GetMapping("/create")
public ApiResult<Long> create(@RequestParam @NotBlank(message = "sku 不能为空") String sku) {
long orderId = orderService.createOrder(sku);
return ApiResult.ok(orderId);
}
}
5.8 配置文件
# application.yml(公共底座)
spring:
application:
name: order-service
profiles:
active: dev
lifecycle:
timeout-per-shutdown-phase: 30s # 优雅停机等待时间
server:
port: 8080
shutdown: graceful # 开启优雅停机
app:
name: ${APP_NAME:order-service-01} # 服务名可从环境变量覆盖,多实例区分
order-timeout-seconds: 30
id-strategy: snowflake
id:
generator:
worker-id: ${WORKER_ID:1} # 多实例时每台不同,用环境变量注入
datacenter-id: 1
# application-dev.yml
logging:
level:
com.example.order: DEBUG
app:
name: order-service-dev
# application-prod.yml
logging:
level:
com.example.order: INFO
app:
name: order-service-prod
5.9 运行与验证
# 1. 先安装自定义 starter(第 5 章)
cd id-generator-spring-boot-starter && mvn clean install && cd ..
# 2. 打包
cd order-service && mvn clean package
# 3. 本地跑(dev)
java -jar target/order-service-1.0.0.jar
# 4. 验证接口
curl http://localhost:8080/api/order/create?sku=1001
# {"code":0,"message":"success","data":1837429500235776,"traceId":"a1b2c3..."}
# 5. 验证参数校验(不传 sku)
curl http://localhost:8080/api/order/create
# {"code":40001,"message":"sku 不能为空","data":null,"traceId":"a1b2c3..."}
# 6. 生产环境跑(注入环境变量)
SPRING_PROFILES_ACTIVE=prod WORKER_ID=2 java -jar target/order-service-1.0.0.jar
# 7. 验证优雅停机
kill <pid>
# 日志出现 Commencing graceful shutdown ... Graceful shutdown complete
6. 常见坑与排错指南
| 坑 / 现象 | 原因 | 解决方案 |
|---|---|---|
启动类不在包根目录,访问接口报 No qualifying bean |
@ComponentScan 默认只扫启动类所在包及子包 |
启动类上移到根包,或显式@ComponentScan(basePackages=...) |
| 密码/密钥硬编码在 yml 里提交了 Git | 敏感信息进仓库即永久泄露,删掉也留历史 | 改用 ${ENV_VAR} 占位符 +环境变量/配置中心注入,并轮换已泄露密钥 |
| 改了 yml 配置却不生效 | 配置被更高优先级来源覆盖(命令行/环境变量) | 记住优先级:命令行 > 系统属性 > 环境变量 > profile > 默认;用 --debug 或 actuator 查看实际生效值 |
用 @Value 读类型不匹配的配置,启动才报错 |
@Value 类型不安全、无校验 |
改用 @ConfigurationProperties +@Validated,启动时 fail-fast |
| 自动配置「不生效」但没报错 | @ConditionalOnClass 缺类 /@ConditionalOnMissingBean 被自定义 Bean 顶掉 |
--debug 看 Positive/Negativematches,按报告对症下药 |
| 自定义 starter 引入后毫无反应 | AutoConfiguration.imports 文件名写错(如写了老的spring.factories) |
检查META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports路径一字不差 |
| 日志只打控制台不落文件,线上无法回溯 | 没配 RollingFileAppender | 配 logback-spring.xml 滚动文件 +maxHistory 归档 |
用 System.out.println 打日志 |
无级别、无格式、无 traceId、性能差 | 统一 @Slf4j,禁止 System.out |
异常只 log.error(e.getMessage()) |
丢了堆栈,无法定位出错行 | log.error("业务失败", e) 带异常对象 |
| MDC traceId 串号,不同请求同一个 id | 请求结束没 MDC.remove(),Tomcat 线程池复用线程 |
在过滤器的 finally 里清理 MDC |
依赖版本不统一,运行时 NoSuchMethodError |
没继承 spring-boot-starter-parent 统一版本 |
继承父 POM,或 dependencyManagement 统一管理 |
打出的 jar 用 java -jar 跑报「no main manifest」 |
用了 .jar.original 那个包 |
用 spring-boot-maven-plugin 打的 .jar(fatjar) |
发版 kill -9 导致正在处理的请求/事务中断 |
-9 是强杀,不触发优雅停机 |
开 server.shutdown=graceful,用SIGTERM( kill 不带 -9)停机 |
部署时把 application-prod.yml 也打进通用包导致误用 |
把环境差异固化进了 jar | 采用「一个 jar + 运行时 Profile」策略,敏感配置外部化 |
8. 总结与延伸阅读
本篇核心一句话:Spring Boot 的「自动配置」是一套基于
classpath 与已有 Bean
的可预测条件判断机制,而非黑魔法;「配置管理」与「日志」则是把自动配置落地到生产的工程纪律。
你从「跑通第一个接口」出发,拆解了
@SpringBootApplication,吃透了
@EnableAutoConfiguration → AutoConfigurationImportSelector → AutoConfiguration.imports → @Conditional
的完整链路,学会了用 --debug
读「自动配置报告」排查一切「配置不生效」;掌握了
@ConfigurationProperties + 校验、配置优先级、Profile
切换与敏感信息外部化;配置了生产级日志(滚动、按包隔离、traceId);亲手写了一个自定义
starter;最后打包部署并开启优雅停机。这套能力是后续 Web 开发(阶段
3)、数据访问(阶段 4)、分布式组件(阶段 10)的通用底座。
延伸阅读:
- Spring
Boot 官方 Reference:Core Features → Externalized Configuration ——
配置优先级与外部化的权威说明; - Spring
Boot 官方 Reference:Creating Your Own Auto-configuration —— 自定义
starter 官方指南; - Spring
Boot 官方 Reference:Logging —— 日志体系与
logback-spring.xml支持; - 《Spring Boot 实战》(Craig Walls 著)—— 经典入门书,案例丰富;
- 阶段 10 深度拆解文档 —— 分布式
ID(雪花算法时钟回拨)与配置中心,是本章自定义 starter
与敏感信息外部化的进阶。
本文遵循系列统一写作规范:8
段模板、统一代码约定(ApiResult/ErrorCode/BizException/GlobalExceptionHandler)、Spring
Boot 3.2.x / JDK 17。