Spring Boot 硬核入门与进阶实战教程
从"能跑起来"到"看懂原理、能做技术选型、能上生产"。本教程面向已有一定编程基础(尤其是有 Go / Python 背景)的学习者,注重原理剖析、技术对比和一线实用性,覆盖 Spring Boot 4.x(Spring Framework 7)最新一代技术栈。
版本基准说明:截至 2026 年中,Spring 生态已完成一次代际更替——Spring Boot 4.0(2025 年 11 月发布)与 Spring Boot 4.1(2026 年 6 月发布)已成为当前主线,底层基于全新的 Spring Framework 7 与 Jakarta EE 11,最低要求 Java 17,官方推荐搭配 Java 21 LTS 或 Java 25 LTS。市面上大量存量项目仍运行在 Spring Boot 3.x(3.5 是 3.x 最后一个版本,已于 2026 年 6 月 30 日结束社区免费支持)。本教程会以 Spring Boot 4.x 为主线讲解,并在关键处标注与 3.x 的差异,方便你对接现有项目和面试考察点。
目录
- 写在前面:这份教程要解决什么问题
- Spring Boot 是什么,为什么会有它
- 环境搭建与第一个项目
- 自动配置原理硬核剖析
- IoC / DI 深入:Bean 生命周期与循环依赖
- 配置管理:从 yaml 到配置中心
- Web 开发:MVC、WebFlux 与 REST 实战
- 数据访问层:JPA / MyBatis / MyBatis-Plus 全面对比
- 安全:Spring Security / Sa-Token / JWT / OAuth2
- 微服务与云原生扩展(前沿)
- Spring Boot 4.x 前沿特性硬核解读
- 可观测性与生产级实践
- 测试:JUnit5 / Mockito / Testcontainers
- 性能调优实战
- 部署与 DevOps:Docker / K8s
- 横向对比:Spring Boot vs Go / Node.js / Python 生态
- 完整实战项目:图书管理系统
- 学习路线图与资源
- 硬核面试题精选
0. 写在前面:这份教程要解决什么问题
市面上 Spring Boot 教程大多止步于"CRUD + 截图 + 跑起来就完事",这份教程刻意反着来,遵循三个原则:
- 每个特性都讲"为什么",不只讲"怎么做"。比如不只是告诉你加
@Transactional,而是讲清楚它底层靠 AOP 代理实现,为什么在同类内部方法调用时会失效。 - 凡是有可替代方案的地方,都给对比表。Spring 生态选择太多(JPA vs MyBatis,Spring Cloud vs Dubbo,Spring Security vs Sa-Token),教程的价值在于告诉你什么场景选什么,而不是只讲一种。
- 紧跟当前技术代际。2025-2026 年 Spring 生态经历了 Boot 3 → Boot 4 的大版本跃迁(类似当年 Boot 2 → Boot 3 的 Jakarta 命名空间迁移),加上虚拟线程、GraalVM 原生镜像、Spring AI 的成熟,"前沿"不是噱头,是真的有东西要学。
阅读建议:如果你只是想跑通一个项目,看第 1、2、6、7 章即可;如果你要做技术选型或者准备面试,把对比表和第 3、4、9、10、13 章啃透。
1. Spring Boot 是什么,为什么会有它
1.1 从 Spring 到 Spring Boot 的进化史
Spring Framework(2003年) 诞生的初衷是解决 EJB(Enterprise JavaBeans)过度设计的问题,核心贡献是 IoC 容器和 AOP,让 Java 企业级开发从"侵入式重量级"变成"POJO + 配置"。但配置本身很快变成了新的地狱——一个中型项目动辄几十个 XML 文件,<bean> 标签写到手软,还要手动管理各个框架(Spring MVC、Hibernate、Quartz……)之间的版本兼容性。
Spring Boot(2014年发布 1.0) 就是为了终结这种"配置地狱"而生的,它不是一个新框架,而是 Spring 生态的一种"opinionated"(有主见的)打包和引导方式,核心解决三件事:
| 痛点 | Spring Boot 的解法 |
|---|---|
| 依赖版本对不齐、互相打架 | 起步依赖(Starter) + 依赖仲裁(BOM),一个 spring-boot-starter-web 背后锁定了几十个包的兼容版本 |
| 大量样板配置代码 | 自动配置(Auto-Configuration),根据 classpath 里有什么 jar 包、有什么 Bean,自动帮你配好八成的常见场景 |
| 部署要装 Tomcat、配 web.xml | 内嵌容器(Tomcat/Jetty/Netty),java -jar app.jar 直接起服务,天生适配容器化 |
1.2 对比:传统 Spring / SSM / Spring Boot / Jakarta EE
| 维度 | 传统 Spring (XML) | SSM (Spring+SpringMVC+MyBatis) | Spring Boot | Jakarta EE (原 JavaEE) |
|---|---|---|---|---|
| 配置方式 | 大量 XML | XML + 部分注解 | 注解 + 极少配置,约定优于配置 | 标准化注解 + 部署描述符 |
| 依赖管理 | 手动对齐版本 | 手动对齐版本 | Starter + BOM 自动仲裁 | 依赖具体应用服务器实现 |
| 启动部署 | 外置容器 + war | 外置容器 + war | 内嵌容器,jar 直接跑 | 外置应用服务器(WildFly/GlassFish) |
| 学习曲线 | 陡峭 | 中等 | 平缓(入门),但深挖原理仍有厚度 | 陡峭,且各厂商实现差异大 |
| 云原生适配 | 差 | 差 | 好(容器化、K8s、Actuator 健康检查天然契合) | 一般,近年通过 MicroProfile 改善 |
| 目前地位 | 历史遗留项目维护 | 大量存量国内项目(政企常见) | 事实上的 Java 后端标准 | 规范标准,但市场份额被 Spring 生态挤压 |
一句话总结:Spring Boot 不是替代 Spring,而是"把 Spring 用得更顺手"的一层封装;理解这一点很重要,因为后面讲自动配置原理时,你会发现 Spring Boot 的魔法本质上还是 Spring 的 BeanFactoryPostProcessor、@Conditional 这些老机制在发力,只是被封装得让你感知不到。
2. 环境搭建与第一个项目
2.1 JDK 版本怎么选:Java 17 / 21 / 25 对比
Spring Boot 4.x 最低要求 Java 17,官方支持一路到 Java 26;Spring Boot 3.x 系列同样最低 Java 17。截至 2026 年,Java 生态的 LTS(长期支持版本)序列是 8 → 11 → 17 → 21 → 25(注意从 21 开始 LTS 间隔从 3 年缩短到 2 年),实际项目选型建议:
| 版本 | 发布时间 | 定位 | 关键特性 | 建议场景 |
|---|---|---|---|---|
| Java 17 LTS | 2021.9 | 存量主力 | 密封类、Record、模式匹配预览 | 老项目维护,Spring Boot 3.x 最低基线 |
| Java 21 LTS | 2023.9 | 当前企业主流 | 虚拟线程(正式)、Record Pattern、分代 ZGC | 新项目首选,生态兼容性最好,各大云厂商镜像齐全 |
| Java 25 LTS | 2025.9 | 最新 LTS | 结构化并发(Structured Concurrency 转正)、值对象预览、紧凑对象头(省内存) | 想用最新特性、且团队敢于跟进新版本的团队 |
| Java 26(非LTS) | 2026.3 | 尝鲜 | HTTP/3 客户端标准化、AOT 对象缓存 | 仅用于验证新特性,不建议生产 |
给你的建议(结合你熟悉 Go 的背景类比):Java 的虚拟线程之于 Java,约等于 Goroutine 之于 Go——都是"用同步写法获得异步吞吐"的方案,但实现机制不同(虚拟线程是 JVM 层面的 M:N 调度,Goroutine 是 Go runtime 自己的调度器,两者都不是操作系统线程)。第 10 章会详细对比。
结论:新项目直接上 Java 21 LTS(生态最稳),追新可以上 Java 25 LTS。不要再用 Java 8 起新 Spring Boot 4.x 项目——直接不兼容。
2.2 Maven vs Gradle
| 维度 | Maven | Gradle |
|---|---|---|
| 配置文件 | pom.xml(XML) | build.gradle / build.gradle.kts(Groovy/Kotlin DSL) |
| 构建速度 | 较慢,全量执行 | 快,增量构建 + 构建缓存 + 守护进程 |
| 学习曲线 | 平缓,约定死板但省心 | 陡峭,灵活但需要理解 Task/Plugin 机制 |
| 依赖管理 | 声明式,仲裁规则简单(最短路径优先) | 更强大但规则复杂 |
| 国内生态 | 教学、企业项目首选,中文资料最全 | Android/大型多模块项目常用 |
| 与 Spring Boot 4 关系 | 官方脚手架首选之一 | 4.0 起要求 Gradle 8.14+(Gradle 9 支持) |
教学与入门场景强烈建议 Maven:出错信息更直观,中文社区资料最完善,mvn dependency:tree 排查依赖冲突比 Gradle 上手快得多。等你要做大型多模块单体或者关注构建速度时再迁移 Gradle。
2.3 用 Spring Initializr 创建第一个项目
访问 https://start.spring.io(或在 IDEA/VS Code 里用内置的 Spring Initializr 插件),选择:
- Project:Maven
- Language:Java
- Spring Boot:选最新稳定版(4.1.x)
- 依赖:
Spring Web、Spring Boot DevTools、Lombok(可选,减少 getter/setter 样板代码)
生成后的核心目录结构:
my-app/
├── pom.xml # Maven 依赖与构建配置
├── src/main/java/com/example/demo/
│ └── DemoApplication.java # 程序入口
├── src/main/resources/
│ ├── application.yml # 核心配置文件
│ └── static/, templates/ # 静态资源、模板
└── src/test/java/... # 测试代码2.4 第一个 Controller
@RestController
@RequestMapping("/api/hello")
public class HelloController {
@GetMapping
public Map<String, Object> hello(@RequestParam(defaultValue = "world") String name) {
return Map.of(
"message", "Hello, " + name + "!",
"timestamp", Instant.now().toString()
);
}
}@RestController = @Controller + @ResponseBody,返回值自动被 Jackson 序列化成 JSON。启动主类跑 mvn spring-boot:run,访问 http://localhost:8080/api/hello?name=Maple 即可看到响应——这一步看起来平平无奇,但背后自动配置替你做了:内嵌 Tomcat 启动、DispatcherServlet 注册、Jackson HttpMessageConverter 注册、异常处理器兜底……第 3 章拆开讲这些魔法是怎么实现的。
3. 自动配置原理硬核剖析
这一章是本教程"硬核"的第一个体现点,也是面试高频区。目标:讲清楚 @SpringBootApplication 到底做了什么,"约定优于配置"的魔法是怎么工作的。
3.1 拆解 @SpringBootApplication
这个注解其实是三个注解的组合:
@SpringBootConfiguration // 本质是 @Configuration,标记这是一个配置类
@EnableAutoConfiguration // 核心:开启自动配置机制
@ComponentScan // 扫描当前包及子包下的 @Component/@Service/@Repository/@Controller
public @interface SpringBootApplication { ... }真正的"魔法"在 @EnableAutoConfiguration 上。
3.2 自动配置是怎么被发现的:Boot 3 与 Boot 4/3 的差异
在 Spring Boot 2.x 时代,自动配置类靠 META-INF/spring.factories 文件里的一行配置来注册:
# spring.factories(Boot 2.x 写法,已过时)
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.MyAutoConfiguration,\
com.example.AnotherAutoConfigurationSpring Boot 3.x 起(沿用到 4.x)改成了性能更好、加载更快的写法,文件路径变成:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports内容就是一行一个全限定类名,不再需要 key-value 格式,Boot 启动时通过 ImportCandidates 扫描这个文件加载自动配置类,比老的 SpringFactoriesLoader 更快、更利于编译期处理(AOT)——这也是为什么 GraalVM 原生镜像在 Boot 3+ 之后支持度大幅提升的原因之一(详见第 10 章)。
3.3 条件注解(@Conditional 系列):自动配置为什么"聪明"
自动配置类内部大量使用条件注解,决定某个 Bean 要不要被创建:
| 注解 | 作用 |
|---|---|
@ConditionalOnClass | classpath 里存在指定类才生效(比如检测到 Redisson 就装配 Redisson 相关 Bean) |
@ConditionalOnMissingBean | 容器里没有该类型 Bean 时才生效——这就是为什么你自己定义一个 DataSource Bean,官方自动配置的默认数据源会自动让位 |
@ConditionalOnProperty | 根据配置文件某个属性值决定是否生效 |
@ConditionalOnWebApplication | 是否是 Web 应用(Servlet 型还是响应式型还进一步细分) |
@ConditionalOnMissingClass | 反过来,classpath 里没有某类才生效 |
举个具体例子,DataSourceAutoConfiguration 的核心逻辑简化后大概是:
@AutoConfiguration
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory")
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public DataSource dataSource(DataSourceProperties properties) {
return properties.initializeDataSourceBuilder().build();
}
}理解了这套机制,你就明白了:Spring Boot 的"约定优于配置"不是黑魔法,而是"预先写好了一堆按条件生效的配置类,帮你把 90% 场景的样板代码写掉了"。所以调试自动配置问题的黄金手段是——在 application.yml 里加一行 debug: true,启动日志会打印 CONDITIONS EVALUATION REPORT,清楚地告诉你每个自动配置类是 Positive(生效)还是 Negative(未生效,附带原因)。
3.4 实战:手写一个自己的 Starter
理解原理后,自己写一个 Starter 并不难,三步走:
- 定义配置属性类(对接
application.yml):
@ConfigurationProperties(prefix = "myapp.greeting")
public class GreetingProperties {
private String prefix = "Hello";
private String suffix = "!";
// getter/setter 省略
}- 写自动配置类:
@AutoConfiguration
@EnableConfigurationProperties(GreetingProperties.class)
@ConditionalOnClass(GreetingService.class)
public class GreetingAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public GreetingService greetingService(GreetingProperties props) {
return new GreetingService(props.getPrefix(), props.getSuffix());
}
}- 注册到自动配置发现文件:在
src/main/resources/META-INF/spring/下新建org.springframework.boot.autoconfigure.AutoConfiguration.imports,写入:
com.example.myapp.autoconfigure.GreetingAutoConfiguration打包成 jar 后,任何项目只要把这个 jar 加进依赖,GreetingService 就会自动被装配好——这正是所有第三方 Starter(MyBatis-Plus Starter、Sa-Token Starter、阿里云 OSS Starter 等)的实现套路,没有任何魔法,全是约定 + 条件装配。
4. IoC / DI 深入:Bean 生命周期与循环依赖
4.1 Bean 完整生命周期(面试高频,按顺序背下来意义不大,理解每一步"为什么在那里"更重要)
① 实例化(Instantiation)——反射调用构造器,得到"半成品"对象
↓
② 属性填充(Populate)——依赖注入,@Autowired 字段/setter 被赋值
↓
③ Aware 接口回调——如 BeanNameAware、ApplicationContextAware,让 Bean 感知容器
↓
④ BeanPostProcessor.postProcessBeforeInitialization
↓
⑤ 初始化:@PostConstruct → InitializingBean.afterPropertiesSet() → 自定义 init-method
↓
⑥ BeanPostProcessor.postProcessAfterInitialization
(⭐ AOP 代理就是在这一步生成的——所以 @Transactional、@Async 等注解本质是靠这里插入代理对象实现的)
↓
⑦ Bean 就绪,可以被使用
↓
⑧ 容器关闭时:@PreDestroy → DisposableBean.destroy() → 自定义 destroy-method这里最容易被问到、也最能体现"懂不懂原理"的一点:@Transactional、@Async、@Cacheable 这些注解为什么在同一个类内部方法互相调用时会"失效"? 答案就在第⑥步——AOP 代理是在 Bean 初始化完成后生成的一个"外壳"对象,Spring 容器里真正持有的、被注入到别处的是这个代理对象。但如果你在同一个类内部用 this.otherMethod() 调用另一个标了 @Transactional 的方法,走的是 this 指针,即原始对象,根本没经过代理外壳,注解自然不生效。解决方案:注入自身的代理对象(AopContext.currentProxy() 或自我注入),或者把这两个方法拆到不同的类里。
4.2 循环依赖与三级缓存(硬核,源码级理解)
A 依赖 B,B 又依赖 A,Spring 是怎么解决这种循环依赖的?答案是三级缓存(仅对单例 Bean 的字段注入生效,构造器注入的循环依赖 Spring 无法解决,会直接抛 BeanCurrentlyInCreationException):
| 缓存 | 名称 | 存的是什么 |
|---|---|---|
| 一级缓存 | singletonObjects | 完全初始化好的成品 Bean |
| 二级缓存 | earlySingletonObjects | 提前暴露的"半成品"Bean(已实例化但未完成属性填充) |
| 三级缓存 | singletonFactories | Bean 的 ObjectFactory,可以按需生成早期引用(这一层的存在专门是为了兼容 AOP 代理) |
简化流程:创建 A 时,A 刚实例化完(还没填充属性)就把自己的 ObjectFactory 放进三级缓存 → 开始填充 A 的属性,发现要注入 B → 去创建 B → B 填充属性时发现要注入 A → 从缓存里找 A,三级缓存命中,通过 ObjectFactory.getObject() 拿到 A 的早期引用(如果 A 需要被 AOP 代理,这一步就完成代理),放入二级缓存并从三级缓存删除(因为已经决定了到底注入原始对象还是代理对象,不需要重复判断)→ B 创建完成 → 回头继续完成 A 的属性填充 → A 也创建完成,晋升进一级缓存。
为什么非要三级,而不是两级就够? 因为要处理"A 被 AOP 代理"这种情况——如果只有二级缓存,没法保证提前暴露给 B 的 A 和最终 A 初始化完成后暴露给外界的 A 是同一个对象(一个是代理,一个不是),三级缓存通过 ObjectFactory 延迟决定,保证了这个一致性。
4.3 依赖注入方式对比:构造器注入 vs 字段注入 vs Setter 注入
| 方式 | 示例 | 优点 | 缺点 |
|---|---|---|---|
| 字段注入 | @Autowired private UserService userService; | 代码最少,看起来"干净" | 不推荐:无法声明 final,无法在容器外单元测试直接 new,隐藏了真实依赖数量(容易堆积成"上帝类"),循环依赖时会被"悄悄允许"从而掩盖设计问题 |
| Setter 注入 | @Autowired public void setUserService(...) | 支持可选依赖、支持运行时重新注入 | 依赖可能处于未设置状态,存在空指针风险 |
| 构造器注入 | public UserController(UserService s) { this.userService = s; } | 官方推荐:依赖不可变(配合 final)、依赖关系一目了然、天然杜绝循环依赖(编译期/启动期就会报错,倒逼你重构设计) | 依赖多时构造器参数长(配合 Lombok @RequiredArgsConstructor 可解决) |
结论与你 Go 背景的类比:构造器注入本质上就是 Go 里"显式传参构造 struct"的思路(NewUserService(db, cache)),字段注入则更像是隐式的全局单例查找。Spring 官方文档和 IDEA 的 Inspection 都明确建议用构造器注入 + final 字段,这也是团队协作时能一眼看出"这个类到底依赖了什么"的最直接方式。
5. 配置管理:从 yaml 到配置中心
5.1 properties vs yaml
# application.properties
server.port=8080
spring.datasource.url=jdbc:mysql://localhost:3306/demo# application.yml —— 推荐,层级更清晰,尤其是复杂嵌套配置
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/demoyaml 优势明显:层级配置更直观、支持列表和 map 更自然,是目前实际项目的主流选择;properties 的优势仅剩"每行独立,写错一行不会因为缩进错误影响一大片",教学场景两种都要会读。
5.2 多环境 Profile
# application.yml —— 公共配置
spring:
application:
name: demo-app
profiles:
active: dev # 激活哪个环境
---
spring:
config:
activate:
on-profile: dev
server:
port: 8080
---
spring:
config:
activate:
on-profile: prod
server:
port: 80启动时用 --spring.profiles.active=prod 或环境变量 SPRING_PROFILES_ACTIVE=prod 切换,这是容器化部署时最常用的注入方式(K8s 的 env 字段直接传)。
5.3 外部化配置优先级(硬核,容易被面试问到"配置到底谁生效")
Spring Boot 支持从十几种来源读配置,按优先级从高到低(高的覆盖低的)大致顺序:
- 命令行参数(
--server.port=9000) SPRING_APPLICATION_JSON环境变量/属性中的 JSONServletConfig/ServletContext初始化参数- Java 系统属性(
System.getProperties()) - 操作系统环境变量
- 随机值配置(
RandomValuePropertySource,如${random.uuid}) - jar 包外部的 profile 专属配置文件(
application-{profile}.yml) - jar 包内部的 profile 专属配置文件
- jar 包外部的
application.yml - jar 包内部的
application.yml @PropertySource注解引入的配置- 默认属性(
SpringApplication.setDefaultProperties)
记忆口诀:命令行 > 环境变量 > 外部配置文件 > 内部(打包进 jar 的)配置文件 > 代码里的默认值。生产环境常用套路是"jar 包里放开发默认配置,用环境变量或外挂 application-prod.yml 覆盖敏感配置(数据库密码等)",这样镜像可以做到"一次构建,多环境部署"。
5.4 @ConfigurationProperties + 校验
@ConfigurationProperties(prefix = "myapp.mail")
@Validated
public record MailProperties(
@NotBlank String host,
@Min(1) @Max(65535) int port,
@Email String from
) {}Spring Boot 3+ 起 @ConfigurationProperties 原生支持绑定到 Record(不可变配置对象,天然线程安全),配合 @Validated 可以在启动阶段就校验配置合法性,比运行时才发现"端口配错了"体验好得多——这也是"快速失败(fail-fast)"理念在配置管理上的体现。
5.5 配置中心:Nacos / Apollo vs 本地配置文件
| 维度 | 本地配置文件 | Nacos / Apollo 配置中心 |
|---|---|---|
| 修改生效 | 需要重启或重新部署 | 动态推送,无需重启(配合 @RefreshScope) |
| 多实例一致性 | 需要人工保证每台机器配置一致 | 中心化管理,天然一致 |
| 权限与审计 | 无 | 支持修改记录、灰度发布、权限管控 |
| 适用场景 | 单体应用、小项目 | 微服务集群、需要动态调参(限流阈值、开关)的场景 |
小项目/教学场景,本地 yaml 完全够用;一旦进入微服务架构,配置中心几乎是标配,详见第 9 章。
6. Web 开发:MVC、WebFlux 与 REST 实战
6.1 Spring MVC 请求处理流程
客户端请求
↓
DispatcherServlet(前端控制器,所有请求的统一入口)
↓
HandlerMapping(根据 URL 找到该由哪个 Controller 方法处理)
↓
HandlerAdapter(适配调用具体的 Controller 方法)
↓
拦截器 preHandle → Controller 方法执行 → 拦截器 postHandle
↓
ViewResolver(如果返回视图名)或 HttpMessageConverter(如果是 @ResponseBody / @RestController,序列化成 JSON)
↓
拦截器 afterCompletion
↓
响应返回客户端理解这条链路的意义:拦截器、全局异常处理器、参数校验失败处理,本质上都是在这条链路的某个节点上插入自己的逻辑。
6.2 Spring MVC vs WebFlux 全方位对比(重点,很多人选错)
| 维度 | Spring MVC | Spring WebFlux |
|---|---|---|
| 编程模型 | 阻塞式(Servlet),一个请求一个线程 | 响应式(Reactive Streams),基于 Mono/Flux |
| 底层容器 | Tomcat / Jetty(Servlet 容器) | Netty(默认,非阻塞 I/O) |
| 并发模型 | 传统线程池,虚拟线程加持后阻塞代价大幅降低(见第10章) | 事件循环 + 少量线程处理大量连接 |
| 学习曲线 | 平缓,符合大多数人直觉 | 陡峭,需要理解背压(Backpressure)、操作符链、错误处理范式的转变 |
| 调试难度 | 简单,堆栈清晰 | 较难,异步调用栈不直观 |
| 吞吐场景 | CPU 密集型、常规业务系统 | 高并发 I/O 密集型(网关、代理、大量下游调用聚合的场景) |
| 生态成熟度 | 极其成熟,几乎所有第三方库都原生支持 | 逐渐成熟,部分老旧库(如某些 JDBC 驱动)需要额外适配(R2DBC 替代 JDBC) |
| 什么时候选它 | 90% 的业务系统,尤其是数据库交互密集、逻辑复杂的场景 | API 网关、BFF 聚合层、需要对接大量外部 HTTP 服务且追求极致吞吐的场景 |
一个容易被忽略的真相:自从 Java 21 虚拟线程正式可用后,Spring MVC 配合虚拟线程(spring.threads.virtual.enabled=true)在很多场景下已经能达到接近 WebFlux 的吞吐表现,同时保留了阻塞式编程的简单心智模型。这直接改变了"要高并发就必须上 WebFlux"的旧共识——第 10 章会详细展开这个对比。给你的建议:除非你的场景明确是"网关聚合大量下游调用"这类强 I/O 密集型,否则默认选 MVC + 虚拟线程,WebFlux 的心智负担收益比在多数业务场景下并不划算。
6.3 RESTful API 设计规范速览
@RestController
@RequestMapping("/api/v1/books")
@RequiredArgsConstructor // Lombok:为 final 字段生成构造器,配合构造器注入
public class BookController {
private final BookService bookService;
@GetMapping // GET /api/v1/books 查询列表
public Page<BookDTO> list(BookQuery query, Pageable pageable) { ... }
@GetMapping("/{id}") // GET /api/v1/books/1 查询单个
public BookDTO get(@PathVariable Long id) { ... }
@PostMapping // POST /api/v1/books 创建
public ResponseEntity<BookDTO> create(@Valid @RequestBody BookCreateRequest req) {
BookDTO created = bookService.create(req);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
@PutMapping("/{id}") // PUT /api/v1/books/1 整体更新
public BookDTO update(@PathVariable Long id, @Valid @RequestBody BookUpdateRequest req) { ... }
@DeleteMapping("/{id}") // DELETE /api/v1/books/1 删除
public ResponseEntity<Void> delete(@PathVariable Long id) {
bookService.delete(id);
return ResponseEntity.noContent().build();
}
}设计要点:资源用名词复数(/books 而不是 /getBooks)、用 HTTP 方法表达动作、版本号放路径前缀(/api/v1/)方便后续平滑升级、状态码要用对(201 创建成功、204 无内容、404 未找到、422 参数校验失败比笼统的 400 更精确)。
6.4 参数校验 + 全局异常处理
public record BookCreateRequest(
@NotBlank(message = "书名不能为空") String title,
@NotBlank String author,
@Positive(message = "价格必须为正数") BigDecimal price,
@Past(message = "出版日期不能是未来") LocalDate publishDate
) {}
@RestControllerAdvice // 全局异常处理,捕获所有 Controller 抛出的异常
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class) // @Valid 校验失败
@ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
public ApiError handleValidation(MethodArgumentNotValidException ex) {
String detail = ex.getBindingResult().getFieldErrors().stream()
.map(e -> e.getField() + ": " + e.getDefaultMessage())
.collect(Collectors.joining("; "));
return new ApiError("VALIDATION_FAILED", detail);
}
@ExceptionHandler(BusinessException.class) // 自定义业务异常
public ResponseEntity<ApiError> handleBusiness(BusinessException ex) {
return ResponseEntity.status(ex.getStatus())
.body(new ApiError(ex.getCode(), ex.getMessage()));
}
@ExceptionHandler(Exception.class) // 兜底,防止把堆栈信息暴露给前端
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ApiError handleUnknown(Exception ex) {
log.error("未捕获异常", ex);
return new ApiError("INTERNAL_ERROR", "服务器内部错误");
}
}@RestControllerAdvice 让异常处理逻辑与业务代码彻底解耦,Controller 里只需要专注业务逻辑、直接抛异常即可,这是生产项目里几乎必备的模式。
6.5 拦截器 vs 过滤器 vs AOP:三者怎么选
这三者都能"在业务代码之外插入通用逻辑",很多初学者分不清该用哪个:
| 维度 | Filter(过滤器) | Interceptor(拦截器) | AOP(切面) |
|---|---|---|---|
| 所属规范 | Servlet 规范,Spring MVC 之外的更底层机制 | Spring MVC 自己的机制 | Spring 框架级(基于动态代理) |
| 能拿到什么 | 原始 ServletRequest/ServletResponse | HandlerMethod,能拿到具体要执行的 Controller 方法信息 | 任意方法的入参、返回值、异常,粒度最细 |
| 典型场景 | 编码设置、跨域 CORS、请求日志打点(越早越好的通用逻辑) | 登录校验、权限校验、接口耗时统计(需要知道具体是哪个 Controller 方法) | 事务管理、方法级日志、自定义限流注解、幂等控制 |
| 是否能作用于非 Controller 的普通 Service | 否 | 否 | 是,AOP 可以切任意 Spring 管理的 Bean 方法 |
选型口诀:要在"进入 Spring MVC 之前"拦截,用 Filter;要拿到具体 Controller/方法信息做统一处理,用 Interceptor;要给任意 Service 方法织入通用逻辑(不限于 Web 层),用 AOP。
7. 数据访问层:JPA / MyBatis / MyBatis-Plus 全面对比
7.1 硬核对比表:该用哪个 ORM/持久层框架
| 维度 | Spring Data JPA | MyBatis | MyBatis-Plus | jOOQ |
|---|---|---|---|---|
| 编程范式 | ORM,面向对象操作,自动生成 SQL | SQL 手写,Java 与 SQL 解耦 | MyBatis 基础上加了单表 CRUD 自动化 | 类型安全的 SQL DSL,编译期检查 |
| SQL 掌控力 | 弱(复杂查询依赖 JPQL/Criteria API,容易失控) | 强,SQL 完全可控 | 简单场景自动生成,复杂场景仍手写 XML/注解 SQL | 极强,SQL 即代码 |
| 上手难度 | 简单查询很快,但深层机制(懒加载、N+1 查询)容易踩坑 | 需要理解 SQL + XML/注解映射规则 | 最低,单表 CRUD 几乎零代码 | 较陡,DSL 语法需要适应 |
| 复杂查询/多表 JOIN | 弱项,容易写出低效的 JPQL 或退化成原生 SQL | 强项,复杂报表类 SQL 得心应手 | 同 MyBatis | 强,且类型安全 |
| 国内使用率 | 中,偏互联网大厂和强调 DDD 的团队 | 高,尤其政企、传统行业项目 | 国内互联网最主流,尤其中小团队 | 低,多见于对 SQL 质量要求极高的团队 |
| 典型踩坑点 | N+1 查询问题(懒加载在循环里触发大量 SQL)、@Transactional 边界内才能访问懒加载属性 | XML 维护成本、动态 SQL 拼接复杂时可读性下降 | 过度依赖自动生成,遇到复杂业务 SQL 仍要退回手写 | 生态相对小众,团队学习成本 |
给你的建议:教学场景强烈建议先讲 MyBatis(SQL 完全可见,学生能直观看到"我写的 SQL 长什么样、返回什么",对理解数据库原理更友好),进阶再介绍 MyBatis-Plus(省去大量单表 CRUD 样板代码,BaseMapper 自带增删改查),JPA 放在"了解即可"的位置,除非项目团队本身是 JPA 重度用户。
7.2 MyBatis-Plus 核心用法速览
@TableName("t_book")
public class Book {
@TableId(type = IdType.AUTO)
private Long id;
private String title;
private BigDecimal price;
}
public interface BookMapper extends BaseMapper<Book> {
// 继承 BaseMapper 后自动拥有 insert/deleteById/updateById/selectById/selectList 等方法
// 复杂查询仍可以自己写方法 + XML 或 @Select 注解
}
// Service 层调用示例
List<Book> cheapBooks = bookMapper.selectList(
new LambdaQueryWrapper<Book>()
.lt(Book::getPrice, new BigDecimal("50"))
.orderByDesc(Book::getId)
);LambdaQueryWrapper 用方法引用代替字符串字段名,重构字段名时编译器会帮你发现所有引用点,比 MyBatis 原生的字符串拼接 SQL 更安全。
7.3 事务原理:@Transactional 失效场景大汇总(面试高频)
@Transactional 本质是 AOP 代理 + 数据库事务的结合,凡是代理机制失效的场景,事务注解就会失效:
| 失效场景 | 原因 |
|---|---|
同类内部方法调用(this.otherMethod()) | 没走代理对象,见 4.1 节的详细解释 |
方法不是 public | Spring AOP 默认只能代理 public 方法 |
异常被 catch 吞掉,没有再抛出 | 事务回滚依赖捕获到异常,你自己 catch 了就没机会回滚 |
抛出的是受检异常(Checked Exception)且未配置 rollbackFor | @Transactional 默认只对 RuntimeException 和 Error 回滚,业务受检异常需要显式声明 rollbackFor = Exception.class |
| 数据库引擎不支持事务 | 如 MySQL 的 MyISAM 引擎,必须用 InnoDB |
| 多线程中调用 | 事务基于 ThreadLocal 绑定数据库连接,跨线程后连接不是同一个 |
类没有被 Spring 管理(没加 @Service 等注解、手动 new 出来的对象) | 根本不在容器里,谈不上被代理 |
7.4 多数据源与读写分离思路
简单方案:@Primary + 自定义 AbstractRoutingDataSource,通过 ThreadLocal 存储"当前该走哪个数据源"的标记,结合自定义注解 @DataSource("slave") + AOP 切面在方法执行前设置标记;复杂场景(大规模分库分表)建议直接上 ShardingSphere 这类专业中间件,而不是自己造轮子。
7.5 Redis 集成:三种方式对比
| 方式 | 定位 | 适用场景 |
|---|---|---|
RedisTemplate | 最底层、最灵活,直接操作各种数据结构 | 需要精细控制 Redis 命令的场景(如自定义分布式锁、排行榜 ZSet 操作) |
@Cacheable / Spring Cache 抽象 | 声明式缓存,一个注解搞定"查缓存-没有则查库-写回缓存" | 简单的方法结果缓存场景,追求代码简洁 |
| Redisson | 高级客户端,内置分布式锁、限流器、延迟队列、布隆过滤器等开箱即用的分布式组件 | 需要可靠分布式锁(RedLock 算法实现)、延迟队列等高级特性时的首选 |
一个常见误区:很多人用 SETNX 手写分布式锁,容易漏掉锁续期(业务执行时间超过锁过期时间导致锁提前释放)等边界情况,生产场景直接用 Redisson 的 RLock(自带 Watchdog 自动续期机制)远比手写靠谱。
8. 安全:Spring Security / Sa-Token / JWT / OAuth2
8.1 Spring Security 核心架构:过滤器链
Spring Security 的核心是一条 FilterChain,请求进来后依次经过认证过滤器、授权过滤器等一系列 Filter,最终决定放行还是拒绝。理解这条链,比背诵配置代码更重要——因为绝大多数自定义需求(比如接入自己的 Token 认证方式)本质上就是"往这条链里插入一个自己的 Filter"。
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // 前后端分离场景通常禁用 CSRF(配合 Token 认证)
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/auth/**").permitAll()
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class)
.build();
return http.build();
}
}8.2 Spring Security vs Sa-Token:国内实用向对比
Sa-Token 是国产开源的轻量级权限认证框架,在国内中小项目和培训场景使用率非常高,值得单独拉出来对比:
| 维度 | Spring Security | Sa-Token |
|---|---|---|
| 学习曲线 | 陡峭,过滤器链、AuthenticationManager、UserDetailsService 等概念多,新手容易迷失 | 平缓,API 设计追求"能一行搞定就不写两行",StpUtil.login(id) 一行完成登录 |
| 功能完备度 | 认证 + 授权体系完整,深度集成 OAuth2/OIDC,企业合规场景经过大量验证 | 登录认证、权限校验、Session 会话、踢人下线、二级验证等功能齐全,同样覆盖大部分业务需求 |
| 生态成熟度 | Spring 官方出品,文档、社区、企业采用度都是天花板级别 | 国产社区活跃,中文文档极其友好,但海外生态和大厂长期验证不如 Security |
| 定制成本 | 高(要理解一整套认证授权模型才能定制) | 低,API 直观,改造成本小 |
| 典型选择场景 | 大型企业级项目、需要对接标准 OAuth2/SSO、合规要求高的场景 | 中小项目、培训教学、追求开发效率的团队 |
给教学场景的建议:如果目标是让学生快速理解"认证授权是什么、怎么落地一个可用的登录鉴权系统",Sa-Token 上手成本低、能更快聚焦在业务逻辑本身;如果目标是为学生进入大厂/正规企业项目做准备,Spring Security 的过滤器链思维和 OAuth2 集成经验是绕不开的必修课。两者可以分阶段安排:先用 Sa-Token 让学生体会到"权限系统解决了什么问题",再用 Spring Security 讲清楚"企业级系统底层是怎么实现的"。
8.3 JWT 无状态认证实战
// 登录成功后签发 Token
public String generateToken(Long userId, String role) {
return Jwts.builder()
.subject(String.valueOf(userId))
.claim("role", role)
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + 2 * 60 * 60 * 1000)) // 2小时过期
.signWith(secretKey, Jwts.SIG.HS256)
.compact();
}
// 自定义 Filter 校验 Token
public class JwtAuthFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) {
String token = extractToken(req);
if (token != null && jwtService.validate(token)) {
Claims claims = jwtService.parseClaims(token);
var auth = new UsernamePasswordAuthenticationToken(
claims.getSubject(), null,
List.of(new SimpleGrantedAuthority("ROLE_" + claims.get("role")))
);
SecurityContextHolder.getContext().setAuthentication(auth);
}
chain.doFilter(req, res);
}
}JWT 的核心权衡:无状态、天然适合分布式/微服务场景(不需要 Session 共享),代价是Token 一旦签发难以主动失效(除非配合 Redis 维护黑名单,但这样又引入了状态,本质上是在"完全无状态"和"可控可撤销"之间做取舍)。
8.4 OAuth2 / OIDC 简介
OAuth2 解决的是"第三方应用如何在用户授权下访问受保护资源"(微信登录、GitHub 登录都是 OAuth2 落地),OIDC(OpenID Connect)是在 OAuth2 基础上加了一层标准化的身份认证语义。Spring 生态里 spring-boot-starter-oauth2-client 用来做"接入第三方登录",spring-boot-starter-oauth2-resource-server 用来做"自己的服务作为资源服务器校验 Token"——这一块内容体系庞大,教学中建议作为选修的进阶专题单独展开。
9. 微服务与云原生扩展(前沿)
9.1 单体到微服务:为什么、什么时候拆
不要为了"微服务"而微服务——单体架构在团队规模小、业务边界不清晰的早期阶段往往是更优解(部署简单、调试直观、没有分布式事务的坑)。拆分的信号通常是:团队规模大到"一个发布窗口卡住所有人"、某个模块需要独立弹性伸缩(比如秒杀模块)、技术栈需要按模块差异化(AI 推理服务用 Python,核心交易用 Java)。
9.2 Spring Cloud 生态全景
| 能力域 | 主流组件(Spring Cloud 原生) | 主流组件(Alibaba 系) |
|---|---|---|
| 服务注册与发现 | Eureka(已停止大版本更新,不建议新项目使用) | Nacos(同时具备配置中心能力,国内事实标准) |
| 配置中心 | Spring Cloud Config | Nacos Config、Apollo |
| 服务间调用 | OpenFeign(声明式 HTTP 客户端) | 同样常用 OpenFeign,或直接用 Dubbo RPC |
| 网关 | Spring Cloud Gateway(基于 WebFlux/Netty,异步非阻塞) | 同样广泛使用 Spring Cloud Gateway |
| 熔断限流 | Resilience4j(替代已停止维护的 Hystrix) | Sentinel(阿里开源,国内使用率极高,带控制台可视化配置限流规则) |
| 分布式事务 | 无原生方案 | Seata(阿里开源,AT/TCC/Saga 多种模式) |
| 链路追踪 | Micrometer Tracing(替代 Spring Cloud Sleuth) | 同上,通常对接 SkyWalking/Zipkin |
国内实践建议:绝大多数国内互联网公司的技术栈是 Spring Cloud Alibaba(Nacos + Sentinel + Seata)而不是 Netflix 系组件,原因是 Netflix OSS 系列(Eureka、Hystrix、Zuul)大多已经停止维护,Alibaba 系组件持续迭代、中文文档和社区支持也更好。注意版本兼容坑:Spring Cloud Alibaba、Spring Cloud、Spring Boot 三者的版本号必须严格对齐(例如某个 Spring Boot 3.x 版本只能配特定的 Spring Cloud 版本,再对应特定的 Spring Cloud Alibaba 版本),务必在选型时查阅 Spring Cloud Alibaba 官方 Wiki 的版本对照表,版本号是这个生态里"血泪坑"最多的地方,没有之一。
9.3 Dubbo vs Spring Cloud OpenFeign
| 维度 | Dubbo | Spring Cloud OpenFeign |
|---|---|---|
| 通信协议 | 自定义二进制 RPC 协议,性能更高 | 基于 HTTP + JSON,通用性更好,调试更直观(能直接用 curl/Postman 测) |
| 生态定位 | 更偏"RPC 框架",国内阿里系企业级项目常见 | Spring Cloud 生态原生组件,与 Spring Boot 整合度最高 |
| 跨语言调用 | 较弱(原生面向 Java 生态) | 强(HTTP + JSON 天然跨语言,Go/Python/Node 服务都能直接调) |
| 何时选它 | 内部 Java 服务间高频调用、追求极致性能 | 需要跨语言互调、或者希望调试链路更直观 |
9.4 Spring Cloud Gateway 实战速览
@Bean
public RouteLocator customRoutes(RouteLocatorBuilder builder) {
return builder.routes()
.route("book-service", r -> r
.path("/api/v1/books/**")
.filters(f -> f
.stripPrefix(1)
.addRequestHeader("X-Gateway", "true")
.circuitBreaker(c -> c.setName("bookServiceCB").setFallbackUri("forward:/fallback"))
)
.uri("lb://book-service")) // lb:// 表示走负载均衡,配合服务发现动态解析实例
.build();
}Gateway 基于 WebFlux/Netty 实现,天然非阻塞,这也是"网关类场景更适合响应式编程"的典型例证——大量请求只是简单转发和聚合,I/O 密集、CPU 计算少,正是 WebFlux 的甜蜜点(回顾 6.2 节的对比表)。
10. Spring Boot 4.x 前沿特性硬核解读
这一章专门讲 2025-2026 这一波 Spring 生态的代际更新,是本教程"前沿"二字的核心体现。
10.1 Spring Boot 4.0 / 4.1:代际跃迁在改什么
Spring Boot 4.0 于 2025 年 11 月发布,4.1 于 2026 年 6 月跟进,底层升级到 Spring Framework 7 + Jakarta EE 11,关键变化:
| 变化点 | 说明 |
|---|---|
| 代码库模块化 | Spring Boot 4 对代码库做了彻底模块化拆分,产出更小、更聚焦的 jar,减少不必要依赖被动引入 |
| 容器基线抬高 | 要求 Servlet 6.1 兼容容器;内嵌 Tomcat/Jetty 随之升级,Undertow 因尚未适配 Servlet 6.1 被移除支持 |
| Spring Data AOT | 查询生成从运行时提前到编译期完成,官方数据显示可带来 50%-70% 的启动速度提升 |
| HTTP Interface Client 增强 + REST Test Client | 声明式 HTTP 客户端和测试客户端进一步减少样板代码 |
| Spring Framework 7 弹性注解 | 新增 @Retryable(自动重试,可配置次数/延迟/抖动/退避策略,支持响应式类型)、@ConcurrencyLimit(限制方法并发调用数,防止过载) |
| 首个原生支持的 API 版本管理 | 不再需要自己手搓 URL 前缀或 Header 判断来做接口版本控制 |
| GraalVM 原生镜像支持更成熟 | 相比 Boot 3 时代的"能用但坑多",Boot 4 打磨了大量原生镜像下的边界问题 |
给存量项目的提醒:从 Spring Boot 3.5(3.x 最后一版,已于 2026 年 6 月底结束社区支持)升级到 4.x,除了常规的 javax.* 早已在 Boot 3 时完成到 jakarta.* 的迁移之外,Boot 4 阶段还有配置属性重命名、部分被弃用 API 移除等变更,升级前务必仔细过一遍官方 Migration Guide,不要指望"改个版本号就能编译通过"。
10.2 虚拟线程(Virtual Threads)硬核对比:Java vs Go 并发模型
Java 21 起虚拟线程转正,Spring Boot 只需一行配置即可全局启用:
spring:
threads:
virtual:
enabled: true| 维度 | Java 虚拟线程(Virtual Threads) | Go Goroutine(你熟悉的对照物) |
|---|---|---|
| 调度模型 | JVM 层面 M:N 调度,挂载到少量"载体线程(Carrier Thread)"上 | Go runtime 自己的 GMP 调度模型,同样是 M:N |
| 编程心智 | 完全兼容传统同步阻塞写法,Thread.sleep()、传统 synchronized、阻塞 I/O 调用都能被调度器识别并"卸载"底层线程 | 需要显式用 go func(){}() + channel,是一套独立的并发原语 |
| 阻塞代价 | 接近于 0(虚拟线程阻塞时会被自动"卸下"载体线程,不占用系统线程资源) | Goroutine 本身也轻量,阻塞系统调用会被 runtime 自动扩容 M(系统线程) |
| 兼容性 | 对存量代码几乎零改造——原来的 Spring MVC 阻塞式代码不用重写,开个开关就能享受高并发吞吐 | Go 从语言设计之初就是这个模型,没有"新旧兼容"问题 |
| 一个已知限制 | synchronized 块内部分场景仍可能发生"Pinning"(虚拟线程被钉死在载体线程上),Java 21 之后的版本在持续改善这一点 | 无此问题 |
这为什么是"前沿"且"实用"的知识点:过去业界共识是"高并发就要上 WebFlux/响应式编程",虚拟线程的出现某种程度上动摇了这个共识——你可以继续写"看起来阻塞、其实不阻塞底层系统线程"的传统代码,却获得接近响应式的吞吐能力,学习成本却远低于 WebFlux。对教学场景来说,这是一个非常好的切入点,可以引导学生思考"新技术出现如何改变旧的架构决策"。
10.3 GraalVM 原生镜像:启动速度与内存的量级差异
# 传统方式:JVM 启动,需要类加载、JIT 预热
java -jar app.jar # 启动通常需要 1-3 秒,内存占用 200MB+
# GraalVM Native Image:AOT 编译成机器码,无需 JVM
./app # 启动通常在 50-100 毫秒级,内存占用可降到几十 MB| 维度 | 传统 JVM 部署 | GraalVM Native Image |
|---|---|---|
| 启动速度 | 秒级 | 毫秒级(数量级差异) |
| 内存占用 | 较高,JVM 本身有开销 | 大幅降低 |
| 峰值吞吐 | JIT 预热后性能更优(长期运行的服务,JIT 能做深度优化) | AOT 编译缺少运行时 profile 信息,峰值吞吐通常略逊于预热完成的 JVM |
| 构建时间 | 快 | 慢得多(原生镜像编译是重量级过程) |
| 反射/动态代理支持 | 完全支持 | 需要显式配置反射元数据(Spring Boot 已大幅自动化这部分工作) |
| 适用场景 | 长期运行的传统服务、Web 应用 | Serverless/FaaS、需要极致冷启动速度的场景、CLI 工具、Sidecar |
结合 Boot 4 对原生镜像支持成熟度的提升,原生镜像正在从"实验性尝鲜"走向"部分场景的生产可用选项",尤其是 Serverless 函数计算这类对冷启动极度敏感的场景。
10.4 Spring AI:给 Java 后端接上大模型能力
考虑到你也在做 AIGC 相关的课程内容,这里单独提一下 Spring AI——它是 Spring 官方对标 LangChain 的 AI 应用开发框架,目标是让 Java 开发者用熟悉的 Spring 编程模型接入大模型能力:
| 能力 | 说明 |
|---|---|
| 统一模型接入 | 一套 API 对接 OpenAI、Anthropic、Google、Ollama(本地模型)等主流厂商,切换供应商基本不用改业务代码 |
| RAG 支持 | 内置文档加载、切分、向量化、检索增强生成的标准化流程 |
| 向量数据库集成 | PGVector、Milvus、Redis、Chroma、Qdrant 等主流向量库都有 Starter |
| 工具调用(Function Calling) | 让大模型可以调用你定义的 Java 方法/Spring Bean 完成实时查询等操作 |
| MCP(Model Context Protocol)支持 | Spring AI 2.0 已经把 MCP 支持并入核心模块,可以让 Spring 应用同时作为 MCP Server 或 Client 接入更广泛的智能体生态 |
版本现状(写作时点):Spring AI 1.x 系列已经是生产可用的稳定版本(1.0/1.1 持续发布补丁),基于 Spring Boot 4 的 2.0 系列仍处于里程碑(Milestone)阶段,教学和实验场景可以尝鲜,生产项目建议先锁定 1.x 稳定版。对比 Python 生态的 LangChain:Spring AI 的优势是能直接复用现有 Spring Boot 项目的基础设施(安全、可观测性、配置管理),劣势是模型/工具生态的更新速度天然慢于 Python 系(大模型领域绝大多数新工具是 Python 优先发布 SDK)。
11. 可观测性与生产级实践
11.1 三大支柱:日志、指标、链路追踪
生产系统的可观测性通常拆成三个维度,缺一不可:
| 维度 | 解决"什么问题" | Spring 生态方案 |
|---|---|---|
| 日志(Logging) | 发生了什么,具体细节是什么 | Logback + SLF4J,建议输出结构化 JSON 日志便于采集 |
| 指标(Metrics) | 系统整体健康状况、趋势 | Micrometer + Prometheus 采集 + Grafana 可视化 |
| 链路追踪(Tracing) | 一个请求跨多少个服务、每一跳耗时多少 | Micrometer Tracing(已取代废弃的 Spring Cloud Sleuth)+ Zipkin/SkyWalking |
11.2 Actuator:开箱即用的生产监控端点
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus # 按需暴露,不要无脑 include: "*"(安全隐患)
endpoint:
health:
show-details: when-authorized// 自定义健康检查:比如检查某个下游依赖是否可用
@Component
public class DownstreamHealthIndicator implements HealthIndicator {
@Override
public Health health() {
boolean reachable = checkDownstreamService();
return reachable
? Health.up().withDetail("downstream", "reachable").build()
: Health.down().withDetail("downstream", "unreachable").build();
}
}/actuator/health 是 K8s 探针(liveness/readiness probe)最常见的对接端点,/actuator/prometheus 直接暴露 Prometheus 格式的指标数据,这条链路是"Spring Boot 天生适配云原生"最直观的体现之一。
11.3 分布式链路追踪落地
management:
tracing:
sampling:
probability: 1.0 # 生产环境通常调低采样率(比如 0.1),避免追踪数据本身拖垮系统
zipkin:
tracing:
endpoint: http://localhost:9411/api/v2/spans引入 micrometer-tracing-bridge-brave 和 zipkin-reporter-brave 依赖后,Spring Boot 会自动给每个请求生成 TraceId/SpanId 并透传到下游调用(通过 HTTP Header 自动传播),配合 Zipkin 或国内更常用的 SkyWalking(支持无侵入式字节码增强探针,几乎不用改代码)即可看到完整调用链路和每一跳耗时,这是排查"接口偶尔慢"这类疑难问题的核心武器。
12. 测试:JUnit5 / Mockito / Testcontainers
12.1 单元测试:JUnit5 + Mockito
@ExtendWith(MockitoExtension.class)
class BookServiceTest {
@Mock
private BookMapper bookMapper;
@InjectMocks
private BookServiceImpl bookService;
@Test
void shouldThrowWhenBookNotFound() {
when(bookMapper.selectById(1L)).thenReturn(null);
assertThatThrownBy(() -> bookService.getById(1L))
.isInstanceOf(BusinessException.class)
.hasMessageContaining("不存在");
}
}单元测试的原则是"隔离外部依赖"——用 @Mock 模拟掉数据库、外部服务这些不确定的边界,只测试自己业务逻辑的正确性,运行速度要求是毫秒级。
12.2 集成测试:@SpringBootTest
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
class BookControllerIT {
@Autowired
private MockMvc mockMvc;
@Test
void shouldCreateBookSuccessfully() throws Exception {
mockMvc.perform(post("/api/v1/books")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"title":"三体","author":"刘慈欣","price":59.9}
"""))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.title").value("三体"));
}
}12.3 Testcontainers:用真实数据库/中间件做测试(硬核,很多人不知道这个工具)
很多团队的集成测试要么用 H2 内存数据库"糊弄过去"(H2 和生产用的 MySQL/PostgreSQL 语法、行为有细微差异,测试通过≠生产没问题),要么依赖一个共享的测试数据库(脏数据、并发测试互相干扰)。Testcontainers 的思路是:测试启动时用 Docker 拉起一个真实的、干净的、临时的数据库/Redis/Kafka 容器实例,测试结束自动销毁。
@Testcontainers
@SpringBootTest
class BookRepositoryIT {
@Container
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0")
.withDatabaseName("test_db");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", mysql::getJdbcUrl);
registry.add("spring.datasource.username", mysql::getUsername);
registry.add("spring.datasource.password", mysql::getPassword);
}
@Test
void shouldPersistBookCorrectly() {
// 这里跑的是对真实 MySQL 8.0 实例的测试,而不是行为有差异的 H2 模拟
}
}代价是测试速度变慢(需要拉起容器),收益是测试环境与生产环境高度一致,能提前暴露"H2 能跑但生产 MySQL 跑不了"的坑(比如某些 MySQL 特有的 SQL 语法、字符集问题)。这是目前 Java 社区公认的集成测试最佳实践之一,教学中值得作为"进阶"内容专门演示一次。
13. 性能调优实战
13.1 HikariCP 连接池参数硬核解读
Spring Boot 默认内置的连接池是 HikariCP(公认 JVM 生态里性能最好的连接池实现),核心参数:
spring:
datasource:
hikari:
maximum-pool-size: 20 # 最大连接数:不是越大越好!参考公式:((核心数 * 2) + 有效磁盘数),盲目调大反而因为数据库端连接切换开销和锁竞争降低吞吐
minimum-idle: 5 # 最小空闲连接数
connection-timeout: 30000 # 获取连接超时时间(毫秒),超时快速失败比无限等待更利于系统整体稳定性
idle-timeout: 600000 # 空闲连接超过这个时间会被回收
max-lifetime: 1800000 # 连接最大存活时间,避免用到数据库端已经失效的"僵尸连接"一个反直觉但重要的认知:连接池不是越大越好。数据库本身处理并发连接是有代价的(上下文切换、锁竞争),HikariCP 官方文档明确建议连接数不要盲目调大,先从较小的数值(比如 10-20)开始压测,观察数据库端 CPU/锁等待情况再调整,而不是拍脑袋设一个"看起来安全"的大数字。
13.2 缓存穿透 / 击穿 / 雪崩
| 问题 | 场景 | 解法 |
|---|---|---|
| 缓存穿透 | 查询一个根本不存在的数据,缓存和数据库都没有,每次请求都打到数据库 | 缓存空值(设置较短过期时间)、布隆过滤器提前拦截明显不存在的 key |
| 缓存击穿 | 某个热点 key 恰好过期的瞬间,大量并发请求同时打到数据库 | 热点数据设置永不过期(后台异步刷新)、或获取锁后只让一个请求去重建缓存,其余等待 |
| 缓存雪崩 | 大量 key 同时过期,或者 Redis 实例整体宕机,请求集中打到数据库 | 过期时间加随机抖动避免同时失效、Redis 做高可用集群部署、结合熔断限流兜底 |
13.3 异步化选型:@Async vs CompletableFuture vs 虚拟线程
| 方案 | 适用场景 |
|---|---|
@Async | 简单的"发个请求不用等结果"场景(如异步发邮件、异步记日志),依赖固定大小线程池,池满会阻塞或拒绝 |
CompletableFuture | 需要编排多个异步任务(并行调用多个下游服务后聚合结果)时更灵活,支持 thenCombine、allOf 等组合操作符 |
| 虚拟线程 | 大量轻量级并发任务(比如批量调用 1000 个下游接口),虚拟线程配合 Executors.newVirtualThreadPerTaskExecutor() 可以直接"每个任务一个线程"而不用担心线程数爆炸——这是虚拟线程对传统异步编程范式最大的冲击:很多场景下不再需要精心设计线程池大小,直接"来一个任务开一个虚拟线程"即可 |
14. 部署与 DevOps:Docker / K8s
14.1 打包:jar vs war
Spring Boot 默认打成可执行 jar(内嵌容器,java -jar app.jar 直接跑),只有需要部署到已有的外置应用服务器(传统企业客户要求用自己的 Tomcat 集群)时才需要打 war 包,新项目默认就用 jar,这也是云原生部署的标准形态。
14.2 Docker 分层构建
# 多阶段构建:第一阶段编译,第二阶段只拷贝产物,减小最终镜像体积
FROM eclipse-temurin:21-jdk AS build
WORKDIR /app
COPY . .
RUN ./mvnw clean package -DskipTests
FROM eclipse-temurin:21-jre AS runtime
WORKDIR /app
# 利用 Spring Boot 的分层 jar 特性,把变化频率低的依赖层和变化频率高的业务代码层分开
COPY --from=build /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]进阶技巧:Spring Boot 支持 spring-boot:build-image(基于 Cloud Native Buildpacks,不用手写 Dockerfile 也能产出优化过的镜像),以及分层 jar(Layered Jar)——把依赖库(很少变化)和业务代码(频繁变化)分成不同的 Docker 层,这样每次只有业务代码层需要重新构建和推送,大幅提升 CI/CD 效率,对经常迭代的项目收益明显。
14.3 Kubernetes 部署示例
apiVersion: apps/v1
kind: Deployment
metadata:
name: book-service
spec:
replicas: 3
selector:
matchLabels: { app: book-service }
template:
metadata:
labels: { app: book-service }
spec:
containers:
- name: book-service
image: registry.example.com/book-service:1.2.0
ports: [{ containerPort: 8080 }]
env:
- name: SPRING_PROFILES_ACTIVE
value: "prod"
livenessProbe:
httpGet: { path: /actuator/health/liveness, port: 8080 }
initialDelaySeconds: 20
readinessProbe:
httpGet: { path: /actuator/health/readiness, port: 8080 }
initialDelaySeconds: 10
resources:
requests: { cpu: "250m", memory: "512Mi" }
limits: { cpu: "1", memory: "1Gi" }
---
apiVersion: v1
kind: Service
metadata:
name: book-service
spec:
selector: { app: book-service }
ports: [{ port: 80, targetPort: 8080 }]注意这里的 liveness/readiness 探针直接对接第 11 章讲的 Actuator 健康检查端点——Spring Boot 的 health 端点默认已经支持区分"存活探针"(进程是否卡死,卡死了就重启容器)和"就绪探针"(是否准备好接收流量,比如启动阶段数据库连接还没建立完成时不应该接流量),这是"Spring Boot 天生契合云原生"的又一个具体例证。
15. 横向对比:Spring Boot vs Go / Node.js / Python 生态
结合你已经熟悉 Go(Gin/Go-Zero)技术栈的背景,这张表专门做跨语言横向对比,帮助建立"什么场景该用什么语言/框架"的整体判断力,这也是给学生做技术选型教学时很有说服力的素材:
| 维度 | Spring Boot (Java) | Gin / Go-Zero (Go) | Express / NestJS (Node.js) | Django / FastAPI (Python) |
|---|---|---|---|---|
| 启动速度 | 传统 JVM 秒级;GraalVM 原生镜像后毫秒级 | 毫秒级,编译型语言天然优势 | 秒级以内,较快 | 较快,但 GIL 限制下的真并发能力弱 |
| 内存占用 | 传统 JVM 较高(200MB+),原生镜像后大幅降低 | 极低,编译产物是单一静态二进制 | 中等 | 较高(尤其是带一堆依赖库时) |
| 并发模型 | 传统线程池 / 虚拟线程(M:N 调度) / WebFlux 响应式 | Goroutine(M:N 调度,语言原生支持,心智负担最低) | 事件循环(单线程非阻塞,CPU 密集型任务需要 Worker 进程) | 依赖 asyncio(FastAPI)或多进程(Django + Gunicorn),GIL 是硬限制 |
| 类型系统 | 强类型,编译期检查严格 | 强类型,编译期检查严格 | TypeScript 下强类型,JS 下弱类型 | 动态类型(类型注解仅辅助,非强制) |
| 生态成熟度 | 极其成熟,企业级功能(安全、事务、监控)开箱即用,几乎"你能想到的需求都有现成方案" | 生态相对年轻,很多"企业级"能力需要自己拼装(不像 Spring 全家桶那样成体系) | 生态庞大但质量参差,选型需要更多甄别 | AI/数据科学领域生态无可替代,但传统企业级 Web 后端生态弱于 Java/Go |
| 学习曲线 | 入门容易,精通难(框架能力太庞大,本教程讲的还只是核心子集) | 入门到中等熟练都比较平缓,语言本身简洁 | 入门容易,但大型项目治理难度不低 | 入门最容易,但深挖性能优化路径较窄 |
| 团队协作/大型项目治理 | 强,Spring 的分层架构、依赖注入天然适合大团队协作和长期维护 | 中等,缺少统一"重型框架"的约束,团队规范全靠自律 | 中等偏强(NestJS 借鉴了 Spring 的 DI/装饰器思想,Express 则较松散) | 中等,大型 Python 项目的长期可维护性常被诟病 |
| 最佳适用场景 | 复杂业务系统、金融/政企核心系统、需要事务保证和复杂权限体系的场景 | 高并发中间件、网关、云原生基础设施、对资源占用和启动速度极度敏感的服务 | 全栈 JS 团队、BFF 层、实时应用(WebSocket) | 数据科学、AI 模型服务、快速原型验证 |
给你(同时懂 Go 和要教 Java)的一个实用类比:Spring 的 IoC 容器 + AOP,本质上是用"重框架"的方式统一解决了 Go 生态里"每个团队各自实现一套依赖注入/中间件方案"的碎片化问题——代价是学习曲线更陡、框架本身更"重",收益是大型团队协作时的规范性和可维护性显著更好。这也是为什么金融、政企这类对"代码几十年后还要有人能看懂、能维护"要求极高的领域,Java/Spring 生态依然是绝对主流的原因。
16. 完整实战项目:图书管理系统
一个覆盖前面所有知识点的最小可用(MVP)实战项目骨架,包含:CRUD + JWT 登录鉴权 + Redis 缓存 + 全局异常处理 + Docker 部署。
16.1 项目结构
book-management/
├── src/main/java/com/example/book/
│ ├── BookManagementApplication.java
│ ├── config/
│ │ ├── SecurityConfig.java # Spring Security + JWT 配置
│ │ └── RedisConfig.java # Redis 序列化配置
│ ├── controller/
│ │ ├── AuthController.java # 登录/注册
│ │ └── BookController.java # 图书 CRUD
│ ├── service/
│ │ ├── AuthService.java
│ │ └── BookService.java
│ ├── entity/Book.java
│ ├── mapper/BookMapper.java # MyBatis-Plus BaseMapper
│ ├── dto/ # 请求/响应 DTO
│ ├── security/JwtAuthFilter.java
│ ├── exception/
│ │ ├── BusinessException.java
│ │ └── GlobalExceptionHandler.java
│ └── util/JwtUtils.java
├── src/main/resources/
│ ├── application.yml
│ └── application-prod.yml
├── src/test/java/... # 单元测试 + Testcontainers 集成测试
├── Dockerfile
└── pom.xml16.2 核心业务代码:带缓存的图书查询
@Service
@RequiredArgsConstructor
public class BookService {
private final BookMapper bookMapper;
private final RedisTemplate<String, Object> redisTemplate;
private static final String CACHE_KEY_PREFIX = "book:";
private static final Duration CACHE_TTL = Duration.ofMinutes(30);
public BookDTO getById(Long id) {
String cacheKey = CACHE_KEY_PREFIX + id;
// 1. 查缓存
BookDTO cached = (BookDTO) redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
return cached;
}
// 2. 查数据库
Book book = bookMapper.selectById(id);
if (book == null) {
// 防止缓存穿透:对不存在的数据也缓存一个空标记,短过期时间
redisTemplate.opsForValue().set(cacheKey, NULL_PLACEHOLDER, Duration.ofMinutes(2));
throw new BusinessException("BOOK_NOT_FOUND", "图书不存在", HttpStatus.NOT_FOUND);
}
BookDTO dto = BookDTO.from(book);
// 3. 写回缓存,过期时间加随机抖动防止雪崩
long jitter = ThreadLocalRandom.current().nextLong(60);
redisTemplate.opsForValue().set(cacheKey, dto, CACHE_TTL.plusSeconds(jitter));
return dto;
}
@Transactional(rollbackFor = Exception.class)
public void updatePrice(Long id, BigDecimal newPrice) {
int updated = bookMapper.update(null,
new LambdaUpdateWrapper<Book>()
.eq(Book::getId, id)
.set(Book::getPrice, newPrice));
if (updated == 0) {
throw new BusinessException("BOOK_NOT_FOUND", "图书不存在", HttpStatus.NOT_FOUND);
}
// 更新后主动删除缓存,保证下次读取到最新数据(Cache-Aside 模式的标准写法)
redisTemplate.delete(CACHE_KEY_PREFIX + id);
}
}这段代码本身就是第 7、13 章讲的"缓存穿透防护"、"过期抖动防雪崩"、"事务回滚配置"、"更新后失效缓存而非直接更新缓存(Cache-Aside 模式)"这几个知识点的综合演练,建议作为课堂上的重点讲解代码。
16.3 Dockerfile(复用第 14 章的分层构建思路)
FROM eclipse-temurin:21-jdk AS build
WORKDIR /app
COPY . .
RUN ./mvnw clean package -DskipTests
FROM eclipse-temurin:21-jre AS runtime
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar", "--spring.profiles.active=prod"]17. 学习路线图与资源
17.1 建议学习顺序(配合本教程章节)
第一阶段:能跑起来(第1-2章)
→ Spring Boot 是什么,环境搭建,第一个 Controller
第二阶段:看懂原理(第3-5章)
→ 自动配置、IoC/DI、Bean 生命周期、配置管理
→ 这一阶段是把"会用"升级成"懂原理"的分水岭,务必啃透
第三阶段:能做真实业务(第6-8章)
→ Web 层、数据访问层、安全认证
→ 完成一个完整的 CRUD + 登录鉴权项目(对应第16章)
第四阶段:能做技术选型、看懂架构(第9-10章)
→ 微服务、云原生、虚拟线程、GraalVM、Spring AI
→ 这一阶段开始接触"为什么这么设计"、"什么场景选什么方案"
第五阶段:能上生产(第11-14章)
→ 可观测性、测试、性能调优、部署
→ 补齐"从能跑到能稳定跑在生产环境"之间的差距
第六阶段:能拿去面试、指导他人(第15、18章 + 实战项目)
→ 横向对比建立宏观判断力,面试题查漏补缺17.2 官方权威资源
- Spring 官方文档:
https://docs.spring.io/spring-boot/(永远以官方文档为准,第三方教程会过时) - Spring 官方 Guides(大量可运行的最小示例):
https://spring.io/guides - Spring Boot GitHub Releases(追踪版本变更):
https://github.com/spring-projects/spring-boot/releases - Baeldung(Java/Spring 领域公认最系统的英文教程站):
https://www.baeldung.com
18. 硬核面试题精选
以下问题都是本教程正文里已经详细拆解过原理的高频面试题,建议学完对应章节后回来自测:
@SpringBootApplication由哪几个注解组成,各自的作用是什么?(回看 3.1)- Spring Boot 的自动配置是怎么被发现和加载的?Boot 2 和 Boot 3/4 的实现方式有什么区别?(回看 3.2)
- 说说 Bean 的完整生命周期,AOP 代理是在哪个环节生成的?(回看 4.1)
- Spring 是怎么解决循环依赖的?为什么需要三级缓存而不是两级?构造器注入的循环依赖为什么无法被解决?(回看 4.2)
@Transactional在哪些场景下会失效?为什么?(回看 7.3)- 说说 Spring MVC 处理一个请求的完整流程。(回看 6.1)
- Spring MVC 和 WebFlux 该怎么选?虚拟线程出现后,这个选型逻辑发生了什么变化?(回看 6.2、10.2)
- Filter、Interceptor、AOP 三者的区别和各自的适用场景?(回看 6.5)
- 缓存穿透、击穿、雪崩分别是什么问题,怎么解决?(回看 13.2)
- 外部化配置的优先级顺序是怎样的?(回看 5.3)
- 虚拟线程和传统平台线程、和 Go 的 Goroutine 有什么本质区别?(回看 10.2)
- GraalVM 原生镜像相比传统 JVM 部署,优劣分别是什么?(回看 10.3)
结语:这份教程覆盖的内容跨度很大,不建议一次性通读消化,建议按第 17 章的学习路线分阶段推进,每个阶段配合动手写代码(尤其是第 16 章的实战项目),原理性的内容(第 3、4、7.3、10.2 节)值得反复回顾,这些正是把"会用 Spring Boot"和"懂 Spring Boot"区分开的关键分水岭,也是技术面试里最容易拉开差距的地方。