Skip to content

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 的差异,方便你对接现有项目和面试考察点。


目录 ​

  1. 写在前面:这份教程要解决什么问题
  2. Spring Boot 是什么,为什么会有它
  3. 环境搭建与第一个项目
  4. 自动配置原理硬核剖析
  5. IoC / DI 深入:Bean 生命周期与循环依赖
  6. 配置管理:从 yaml 到配置中心
  7. Web 开发:MVC、WebFlux 与 REST 实战
  8. 数据访问层:JPA / MyBatis / MyBatis-Plus 全面对比
  9. 安全:Spring Security / Sa-Token / JWT / OAuth2
  10. 微服务与云原生扩展(前沿)
  11. Spring Boot 4.x 前沿特性硬核解读
  12. 可观测性与生产级实践
  13. 测试:JUnit5 / Mockito / Testcontainers
  14. 性能调优实战
  15. 部署与 DevOps:Docker / K8s
  16. 横向对比:Spring Boot vs Go / Node.js / Python 生态
  17. 完整实战项目:图书管理系统
  18. 学习路线图与资源
  19. 硬核面试题精选

0. 写在前面:这份教程要解决什么问题

市面上 Spring Boot 教程大多止步于"CRUD + 截图 + 跑起来就完事",这份教程刻意反着来,遵循三个原则:

  1. 每个特性都讲"为什么",不只讲"怎么做"。比如不只是告诉你加 @Transactional,而是讲清楚它底层靠 AOP 代理实现,为什么在同类内部方法调用时会失效。
  2. 凡是有可替代方案的地方,都给对比表。Spring 生态选择太多(JPA vs MyBatis,Spring Cloud vs Dubbo,Spring Security vs Sa-Token),教程的价值在于告诉你什么场景选什么,而不是只讲一种。
  3. 紧跟当前技术代际。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 BootJakarta EE (原 JavaEE)
配置方式大量 XMLXML + 部分注解注解 + 极少配置,约定优于配置标准化注解 + 部署描述符
依赖管理手动对齐版本手动对齐版本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 LTS2021.9存量主力密封类、Record、模式匹配预览老项目维护,Spring Boot 3.x 最低基线
Java 21 LTS2023.9当前企业主流虚拟线程(正式)、Record Pattern、分代 ZGC新项目首选,生态兼容性最好,各大云厂商镜像齐全
Java 25 LTS2025.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 ​

维度MavenGradle
配置文件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 ​

java
@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 ​

这个注解其实是三个注解的组合:

java
@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 文件里的一行配置来注册:

properties
# spring.factories(Boot 2.x 写法,已过时)
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.MyAutoConfiguration,\
com.example.AnotherAutoConfiguration

Spring 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 要不要被创建:

注解作用
@ConditionalOnClassclasspath 里存在指定类才生效(比如检测到 Redisson 就装配 Redisson 相关 Bean)
@ConditionalOnMissingBean容器里没有该类型 Bean 时才生效——这就是为什么你自己定义一个 DataSource Bean,官方自动配置的默认数据源会自动让位
@ConditionalOnProperty根据配置文件某个属性值决定是否生效
@ConditionalOnWebApplication是否是 Web 应用(Servlet 型还是响应式型还进一步细分)
@ConditionalOnMissingClass反过来,classpath 里没有某类才生效

举个具体例子,DataSourceAutoConfiguration 的核心逻辑简化后大概是:

java
@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 并不难,三步走:

  1. 定义配置属性类(对接 application.yml):
java
@ConfigurationProperties(prefix = "myapp.greeting")
public class GreetingProperties {
    private String prefix = "Hello";
    private String suffix = "!";
    // getter/setter 省略
}
  1. 写自动配置类:
java
@AutoConfiguration
@EnableConfigurationProperties(GreetingProperties.class)
@ConditionalOnClass(GreetingService.class)
public class GreetingAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public GreetingService greetingService(GreetingProperties props) {
        return new GreetingService(props.getPrefix(), props.getSuffix());
    }
}
  1. 注册到自动配置发现文件:在 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(已实例化但未完成属性填充)
三级缓存singletonFactoriesBean 的 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 ​

properties
# application.properties
server.port=8080
spring.datasource.url=jdbc:mysql://localhost:3306/demo
yaml
# application.yml —— 推荐,层级更清晰,尤其是复杂嵌套配置
server:
  port: 8080
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/demo

yaml 优势明显:层级配置更直观、支持列表和 map 更自然,是目前实际项目的主流选择;properties 的优势仅剩"每行独立,写错一行不会因为缩进错误影响一大片",教学场景两种都要会读。

5.2 多环境 Profile ​

yaml
# 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 支持从十几种来源读配置,按优先级从高到低(高的覆盖低的)大致顺序:

  1. 命令行参数(--server.port=9000)
  2. SPRING_APPLICATION_JSON 环境变量/属性中的 JSON
  3. ServletConfig / ServletContext 初始化参数
  4. Java 系统属性(System.getProperties())
  5. 操作系统环境变量
  6. 随机值配置(RandomValuePropertySource,如 ${random.uuid})
  7. jar 包外部的 profile 专属配置文件(application-{profile}.yml)
  8. jar 包内部的 profile 专属配置文件
  9. jar 包外部的 application.yml
  10. jar 包内部的 application.yml
  11. @PropertySource 注解引入的配置
  12. 默认属性(SpringApplication.setDefaultProperties)

记忆口诀:命令行 > 环境变量 > 外部配置文件 > 内部(打包进 jar 的)配置文件 > 代码里的默认值。生产环境常用套路是"jar 包里放开发默认配置,用环境变量或外挂 application-prod.yml 覆盖敏感配置(数据库密码等)",这样镜像可以做到"一次构建,多环境部署"。

5.4 @ConfigurationProperties + 校验 ​

java
@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 MVCSpring 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 设计规范速览 ​

java
@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 参数校验 + 全局异常处理 ​

java
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/ServletResponseHandlerMethod,能拿到具体要执行的 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 JPAMyBatisMyBatis-PlusjOOQ
编程范式ORM,面向对象操作,自动生成 SQLSQL 手写,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 核心用法速览 ​

java
@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 节的详细解释
方法不是 publicSpring 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"。

java
@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 SecuritySa-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 无状态认证实战 ​

java
// 登录成功后签发 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 ConfigNacos 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 ​

维度DubboSpring Cloud OpenFeign
通信协议自定义二进制 RPC 协议,性能更高基于 HTTP + JSON,通用性更好,调试更直观(能直接用 curl/Postman 测)
生态定位更偏"RPC 框架",国内阿里系企业级项目常见Spring Cloud 生态原生组件,与 Spring Boot 整合度最高
跨语言调用较弱(原生面向 Java 生态)强(HTTP + JSON 天然跨语言,Go/Python/Node 服务都能直接调)
何时选它内部 Java 服务间高频调用、追求极致性能需要跨语言互调、或者希望调试链路更直观

9.4 Spring Cloud Gateway 实战速览 ​

java
@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 只需一行配置即可全局启用:

yaml
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 原生镜像:启动速度与内存的量级差异 ​

bash
# 传统方式: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:开箱即用的生产监控端点 ​

yaml
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus  # 按需暴露,不要无脑 include: "*"(安全隐患)
  endpoint:
    health:
      show-details: when-authorized
java
// 自定义健康检查:比如检查某个下游依赖是否可用
@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 分布式链路追踪落地 ​

yaml
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 ​

java
@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 ​

java
@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 容器实例,测试结束自动销毁。

java
@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 生态里性能最好的连接池实现),核心参数:

yaml
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 分层构建 ​

dockerfile
# 多阶段构建:第一阶段编译,第二阶段只拷贝产物,减小最终镜像体积
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 部署示例 ​

yaml
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.xml

16.2 核心业务代码:带缓存的图书查询 ​

java
@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 章的分层构建思路) ​

dockerfile
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. 硬核面试题精选

以下问题都是本教程正文里已经详细拆解过原理的高频面试题,建议学完对应章节后回来自测:

  1. @SpringBootApplication 由哪几个注解组成,各自的作用是什么?(回看 3.1)
  2. Spring Boot 的自动配置是怎么被发现和加载的?Boot 2 和 Boot 3/4 的实现方式有什么区别?(回看 3.2)
  3. 说说 Bean 的完整生命周期,AOP 代理是在哪个环节生成的?(回看 4.1)
  4. Spring 是怎么解决循环依赖的?为什么需要三级缓存而不是两级?构造器注入的循环依赖为什么无法被解决?(回看 4.2)
  5. @Transactional 在哪些场景下会失效?为什么?(回看 7.3)
  6. 说说 Spring MVC 处理一个请求的完整流程。(回看 6.1)
  7. Spring MVC 和 WebFlux 该怎么选?虚拟线程出现后,这个选型逻辑发生了什么变化?(回看 6.2、10.2)
  8. Filter、Interceptor、AOP 三者的区别和各自的适用场景?(回看 6.5)
  9. 缓存穿透、击穿、雪崩分别是什么问题,怎么解决?(回看 13.2)
  10. 外部化配置的优先级顺序是怎样的?(回看 5.3)
  11. 虚拟线程和传统平台线程、和 Go 的 Goroutine 有什么本质区别?(回看 10.2)
  12. GraalVM 原生镜像相比传统 JVM 部署,优劣分别是什么?(回看 10.3)

结语:这份教程覆盖的内容跨度很大,不建议一次性通读消化,建议按第 17 章的学习路线分阶段推进,每个阶段配合动手写代码(尤其是第 16 章的实战项目),原理性的内容(第 3、4、7.3、10.2 节)值得反复回顾,这些正是把"会用 Spring Boot"和"懂 Spring Boot"区分开的关键分水岭,也是技术面试里最容易拉开差距的地方。

基于 Vite 强力驱动 | 纯静态轻量托管