云计算百科
云计算领域专业知识百科平台

【SpringBoot 4.x 第209节】线上接口变慢如何定位:慢 SQL、慢接口与线程池耗尽排查实战!

🏆 本文收录于 《滚雪球学 Spring Boot 4.x》 专栏。 本专栏面向 有一定 Java 基础,但尚未系统学习 Spring Boot 的读者,采用“滚雪球式学习法”:先跑通、再理解、再重构、再上线,带你从第一个 Hello API 开始,逐步完成一个可部署、可监控、可扩展的后端项目。   🎯 适合人群: Java 初学者、后端入门同学、想系统提升 Spring Boot 工程能力的开发者、准备做项目写进简历的同学,以及想从 Spring Boot 2.x / 3.x 过渡到 Spring Boot 4.x 的开发者。

从“能写接口”到“能做项目”,从“知道注解”到“理解工程化”,这一次,我们不零散学,而是一路滚雪球!

🎉 限时福利:当前专栏活动中,一次订阅,终身阅读,后续更新章节全部免费解锁 👉 立即查看 👈️   🎁 想继续进阶?我还准备了完整的 Spring Boot 全栈进阶实战系列:

👉 《Spring Boot 2.x 实战》 👉 《Spring Boot 3.x 实战》 👉 《Spring Boot 4.x 实战》

想一次打通 Spring Boot 主流版本?可以直接学习 《Spring Boot 全栈实战合集》,系统覆盖 Spring Boot 2.x、3.x、4.x 核心特性、项目实战与企业级开发经验,助你从基础应用走向架构进阶。

演示环境说明:

  • 开发工具:IDEA 2025.x 或更高版本
  • JDK版本:JDK 17 或更高,推荐 JDK 21 / JDK 25
  • Spring Boot版本:4.0.x,例如 4.0.6
  • Spring Framework版本:7.x
  • Jakarta EE版本:Jakarta EE 11
  • Maven版本:3.6.3 或更高,推荐 3.9.x+
  • Gradle版本:Gradle 8.14+ 或 Gradle 9.x
  • 操作系统:Windows 11

全文目录:

  • 一、前言:为什么 Spring Boot 4 值得系统学习?
  • 二、本文要解决什么问题
  • 三、Spring Boot 4.x 的版本背景与技术基础
    • 3.1 Spring Boot 与 Spring Framework 的关系
    • 3.2 Java 17+ 是底线,不是建议
    • 3.3 Jakarta EE 11 与 `jakarta.*`
    • 3.4 模块化 Starter:不要再盲用旧依赖
    • 3.5 Jackson 3:接口返回 JSON 的默认背景变了
  • 四、Spring Boot 4.x 核心变化总览
  • 五、线上接口变慢的核心知识点
    • 5.1 接口慢,不等于 SQL 慢
    • 5.2 Spring Boot 4.x 请求处理流程图
    • 5.3 慢 SQL 怎么判断
    • 5.4 线程池耗尽为什么会让接口变慢
  • 六、项目环境准备
    • 6.1 技术栈选择
    • 6.2 项目分层架构图
  • 七、案例一:最小可运行示例——先定位慢接口
    • 7.1 Maven 依赖
      • 代码解析
    • 7.2 application.yml
      • 配置解析
    • 7.3 启动类
      • 代码解析
    • 7.4 统一响应对象
      • 代码解析
    • 7.5 traceId Filter
      • 代码解析
    • 7.6 慢请求拦截器
      • 代码解析
    • 7.7 基础 Controller
      • 代码解析
    • 7.8 启动与访问
  • 八、案例二:面向真实业务的改造——慢 SQL 与业务接口定位
    • 8.1 Entity:文章实体
      • 代码解析
    • 8.2 DTO 与 VO
      • 代码解析
    • 8.3 Repository
      • 代码解析
    • 8.4 业务异常与全局异常处理
      • 代码解析
    • 8.5 Service 层
      • 代码解析
    • 8.6 Controller
      • 代码解析
    • 8.7 初始化数据
      • 代码解析
    • 8.8 请求示例
    • 8.9 慢 Service 方法 AOP
      • 代码解析
  • 九、案例三:工程化增强实践——线程池耗尽、Actuator 与测试
    • 9.1 线程池配置属性
    • 9.2 线程池 Bean
      • 代码解析
    • 9.3 线程池快照 VO
    • 9.4 DiagnosticService
      • 代码解析
    • 9.5 DiagnosticController
      • 代码解析
    • 9.6 可观测性数据流转图
    • 9.7 慢 SQL DataSource 包装器
      • 代码解析
    • 9.8 RestTestClient 集成测试
      • 代码解析
  • 十、核心源码与配置解析
    • 10.1 一次慢接口日志应该怎么看
    • 10.2 慢接口定位流程图
    • 10.3 慢 SQL 的真实优化思路
    • 10.4 线程池耗尽的真实优化思路
  • 十一、常见问题与踩坑总结
    • 1. 为什么 Spring Boot 4.x 至少要求 Java 17?
    • 2. Spring Boot 4.x 和 Spring Framework 7.x 是什么关系?
    • 3. 为什么 Spring Boot 4.x 中不建议继续把 `spring-boot-starter-web` 作为主推荐依赖?
    • 4. `spring-boot-starter-webmvc` 和 `spring-boot-starter-web` 有什么区别?
    • 5. 为什么 `javax.validation` 在 Spring Boot 4.x 中不能作为主写法?
    • 6. `jakarta.validation`、`jakarta.persistence`、`jakarta.annotation` 分别对应什么场景?
    • 7. Controller 中是否应该直接返回 Entity?
    • 8. DTO、VO、Entity 有什么区别?
    • 9. 全局异常处理为什么没有生效?
    • 10. `application.yml` 配置为什么没有读取到?
    • 11. 参数校验注解为什么没有触发?
    • 12. 为什么加了 `@Valid` 还是没有进入校验?
    • 13. Service 接口和实现类是否必须拆分?
    • 14. Spring Boot 4.x 项目如何选择 Maven 依赖版本?
    • 15. 为什么测试类里的旧导入路径会报错?
    • 16. MockMvc、RestTestClient、WebTestClient 应该怎么选?
    • 17. Jackson 3 对老项目有什么影响?
    • 18. `RestTemplate` 还能不能用?新项目为什么更推荐 `RestClient`?
    • 19. 初学者如何判断自己的项目结构是否合理?
    • 20. 从 Spring Boot 3.x 升级到 4.x 时最应该先检查什么?
  • 十二、项目开发最佳实践
    • 12.1 包结构设计
    • 12.2 Controller 设计规范
    • 12.3 DTO / VO / Entity 使用边界
    • 12.4 统一响应格式
    • 12.5 全局异常处理
    • 12.6 参数校验
    • 12.7 日志记录
    • 12.8 配置管理
    • 12.9 Maven 依赖管理
    • 12.10 Spring Boot 4.x Starter 选择
    • 12.11 单元测试
    • 12.12 集成测试
    • 12.13 接口文档
    • 12.14 API Versioning
    • 12.15 可观测性
    • 12.16 初学者学习路线建议
  • 十三、扩展知识点:API Versioning、RestClient、Jackson 3、JSpecify、OpenTelemetry
    • 13.1 Spring Framework 7 的 API Versioning
    • 13.2 RestClient 与 HTTP Service Client
    • 13.3 Jackson 3 与接口响应
    • 13.4 JSpecify Null-Safety
    • 13.5 Actuator、Micrometer 与 OpenTelemetry
  • 十四、完整代码结构回顾
  • 十五、总结
  • 🧧 学习福利 · 限时开放 🧧
  • 🫵 Who am I?

一、前言:为什么 Spring Boot 4 值得系统学习?

我见过不少线上接口变慢的事故,刚开始排查时大家很容易陷入一个误区:一看到接口慢,就立刻怀疑数据库;一看到数据库没问题,又开始怀疑 JVM;JVM 没发现明显异常,最后才想起来看线程池、连接池、外部接口、日志 I/O。

真实项目里,接口变慢很少只有一个原因。它通常是一个链路问题:

客户端请求进来,经过网关、Tomcat、DispatcherServlet、Controller、Service、Repository、数据库连接池、数据库执行、JSON 序列化,最后返回响应。任何一个环节变慢,最终都会表现为“接口慢”。

Spring Boot 4.x 值得系统学习,并不是因为它只是把版本号从 3.x 改到了 4.x,而是因为它的依赖结构、Starter 组织方式、底层 Spring Framework 7.x、Jackson 3、Jakarta EE 11、测试 Starter、可观测性体系,都在进一步强调“工程化”和“模块化”。

官方文档显示,当前 Spring Boot 4.0.6 至少要求 Java 17,并要求 Spring Framework 7.0.7 或更高版本;Maven 明确支持 3.6.3 及以上版本。也就是说,如果你还拿 Java 8、Java 11 和旧教程里的 javax.* 写法来搭 Spring Boot 4.x 项目,很多问题不是业务代码错了,而是基础版本体系就错了。

本节的主题是“线上接口变慢如何定位”,核心围绕三类最常见问题:

  • 慢接口:请求本身耗时高,但不一定是数据库问题。
  • 慢 SQL:数据库查询、索引、事务、连接池导致响应变慢。
  • 线程池耗尽:业务线程池、Tomcat 线程池、数据库连接池、异步任务池被打满。
  • 我会按“零基础也能跑起来”的方式写,但不会停留在 hello world。你会看到一个从最小接口,到业务接口,再到工程化诊断能力的递进式案例。

    二、本文要解决什么问题

    这篇文章不是单纯教你写一个 Controller,也不是只列几个 Actuator 地址让你背。它要解决的是一个真实开发者迟早会遇到的问题:

    接口在线上突然变慢,我应该先看哪里?怎么证明到底是 SQL 慢、代码慢,还是线程池耗尽?

    我在项目里处理过几类典型场景。

    第一类是“单接口慢”。比如 /api/v1/articles/search 平时 100ms 返回,某天突然变成 3 秒。这个时候不能只看 Controller,因为 Controller 很可能只有一行 articleService.search()。我们要把请求耗时拆开,至少能知道是 Web 层、Service 层、Repository 层还是数据库层慢。

    第二类是“全站都慢”。这种往往不是某条 SQL 的锅,而可能是 Tomcat 线程池被占满、数据库连接池耗尽、业务异步线程池队列堆积、外部接口卡死、日志刷盘慢,甚至是某个全局锁导致大量请求排队。

    第三类是“偶发慢”。压测时不明显,线上偶尔慢。排查这种问题不能靠猜,要靠日志里的 traceId、接口耗时、慢 SQL 记录、线程池指标、Actuator 指标、必要时接入 OpenTelemetry 链路追踪。

    本文会带你完成一个 Spring Boot 4.x Web MVC 项目,逐步加入:

    • spring-boot-starter-webmvc
    • spring-boot-starter-validation
    • spring-boot-starter-data-jpa
    • spring-boot-starter-actuator
    • spring-boot-starter-webmvc-test
    • spring-boot-starter-data-jpa-test
    • 慢接口日志
    • 慢 SQL 日志包装器
    • DTO / VO / Entity 分层
    • Jakarta Validation 参数校验
    • 全局异常处理
    • 线程池配置与耗尽模拟
    • Actuator 健康检查与指标查看
    • RestTestClient 集成测试

    特别说明:本文主案例使用 Spring Data JPA,而不是 MyBatis。原因很简单:Spring Boot 4.x 生态刚进入新一代版本体系时,如果第三方 Starter 的兼容性你没有明确确认,就不要在入门项目里强行引入。JPA 是 Spring Boot 官方 Starter 体系内的一等公民,适合本篇文章做主案例。

    三、Spring Boot 4.x 的版本背景与技术基础

    3.1 Spring Boot 与 Spring Framework 的关系

    很多初学者会把 Spring Boot 和 Spring Framework 混在一起。简单说:

    Spring Framework 是底层核心框架,提供 IoC、Bean 管理、AOP、事务、Spring MVC、数据绑定、校验、事件、资源加载等基础能力。

    Spring Boot 是工程脚手架和自动配置体系,它帮你把一堆常见依赖、默认配置、嵌入式服务器、日志、健康检查、外部化配置整合起来,让你更快搭建可运行项目。

    在 Spring Boot 4.x 体系里,底层基于 Spring Framework 7.x。学习 Spring Boot 时不需要一开始就啃完整源码,但至少要理解几个词:

    • IoC:对象由 Spring 容器创建和管理。
    • Bean:被 Spring 容器管理的对象。
    • MVC:请求从 DispatcherServlet 进入,再分发到 Controller。
    • AOP:不改业务代码的情况下,在方法前后插入横切逻辑,例如日志、耗时统计。
    • 事务:一组数据库操作要么全部成功,要么全部失败。

    本文的慢接口定位,就会用到 MVC 请求链路、AOP 方法耗时统计、DataSource 包装、Actuator 指标暴露这些能力。

    3.2 Java 17+ 是底线,不是建议

    Spring Boot 4.x 官方系统要求至少 Java 17,当前文档还显示 Spring Boot 4.0.6 兼容到 Java 26。对初学者来说,我建议直接使用 Java 21;如果你所在团队已经准备跟进 Java 25,也可以用 Java 25,但不要再拿 Java 8 或 Java 11 创建 Boot 4 项目。

    为什么这点对“接口变慢排查”也重要?

    因为现代 Java 带来了更好的运行时、GC、诊断工具、JFR、线程分析能力。Java 21 之后虚拟线程也进入稳定阶段,虽然本文不会把虚拟线程当成所有性能问题的万能药,但你至少要知道:Spring Boot 4.x 项目已经站在现代 Java 的基础上,而不是过去 Java 8 时代的开发习惯。

    3.3 Jakarta EE 11 与 jakarta.*

    如果你复制旧教程里的代码,经常会看到:

    import javax.validation.Valid;
    import javax.persistence.Entity;
    import javax.annotation.PostConstruct;

    在 Spring Boot 4.x / Spring Framework 7.x 体系中,主代码示例应该使用:

    import jakarta.validation.Valid;
    import jakarta.persistence.Entity;
    import jakarta.annotation.PostConstruct;

    这是从 Java EE 到 Jakarta EE 命名空间迁移之后的结果。你可以把它理解为:生态已经换了新包名,旧 javax.* 代码不能作为新项目主写法。

    本文所有实体、校验、Servlet Filter 都会使用 jakarta.*。

    3.4 模块化 Starter:不要再盲用旧依赖

    Spring Boot 4.x 更强调模块化 Starter。官方 Starter 列表中,spring-boot-starter-webmvc 是 Spring MVC + Tomcat 的 Starter,spring-boot-starter-webmvc-test 是对应测试 Starter;同时,旧的 spring-boot-starter-web 在当前文档中已经标记为推荐迁移到 spring-boot-starter-webmvc 的方向。

    所以本文不会把下面这个作为主推荐:

    <artifactId>spring-boot-starter-web</artifactId>

    而是使用:

    <artifactId>spring-boot-starter-webmvc</artifactId>

    这对初学者非常关键。很多启动失败、测试类导入异常、自动配置不符合预期的问题,并不是你 Controller 写错了,而是依赖一开始就选错了。

    3.5 Jackson 3:接口返回 JSON 的默认背景变了

    Spring Boot 4.x 默认优先使用 Jackson 3。官方文档明确说明 Jackson 3 是 preferred and default library,同时 Jackson 2 支持是为了迁移而保留,并且会在未来的 Spring Boot 4.x 版本中移除。

    这意味着:

    • 你返回的 VO、record、LocalDateTime、Instant,最终会被 Jackson 3 序列化。
    • 老项目里自定义 Jackson 2 的序列化器、模块、ObjectMapper 配置,迁移时要重新确认。
    • 不要随便复制旧教程中针对 Jackson 2 的配置。

    本文会用 record 做响应对象和 VO,同时提醒你在真实项目中关注日期格式、枚举序列化和空字段策略。

    四、Spring Boot 4.x 核心变化总览

    下面这张表是本文最关心的 Spring Boot 4.x 开发习惯变化。

    主题Spring Boot 4.x 推荐习惯初学者容易踩坑
    Web MVC 依赖 spring-boot-starter-webmvc 继续把 spring-boot-starter-web 当主入口
    Web MVC 测试 spring-boot-starter-webmvc-test 只复制旧版 spring-boot-starter-test 示例,不知道模块化测试 Starter
    参数校验 jakarta.validation.* 使用 javax.validation.*
    JPA 实体 jakarta.persistence.* 使用 javax.persistence.*
    JSON Jackson 3 默认优先 复制 Jackson 2 自定义配置
    HTTP 客户端 RestClient、HTTP Service Client、WebClient 新项目仍然首推 RestTemplate
    API 版本 Spring Framework 7 MVC 支持 API Versioning 只靠手写 /v1 字符串,没有接口演进意识
    空安全 Spring Framework 7 使用 JSpecify 标注自身 API 忽略 null 契约,线上 NPE 难排查
    可观测性 Actuator、Micrometer、OpenTelemetry 上线后只看控制台日志

    官方文档里,Spring Framework 的 REST 客户端章节已经把 RestClient 作为同步 fluent API,把 WebClient 作为响应式客户端,把 HTTP Service Client 作为接口式客户端。RestTemplate 仍然存在,但它是早期模板式 API,不适合作为新项目主推荐。

    这和本文主题也有关。接口慢不只发生在本服务内部,调用外部 HTTP 服务慢也很常见。新项目里,我们应该优先围绕 RestClient.Builder、超时、拦截器、链路追踪来设计,而不是继续到处 new RestTemplate。

    五、线上接口变慢的核心知识点

    5.1 接口慢,不等于 SQL 慢

    我刚做后端时,接口慢第一反应就是“是不是 SQL 没加索引”。后来线上事故看多了,才发现很多慢接口根本没慢 SQL。

    常见原因包括:

    • Controller 接收大请求体,JSON 反序列化很慢。
    • 参数校验逻辑写了远程调用。
    • Service 里循环查库,形成 N+1 查询。
    • 外部 HTTP 接口无超时,线程被长时间占住。
    • 线程池队列堆积,新任务排队。
    • 数据库连接池耗尽,请求在等连接。
    • Tomcat 工作线程被阻塞。
    • 日志量过大,异步日志队列打满。
    • 响应对象过大,JSON 序列化耗时。
    • GC、CPU 抖动、磁盘 I/O 抖动。

    所以排查慢接口,一定要按链路拆解,而不是凭经验拍脑袋。

    5.2 Spring Boot 4.x 请求处理流程图

    相关示意图绘制如下,仅供参考:

    这张图很重要。线上接口变慢,本质就是图中某个节点耗时异常。 我的排查习惯是先看入口总耗时,再看 Service 方法耗时,再看 SQL 耗时,最后结合线程池、连接池、JVM 指标判断是不是资源耗尽。

    如果你只在 Controller 里打日志,很容易漏掉真正的慢点。比较稳妥的做法是:

    • Filter 负责生成 traceId。
    • HandlerInterceptor 记录接口总耗时。
    • AOP 记录 Service 方法耗时。
    • DataSource 包装器记录 SQL 耗时。
    • Actuator 暴露线程、JVM、HTTP、指标信息。
    • 必要时接入 OpenTelemetry 做跨服务链路追踪。

    5.3 慢 SQL 怎么判断

    慢 SQL 不只是“执行时间长”。在真实项目中,我通常会看这些维度:

  • SQL 执行耗时是否超过阈值。
  • 是否走索引。
  • 返回行数是否过大。
  • 是否有排序、分组、模糊查询导致临时表。
  • 是否存在 N+1 查询。
  • 是否因为事务过长导致锁等待。
  • 是否因为连接池耗尽导致“拿连接”慢,而不是 SQL 本身慢。
  • 是否业务代码循环调用 Repository。
  • 一个接口慢,SQL 日志显示某条语句只执行 20ms,但接口总耗时 3 秒,这种情况就不能继续死盯 SQL。反过来,如果接口总耗时 800ms,其中 SQL 一条就 700ms,那优化方向就很明确。

    5.4 线程池耗尽为什么会让接口变慢

    线程池耗尽的可怕之处在于,它经常不是让接口立刻报错,而是让接口慢慢排队。

    例如一个业务线程池配置如下:

    • 核心线程:2
    • 最大线程:2
    • 队列容量:1000

    当请求量突然上来,前 2 个任务在执行,后面 1000 个任务排队。用户看到的不是马上失败,而是请求越来越慢,最后超时。 这类问题如果没有线程池指标,很容易被误判成“数据库慢”或“网络慢”。

    Spring Boot Actuator 的 metrics 能帮助我们观察应用指标。官方文档也说明,Spring Boot 会对可用的 ThreadPoolTaskExecutor、ThreadPoolTaskScheduler 进行指标化,只要底层 ThreadPoolExecutor 可用,指标会带上执行器名称。

    这也是为什么本文案例三要专门做线程池耗尽模拟。

    六、项目环境准备

    6.1 技术栈选择

    本文项目采用:

    JDK:Java 21
    Spring Boot:4.0.6
    Spring Framework:7.x
    构建工具:Maven 3.6.3+
    Web:Spring Web MVC
    数据库:H2,方便本地运行
    ORM:Spring Data JPA
    校验:Jakarta Validation
    JSON:Jackson 3
    可观测性:Actuator、Micrometer
    测试:RestTestClient、JUnit Jupiter

    这里使用 H2 是为了让读者不用先安装 MySQL。真实项目中,你可以替换为 MySQL 8.x,再配合 MySQL slow query log 和 EXPLAIN ANALYZE 排查慢 SQL。

    6.2 项目分层架构图

    相关示意图绘制如下,仅供参考:

    这张图体现的是工程分层。 Controller 不应该直接写 SQL,也不应该塞满业务判断;Repository 不应该返回给前端;Entity 不应该直接暴露给客户端。 慢接口定位能力也应该是横切能力:Filter、Interceptor、AOP、DataSource 包装器都属于工程基础设施,不应该污染业务逻辑。

    七、案例一:最小可运行示例——先定位慢接口

    案例一目标很简单:先把 Spring Boot 4.x Web MVC 项目跑起来,并加入最基础的慢接口日志。

    7.1 Maven 依赖

    <!– pom.xml –>
    <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>4.0.6</version>
    <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>slow-api-lab</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>slow-api-lab</name>
    <description>Spring Boot 4.x slow API diagnostics demo</description>

    <properties>
    <java.version>21</java.version>
    </properties>

    <dependencies>
    <!– Spring Boot 4.x 推荐的 Spring Web MVC Starter –>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webmvc</artifactId>
    </dependency>

    <!– Jakarta Validation 参数校验 –>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>

    <!– AOP:用于统计 Service 方法耗时 –>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aspectj</artifactId>
    </dependency>

    <!– Spring Data JPA,本文主案例使用官方数据访问 Starter –>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>

    <!– H2 内存数据库,方便零基础读者本地直接运行 –>
    <dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
    </dependency>

    <!– Actuator:健康检查、指标、线程转储等线上诊断入口 –>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>

    <!– Spring MVC 对应的测试 Starter –>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webmvc-test</artifactId>
    <scope>test</scope>
    </dependency>

    <!– JPA 对应的测试 Starter –>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa-test</artifactId>
    <scope>test</scope>
    </dependency>
    </dependencies>

    <build>
    <plugins>
    <!– 由 Spring Boot Parent 管理插件版本,不要手动乱配 –>
    <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
    </plugins>
    </build>
    </project>

    代码解析

    这份 pom.xml 有几个关键点。

    第一,Web MVC 使用 spring-boot-starter-webmvc,不是把旧教程里的 spring-boot-starter-web 继续当主推荐。官方 Starter 列表已经明确给出 webmvc 和 webmvc-test,这正是 Spring Boot 4.x 模块化 Starter 的体现。

    第二,测试依赖没有只放一个泛泛的测试 Starter,而是补了 spring-boot-starter-webmvc-test 和 spring-boot-starter-data-jpa-test。这样做的好处是测试依赖更贴近技术模块,后续做 Web MVC 接口测试和 JPA 测试时不容易缺包。

    第三,没有手动指定 Spring Framework、Hibernate、Jackson、JUnit 的版本。Spring Boot Parent 会统一管理这些版本。初学者最容易犯的错误就是觉得“版本越新越好”,手动加一堆版本号,最后造成依赖冲突。

    7.2 application.yml

    # src/main/resources/application.yml
    server:
    port: 8080

    spring:
    application:
    name: slowapilab

    datasource:
    url: jdbc:h2:mem:slow_api_lab;MODE=MySQL;DATABASE_TO_LOWER=TRUE
    driver-class-name: org.h2.Driver
    username: sa
    password:

    h2:
    console:
    enabled: true
    path: /h2console

    jpa:
    hibernate:
    ddl-auto: createdrop
    defer-datasource-initialization: true
    open-in-view: false
    properties:
    hibernate:
    format_sql: true

    management:
    endpoints:
    web:
    exposure:
    include: health,info,metrics,threaddump,loggers
    endpoint:
    health:
    show-details: always

    logging:
    pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%X{traceId}] %logger{36} – %msg%n"
    level:
    com.example.slowapi: info
    org.hibernate.SQL: debug
    org.hibernate.orm.jdbc.bind: trace

    app:
    performance:
    slow-request-ms: 500
    slow-method-ms: 300
    slow-sql-ms: 200
    executor:
    diagnostic:
    core-size: 2
    max-size: 2
    queue-capacity: 2

    配置解析

    open-in-view: false 是我在真实项目中非常推荐的配置。它可以避免在 Controller 返回阶段还懒加载数据库关联对象。初学阶段可能觉得开着方便,但真实项目里它会让 SQL 发生在你意想不到的位置,慢接口排查会非常痛苦。

    日志格式里加入 %X{traceId},后面我们会在 Filter 里写入 traceId。这样一次请求从入口到 Service、SQL、异常处理,日志都能串起来。

    app.performance.* 是我们自定义的慢请求、慢方法、慢 SQL 阈值。线上项目里,不同接口、不同数据库的阈值不一定一样,但初学阶段先用统一阈值,有助于建立排查思路。

    7.3 启动类

    // src/main/java/com/example/slowapi/SlowApiLabApplication.java
    package com.example.slowapi;

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

    @SpringBootApplication
    public class SlowApiLabApplication {

    public static void main(String[] args) {
    // Spring Boot 4.x 应用入口,启动内嵌 Tomcat 和 Spring 容器
    SpringApplication.run(SlowApiLabApplication.class, args);
    }
    }

    代码解析

    @SpringBootApplication 是一个组合注解,包含自动配置、组件扫描、配置类能力。 在 Spring Boot 4.x 中,你仍然可以用这种入口写法。变化不在启动类本身,而在依赖结构、底层框架版本和默认自动配置体系。

    7.4 统一响应对象

    // src/main/java/com/example/slowapi/common/ApiResponse.java
    package com.example.slowapi.common;

    import java.time.Instant;

    import org.slf4j.MDC;

    /**
    * 统一接口响应对象。
    * code 为 0 表示成功,非 0 表示失败。
    */

    public record ApiResponse<T>(
    int code,
    String message,
    T data,
    Instant timestamp,
    String traceId
    ) {

    public static <T> ApiResponse<T> ok(T data) {
    return new ApiResponse<>(0, "success", data, Instant.now(), currentTraceId());
    }

    public static <T> ApiResponse<T> fail(int code, String message) {
    return new ApiResponse<>(code, message, null, Instant.now(), currentTraceId());
    }

    private static String currentTraceId() {
    String traceId = MDC.get("traceId");
    return traceId == null ? "" : traceId;
    }
    }

    代码解析

    这段代码把所有接口返回统一成固定结构。 真实项目中,前端最怕每个接口返回格式都不一样:有的返回对象,有的返回数组,有的返回字符串,有的失败时直接抛 HTML 错误页。统一响应能降低联调成本,也方便全局异常处理。

    这里用 Java record,代码更简洁。Spring Boot 4.x 默认进入 Jackson 3 体系后,普通 record 可以正常序列化为 JSON。你后续如果要自定义 Instant 格式、枚举格式、空字段策略,需要基于 Jackson 3 的配置方式处理,不能直接复制老项目 Jackson 2 的自定义类。

    7.5 traceId Filter

    // src/main/java/com/example/slowapi/infra/TraceIdFilter.java
    package com.example.slowapi.infra;

    import java.io.IOException;
    import java.util.UUID;

    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.stereotype.Component;
    import org.springframework.web.filter.OncePerRequestFilter;

    /**
    * 每个请求生成一个 traceId。
    * 线上排查慢接口时,traceId 是串联请求日志、SQL 日志、异常日志的关键。
    */

    @Component
    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 {
    String traceId = request.getHeader("X-Trace-Id");
    if (traceId == null || traceId.isBlank()) {
    traceId = UUID.randomUUID().toString().replace("-", "");
    }

    try {
    MDC.put(TRACE_ID, traceId);
    response.setHeader("X-Trace-Id", traceId);
    filterChain.doFilter(request, response);
    } finally {
    MDC.remove(TRACE_ID);
    }
    }
    }

    代码解析

    注意这里使用的是 jakarta.servlet.*,不是 javax.servlet.*。这是 Spring Boot 4.x / Spring Framework 7.x 体系下必须养成的习惯。

    OncePerRequestFilter 保证一次请求只执行一次。我们把 traceId 放进 MDC,日志格式里通过 %X{traceId} 输出。这样当线上出现慢接口时,你可以根据 traceId 查到同一次请求的完整日志。

    7.6 慢请求拦截器

    // src/main/java/com/example/slowapi/config/PerformanceProperties.java
    package com.example.slowapi.config;

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

    /**
    * 慢请求、慢方法、慢 SQL 阈值配置。
    */

    @ConfigurationProperties(prefix = "app.performance")
    public class PerformanceProperties {

    private long slowRequestMs = 500;
    private long slowMethodMs = 300;
    private long slowSqlMs = 200;

    public long getSlowRequestMs() {
    return slowRequestMs;
    }

    public void setSlowRequestMs(long slowRequestMs) {
    this.slowRequestMs = slowRequestMs;
    }

    public long getSlowMethodMs() {
    return slowMethodMs;
    }

    public void setSlowMethodMs(long slowMethodMs) {
    this.slowMethodMs = slowMethodMs;
    }

    public long getSlowSqlMs() {
    return slowSqlMs;
    }

    public void setSlowSqlMs(long slowSqlMs) {
    this.slowSqlMs = slowSqlMs;
    }
    }

    // src/main/java/com/example/slowapi/infra/SlowRequestInterceptor.java
    package com.example.slowapi.infra;

    import jakarta.servlet.http.HttpServletRequest;
    import jakarta.servlet.http.HttpServletResponse;

    import com.example.slowapi.config.PerformanceProperties;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.web.servlet.HandlerInterceptor;

    /**
    * 统计每个 HTTP 请求的总耗时。
    */

    public class SlowRequestInterceptor implements HandlerInterceptor {

    private static final Logger log = LoggerFactory.getLogger(SlowRequestInterceptor.class);
    private static final String START_TIME = "startTimeNanos";

    private final PerformanceProperties properties;

    public SlowRequestInterceptor(PerformanceProperties properties) {
    this.properties = properties;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
    request.setAttribute(START_TIME, System.nanoTime());
    return true;
    }

    @Override
    public void afterCompletion(
    HttpServletRequest request,
    HttpServletResponse response,
    Object handler,
    Exception ex
    ) {
    Object start = request.getAttribute(START_TIME);
    if (!(start instanceof Long startTime)) {
    return;
    }

    long costMs = (System.nanoTime() startTime) / 1_000_000;
    String method = request.getMethod();
    String uri = request.getRequestURI();
    int status = response.getStatus();

    if (costMs >= properties.getSlowRequestMs()) {
    log.warn("慢接口 detected method={} uri={} status={} costMs={}", method, uri, status, costMs);
    } else {
    log.info("接口完成 method={} uri={} status={} costMs={}", method, uri, status, costMs);
    }
    }
    }

    // src/main/java/com/example/slowapi/config/WebMvcConfig.java
    package com.example.slowapi.config;

    import com.example.slowapi.infra.SlowRequestInterceptor;
    import org.springframework.boot.context.properties.EnableConfigurationProperties;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

    @Configuration
    @EnableConfigurationProperties(PerformanceProperties.class)
    public class WebMvcConfig implements WebMvcConfigurer {

    private final PerformanceProperties performanceProperties;

    public WebMvcConfig(PerformanceProperties performanceProperties) {
    this.performanceProperties = performanceProperties;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new SlowRequestInterceptor(performanceProperties))
    .addPathPatterns("/api/**");
    }
    }

    代码解析

    HandlerInterceptor 适合统计接口总耗时。它比在每个 Controller 方法里手写计时更统一,也不污染业务代码。

    这里我们只拦截 /api/**,避免 Actuator、H2 Console 等内部路径影响业务日志。 如果接口耗时超过 app.performance.slow-request-ms,日志打 warn,否则打 info。

    注意:这个拦截器只能告诉你“接口总耗时慢”,还不能告诉你慢在哪里。后面的案例会继续加入 Service 方法耗时、SQL 耗时和线程池诊断。

    7.7 基础 Controller

    // src/main/java/com/example/slowapi/controller/PingController.java
    package com.example.slowapi.controller;

    import com.example.slowapi.common.ApiResponse;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;

    @RestController
    class PingController {

    @GetMapping("/api/v1/ping")
    ApiResponse<String> ping() {
    return ApiResponse.ok("pong");
    }

    @GetMapping("/api/v1/ping/slow")
    ApiResponse<String> slow(@RequestParam(defaultValue = "800") long delayMs)
    throws InterruptedException {
    // 模拟接口内部阻塞,真实项目中可能是慢 SQL、外部接口慢、锁等待等
    Thread.sleep(delayMs);
    return ApiResponse.ok("slow pong, delayMs=" + delayMs);
    }
    }

    代码解析

    /api/v1/ping 是正常接口。 /api/v1/ping/slow 用 Thread.sleep 模拟慢接口。

    这不是鼓励你在业务代码里 sleep,而是为了让零基础读者能马上看到慢接口日志。真实项目中,慢点可能发生在数据库、外部接口、线程池排队、JSON 序列化等位置。

    7.8 启动与访问

    启动:

    mvn spring-boot:run

    访问正常接口:

    curl http://localhost:8080/api/v1/ping

    响应示例:

    {
    "code": 0,
    "message": "success",
    "data": "pong",
    "timestamp": "2026-05-20T10:00:00Z",
    "traceId": "d0f5d890ff9a46ff9b5a7a0a64a73c51"
    }

    访问慢接口:

    curl "http://localhost:8080/api/v1/ping/slow?delayMs=900"

    日志中会看到类似:

    WARN [d0f5d890ff9a46ff9b5a7a0a64a73c51] SlowRequestInterceptor – 慢接口 detected method=GET uri=/api/v1/ping/slow status=200 costMs=907

    到这里,案例一完成。你已经有了一个 Spring Boot 4.x Web MVC 项目,并能记录接口总耗时。

    八、案例二:面向真实业务的改造——慢 SQL 与业务接口定位

    案例二我们加入真实业务结构:文章发布与文章搜索。 业务不复杂,但足够展示 DTO、VO、Entity、Repository、Service、异常处理、参数校验和慢 SQL 定位。

    8.1 Entity:文章实体

    // src/main/java/com/example/slowapi/entity/Article.java
    package com.example.slowapi.entity;

    import java.time.Instant;

    import jakarta.persistence.Column;
    import jakarta.persistence.Entity;
    import jakarta.persistence.GeneratedValue;
    import jakarta.persistence.GenerationType;
    import jakarta.persistence.Id;
    import jakarta.persistence.PrePersist;
    import jakarta.persistence.PreUpdate;
    import jakarta.persistence.Table;

    /**
    * Article 是数据库实体,只负责和数据库表结构映射。
    * 不建议直接把 Entity 返回给前端。
    */

    @Entity
    @Table(name = "articles")
    public class Article {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 120)
    private String title;

    @Column(nullable = false, length = 4000)
    private String content;

    @Column(nullable = false, length = 20)
    private String status;

    @Column(nullable = false)
    private Instant createdAt;

    @Column(nullable = false)
    private Instant updatedAt;

    protected Article() {
    // JPA 需要无参构造器
    }

    public Article(String title, String content, String status) {
    this.title = title;
    this.content = content;
    this.status = status;
    }

    @PrePersist
    void prePersist() {
    Instant now = Instant.now();
    this.createdAt = now;
    this.updatedAt = now;
    }

    @PreUpdate
    void preUpdate() {
    this.updatedAt = Instant.now();
    }

    public Long getId() {
    return id;
    }

    public String getTitle() {
    return title;
    }

    public String getContent() {
    return content;
    }

    public String getStatus() {
    return status;
    }

    public Instant getCreatedAt() {
    return createdAt;
    }

    public Instant getUpdatedAt() {
    return updatedAt;
    }
    }

    代码解析

    这段代码使用 jakarta.persistence.*,这是 Spring Boot 4.x 体系下的正确写法。

    Entity 的职责是数据库映射,不是接口返回模型。初学者很喜欢直接 return articleRepository.findAll(),这样做短期省事,长期会带来几个问题:

    • 数据库字段变化会影响前端响应。
    • 懒加载关联可能在序列化阶段触发 SQL。
    • 敏感字段容易泄漏。
    • JSON 序列化可能出现循环引用。
    • 接口版本演进困难。

    所以我们后面会用 VO 返回给前端。

    8.2 DTO 与 VO

    // src/main/java/com/example/slowapi/dto/CreateArticleRequest.java
    package com.example.slowapi.dto;

    import jakarta.validation.constraints.NotBlank;
    import jakarta.validation.constraints.Size;

    /**
    * DTO 负责接收客户端请求参数。
    */

    public record CreateArticleRequest(

    @NotBlank(message = "标题不能为空")
    @Size(max = 120, message = "标题不能超过 120 个字符")
    String title,

    @NotBlank(message = "内容不能为空")
    @Size(max = 4000, message = "内容不能超过 4000 个字符")
    String content
    ) {
    }

    // src/main/java/com/example/slowapi/vo/ArticleView.java
    package com.example.slowapi.vo;

    import java.time.Instant;

    /**
    * VO 负责返回给前端。
    */

    public record ArticleView(
    Long id,
    String title,
    String summary,
    String status,
    Instant createdAt
    ) {
    }

    代码解析

    DTO 和 VO 的分离,是我非常建议初学者从第一天就养成的习惯。

    DTO 面向输入,关注参数校验。 VO 面向输出,关注接口契约。 Entity 面向数据库,关注表结构。

    这三者不要混用。你会发现项目越大,分清楚边界越省心。

    这里校验注解来自 jakarta.validation.constraints.*。如果你写成 javax.validation.constraints.NotBlank,在 Spring Boot 4.x 新项目中就是错误方向。

    8.3 Repository

    // src/main/java/com/example/slowapi/repository/ArticleRepository.java
    package com.example.slowapi.repository;

    import java.util.List;

    import com.example.slowapi.entity.Article;
    import org.springframework.data.jpa.repository.JpaRepository;

    public interface ArticleRepository extends JpaRepository<Article, Long> {

    /**
    * 根据标题模糊搜索。
    * 真实 MySQL 项目中,如果数据量大,这类 like '%keyword%' 很容易成为慢 SQL。
    */

    List<Article> findByTitleContainingIgnoreCaseOrderByCreatedAtDesc(String keyword);
    }

    代码解析

    这里用 Spring Data JPA 的方法名查询。 ContainingIgnoreCase 会生成模糊匹配查询。数据少时没感觉,数据量大时,like '%keyword%' 通常无法有效使用普通 BTree 索引,是慢 SQL 的常见来源。

    这正好适合我们讲慢 SQL:不是所有 Repository 方法都安全。方法名看起来优雅,但背后生成的 SQL 是否高效,仍然需要你通过日志、数据库慢查询、执行计划来验证。

    8.4 业务异常与全局异常处理

    // src/main/java/com/example/slowapi/exception/BusinessException.java
    package com.example.slowapi.exception;

    /**
    * 业务异常,表示请求参数合法但业务规则不允许。
    */

    public class BusinessException extends RuntimeException {

    private final int code;

    public BusinessException(int code, String message) {
    super(message);
    this.code = code;
    }

    public int code() {
    return code;
    }
    }

    // src/main/java/com/example/slowapi/exception/GlobalExceptionHandler.java
    package com.example.slowapi.exception;

    import java.util.stream.Collectors;

    import com.example.slowapi.common.ApiResponse;
    import jakarta.validation.ConstraintViolationException;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.http.HttpStatus;
    import org.springframework.web.bind.MethodArgumentNotValidException;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.ResponseStatus;
    import org.springframework.web.bind.annotation.RestControllerAdvice;

    import java.util.concurrent.RejectedExecutionException;

    /**
    * 全局异常处理,保证异常响应格式统一。
    */

    @RestControllerAdvice
    public class GlobalExceptionHandler {

    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler(BusinessException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ApiResponse<Void> handleBusinessException(BusinessException ex) {
    return ApiResponse.fail(ex.code(), ex.getMessage());
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ApiResponse<Void> handleMethodArgumentNotValid(MethodArgumentNotValidException ex) {
    String message = ex.getBindingResult()
    .getFieldErrors()
    .stream()
    .map(error -> error.getField() + ": " + error.getDefaultMessage())
    .collect(Collectors.joining("; "));
    return ApiResponse.fail(400, message);
    }

    @ExceptionHandler(ConstraintViolationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ApiResponse<Void> handleConstraintViolation(ConstraintViolationException ex) {
    return ApiResponse.fail(400, ex.getMessage());
    }

    @ExceptionHandler(RejectedExecutionException.class)
    @ResponseStatus(HttpStatus.TOO_MANY_REQUESTS)
    public ApiResponse<Void> handleRejectedExecution(RejectedExecutionException ex) {
    return ApiResponse.fail(429, "系统繁忙,请稍后重试:" + ex.getMessage());
    }

    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ApiResponse<Void> handleException(Exception ex) {
    log.error("未处理异常", ex);
    return ApiResponse.fail(500, "系统异常,请联系管理员");
    }
    }

    代码解析

    全局异常处理有两个作用。

    第一,给前端稳定的错误格式。 第二,让异常日志带 traceId,方便慢接口排查。

    比如参数校验失败,如果不处理,Spring 默认错误响应对前端不一定友好。我们把字段错误拼成清晰消息。

    这里也提前处理了 RejectedExecutionException。在线程池耗尽时,我们不希望用户一直等待到超时,而是尽快返回 429,告诉客户端系统繁忙。

    8.5 Service 层

    // src/main/java/com/example/slowapi/service/ArticleService.java
    package com.example.slowapi.service;

    import java.util.List;

    import com.example.slowapi.dto.CreateArticleRequest;
    import com.example.slowapi.vo.ArticleView;

    public interface ArticleService {

    ArticleView create(CreateArticleRequest request);

    List<ArticleView> search(String keyword);
    }

    // src/main/java/com/example/slowapi/service/impl/ArticleServiceImpl.java
    package com.example.slowapi.service.impl;

    import java.util.List;

    import com.example.slowapi.dto.CreateArticleRequest;
    import com.example.slowapi.entity.Article;
    import com.example.slowapi.exception.BusinessException;
    import com.example.slowapi.repository.ArticleRepository;
    import com.example.slowapi.service.ArticleService;
    import com.example.slowapi.vo.ArticleView;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;

    @Service
    public class ArticleServiceImpl implements ArticleService {

    private final ArticleRepository articleRepository;

    public ArticleServiceImpl(ArticleRepository articleRepository) {
    this.articleRepository = articleRepository;
    }

    @Override
    @Transactional
    public ArticleView create(CreateArticleRequest request) {
    if (request.title().contains("违法")) {
    throw new BusinessException(1001, "文章标题包含不允许发布的词");
    }

    Article article = new Article(request.title(), request.content(), "PUBLISHED");
    Article saved = articleRepository.save(article);
    return toView(saved);
    }

    @Override
    @Transactional(readOnly = true)
    public List<ArticleView> search(String keyword) {
    String safeKeyword = keyword == null ? "" : keyword.trim();

    // 初学阶段可以允许空关键字,但真实项目应限制分页和最大返回数量
    return articleRepository.findByTitleContainingIgnoreCaseOrderByCreatedAtDesc(safeKeyword)
    .stream()
    .map(this::toView)
    .toList();
    }

    private ArticleView toView(Article article) {
    String content = article.getContent();
    String summary = content.length() <= 60 ? content : content.substring(0, 60) + "…";

    return new ArticleView(
    article.getId(),
    article.getTitle(),
    summary,
    article.getStatus(),
    article.getCreatedAt()
    );
    }
    }

    代码解析

    Service 层负责业务规则,比如标题里不能包含某些词。 Repository 只负责数据访问,不应该知道业务规则。 Controller 只负责接收请求和返回结果,不应该直接操作数据库。

    @Transactional(readOnly = true) 对查询方法很有意义。它明确告诉事务管理器这是只读操作。真实项目中还要关注事务边界不要过大,因为事务过长可能导致锁等待和连接占用,最终表现成接口变慢。

    8.6 Controller

    // src/main/java/com/example/slowapi/controller/ArticleController.java
    package com.example.slowapi.controller;

    import java.util.List;

    import com.example.slowapi.common.ApiResponse;
    import com.example.slowapi.dto.CreateArticleRequest;
    import com.example.slowapi.service.ArticleService;
    import com.example.slowapi.vo.ArticleView;
    import jakarta.validation.Valid;
    import jakarta.validation.constraints.Size;
    import org.springframework.validation.annotation.Validated;
    import org.springframework.web.bind.annotation.*;

    @RestController
    @RequestMapping("/api/v1/articles")
    @Validated
    class ArticleController {

    private final ArticleService articleService;

    ArticleController(ArticleService articleService) {
    this.articleService = articleService;
    }

    @PostMapping
    ApiResponse<ArticleView> create(@Valid @RequestBody CreateArticleRequest request) {
    return ApiResponse.ok(articleService.create(request));
    }

    @GetMapping("/search")
    ApiResponse<List<ArticleView>> search(
    @RequestParam(defaultValue = "")
    @Size(max = 50, message = "搜索关键词不能超过 50 个字符")
    String keyword
    ) {
    return ApiResponse.ok(articleService.search(keyword));
    }
    }

    代码解析

    @Valid @RequestBody 触发请求体校验。 @Validated 让 @RequestParam 上的约束也能生效。很多初学者只给 DTO 加了校验注解,却忘了 Controller 方法参数校验的触发条件。

    这里仍然使用 /api/v1/articles,是为了让读者建立 API 版本意识。后面扩展章节会讲 Spring Framework 7 的 API Versioning 支持。

    8.7 初始化数据

    — src/main/resources/data.sql
    insert into articles (id, title, content, status, created_at, updated_at)
    values
    (1, 'Spring Boot 4.x 慢接口定位入门', '本文讲解如何定位慢接口、慢 SQL 和线程池耗尽问题。', 'PUBLISHED', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
    (2, 'JPA 查询为什么会变慢', '模糊查询、排序、分页不合理、索引缺失都可能导致查询变慢。', 'PUBLISHED', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
    (3, '线程池耗尽排查实战', '线程池队列堆积会让请求变慢,严重时会导致接口超时。', 'PUBLISHED', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);

    代码解析

    因为 spring.jpa.defer-datasource-initialization=true,JPA 建表之后再执行 data.sql。 本文为了本地演示方便使用 H2。生产项目不要依赖 ddl-auto=create-drop 自动建表,应该使用 Flyway、Liquibase 或数据库发布脚本管理表结构。

    8.8 请求示例

    创建文章:

    curl -X POST http://localhost:8080/api/v1/articles \\
    -H "Content-Type: application/json" \\
    -d '{"title":"线上接口变慢如何定位","content":"慢接口排查要先看总耗时,再看慢 SQL 和线程池。"}'

    搜索文章:

    curl "http://localhost:8080/api/v1/articles/search?keyword=Spring"

    参数校验失败:

    curl -X POST http://localhost:8080/api/v1/articles \\
    -H "Content-Type: application/json" \\
    -d '{"title":"","content":""}'

    响应示例:

    {
    "code": 400,
    "message": "title: 标题不能为空; content: 内容不能为空",
    "data": null,
    "timestamp": "2026-05-20T10:00:00Z",
    "traceId": "96e028bba80d4a6c97429e9eb4e56f19"
    }

    8.9 慢 Service 方法 AOP

    // src/main/java/com/example/slowapi/infra/SlowMethodLogAspect.java
    package com.example.slowapi.infra;

    import com.example.slowapi.config.PerformanceProperties;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Component;

    /**
    * 统计 Service 层方法耗时。
    * 这样可以判断慢接口是否主要慢在业务逻辑层。
    */

    @Aspect
    @Component
    public class SlowMethodLogAspect {

    private static final Logger log = LoggerFactory.getLogger(SlowMethodLogAspect.class);

    private final PerformanceProperties properties;

    public SlowMethodLogAspect(PerformanceProperties properties) {
    this.properties = properties;
    }

    @Around("execution(* com.example.slowapi.service..*(..))")
    public Object logSlowMethod(ProceedingJoinPoint joinPoint) throws Throwable {
    long start = System.nanoTime();

    try {
    return joinPoint.proceed();
    } finally {
    long costMs = (System.nanoTime() start) / 1_000_000;
    MethodSignature signature = (MethodSignature) joinPoint.getSignature();
    String methodName = signature.getDeclaringType().getSimpleName() + "." + signature.getName();

    if (costMs >= properties.getSlowMethodMs()) {
    log.warn("慢方法 detected method={} costMs={}", methodName, costMs);
    } else {
    log.info("方法完成 method={} costMs={}", methodName, costMs);
    }
    }
    }
    }

    代码解析

    接口总耗时只能告诉你“慢了”。 AOP 方法耗时能告诉你“是不是 Service 慢”。

    如果接口总耗时 900ms,Service 只用了 20ms,那慢点可能在 Filter、参数解析、返回序列化、网络或容器层。 如果接口总耗时 900ms,Service 用了 880ms,那就继续往 Repository、SQL、外部接口、线程池方向查。

    spring-boot-starter-aspectj 是 Spring Boot 4.x 官方 Starter 列表中的 Starter。这里不要自己手动拼 spring-aop、aspectjweaver 版本。

    九、案例三:工程化增强实践——线程池耗尽、Actuator 与测试

    案例三进入线上排查主题的核心:线程池耗尽。

    9.1 线程池配置属性

    // src/main/java/com/example/slowapi/config/DiagnosticExecutorProperties.java
    package com.example.slowapi.config;

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

    @ConfigurationProperties(prefix = "app.executor.diagnostic")
    public class DiagnosticExecutorProperties {

    private int coreSize = 2;
    private int maxSize = 2;
    private int queueCapacity = 2;

    public int getCoreSize() {
    return coreSize;
    }

    public void setCoreSize(int coreSize) {
    this.coreSize = coreSize;
    }

    public int getMaxSize() {
    return maxSize;
    }

    public void setMaxSize(int maxSize) {
    this.maxSize = maxSize;
    }

    public int getQueueCapacity() {
    return queueCapacity;
    }

    public void setQueueCapacity(int queueCapacity) {
    this.queueCapacity = queueCapacity;
    }
    }

    9.2 线程池 Bean

    // src/main/java/com/example/slowapi/config/DiagnosticExecutorConfig.java
    package com.example.slowapi.config;

    import java.util.concurrent.ThreadPoolExecutor;

    import org.springframework.boot.context.properties.EnableConfigurationProperties;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

    @Configuration
    @EnableConfigurationProperties(DiagnosticExecutorProperties.class)
    public class DiagnosticExecutorConfig {

    @Bean("diagnosticExecutor")
    public ThreadPoolTaskExecutor diagnosticExecutor(DiagnosticExecutorProperties properties) {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

    // 核心线程数:长期保留的工作线程数量
    executor.setCorePoolSize(properties.getCoreSize());

    // 最大线程数:队列满后最多扩展到多少线程
    executor.setMaxPoolSize(properties.getMaxSize());

    // 队列容量:任务来不及执行时先进入队列
    executor.setQueueCapacity(properties.getQueueCapacity());

    // 线程名前缀:线上看 thread dump 时非常有用
    executor.setThreadNamePrefix("diagnostic-");

    // 队列和线程都满时,直接拒绝,让接口快速失败
    executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());

    executor.initialize();
    return executor;
    }
    }

    代码解析

    为了方便演示,这里把线程池配得很小:2 个线程、2 个队列容量。真实项目当然不能照搬这个值。

    关键不是数字,而是思想:

    • 线程池必须有名字。
    • 队列不能无限大。
    • 拒绝策略要符合业务预期。
    • 线程池指标必须可观测。
    • 慢接口排查时要看线程池活跃线程、队列长度、拒绝次数。

    我很不建议初学者在项目里到处 Executors.newFixedThreadPool()。这样创建出来的线程池不容易被 Spring 管理,也不容易被 Actuator/Micrometer 观测。

    9.3 线程池快照 VO

    // src/main/java/com/example/slowapi/vo/ThreadPoolSnapshot.java
    package com.example.slowapi.vo;

    public record ThreadPoolSnapshot(
    int corePoolSize,
    int maxPoolSize,
    int activeCount,
    int poolSize,
    int queueSize,
    int remainingQueueCapacity,
    long completedTaskCount
    ) {
    }

    9.4 DiagnosticService

    // src/main/java/com/example/slowapi/service/DiagnosticService.java
    package com.example.slowapi.service;

    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.CompletableFuture;
    import java.util.concurrent.RejectedExecutionException;

    import com.example.slowapi.vo.ThreadPoolSnapshot;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
    import org.springframework.stereotype.Service;

    @Service
    public class DiagnosticService {

    private final ThreadPoolTaskExecutor diagnosticExecutor;

    public DiagnosticService(@Qualifier("diagnosticExecutor") ThreadPoolTaskExecutor diagnosticExecutor) {
    this.diagnosticExecutor = diagnosticExecutor;
    }

    public ThreadPoolSnapshot snapshot() {
    var executor = diagnosticExecutor.getThreadPoolExecutor();

    return new ThreadPoolSnapshot(
    executor.getCorePoolSize(),
    executor.getMaximumPoolSize(),
    executor.getActiveCount(),
    executor.getPoolSize(),
    executor.getQueue().size(),
    executor.getQueue().remainingCapacity(),
    executor.getCompletedTaskCount()
    );
    }

    public String createPressure(int tasks, long sleepMs) {
    List<CompletableFuture<Void>> acceptedFutures = new ArrayList<>();
    int rejected = 0;

    for (int i = 0; i < tasks; i++) {
    try {
    CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
    try {
    // 模拟阻塞任务:真实项目可能是报表生成、远程调用、文件处理
    Thread.sleep(sleepMs);
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    }
    }, diagnosticExecutor);
    acceptedFutures.add(future);
    } catch (RejectedExecutionException ex) {
    rejected++;
    }
    }

    return "accepted=" + acceptedFutures.size() + ", rejected=" + rejected;
    }
    }

    代码解析

    这里通过 CompletableFuture.runAsync 向业务线程池提交任务。 当线程池和队列都满了,就会抛出 RejectedExecutionException。

    真实项目里,如果你不处理拒绝异常,用户可能看到 500;如果队列过大,用户可能一直等待。比较稳妥的做法是根据业务场景快速失败、限流、降级或返回 429。

    9.5 DiagnosticController

    // src/main/java/com/example/slowapi/controller/DiagnosticController.java
    package com.example.slowapi.controller;

    import com.example.slowapi.common.ApiResponse;
    import com.example.slowapi.service.DiagnosticService;
    import com.example.slowapi.vo.ThreadPoolSnapshot;
    import jakarta.validation.constraints.Max;
    import jakarta.validation.constraints.Min;
    import org.springframework.validation.annotation.Validated;
    import org.springframework.web.bind.annotation.*;

    @RestController
    @RequestMapping("/api/v1/diagnostics")
    @Validated
    class DiagnosticController {

    private final DiagnosticService diagnosticService;

    DiagnosticController(DiagnosticService diagnosticService) {
    this.diagnosticService = diagnosticService;
    }

    @GetMapping("/executor")
    ApiResponse<ThreadPoolSnapshot> executor() {
    return ApiResponse.ok(diagnosticService.snapshot());
    }

    @PostMapping("/pressure")
    ApiResponse<String> pressure(
    @RequestParam(defaultValue = "8")
    @Min(value = 1, message = "任务数至少为 1")
    @Max(value = 100, message = "任务数不能超过 100")
    int tasks,

    @RequestParam(defaultValue = "1000")
    @Min(value = 10, message = "阻塞时间至少为 10ms")
    @Max(value = 10000, message = "阻塞时间不能超过 10000ms")
    long sleepMs
    ) {
    return ApiResponse.ok(diagnosticService.createPressure(tasks, sleepMs));
    }
    }

    代码解析

    这两个接口用于模拟和观察线程池状态:

    查看线程池:

    curl http://localhost:8080/api/v1/diagnostics/executor

    制造压力:

    curl -X POST "http://localhost:8080/api/v1/diagnostics/pressure?tasks=10&sleepMs=3000"

    由于线程池只有 2 个线程、队列容量 2,所以 10 个任务里最多接受 4 个,其他会被拒绝。 这时你会直观看到线程池耗尽是怎么发生的。

    9.6 可观测性数据流转图

    相关示意图绘制如下,仅供参考:

    这张图是线上排查的理想状态。 初学阶段先做到日志 + Actuator;团队项目里再逐步接入指标系统和链路追踪系统。不要一开始就追求全套平台,但也不要上线后只靠控制台日志。

    9.7 慢 SQL DataSource 包装器

    为了不引入第三方 SQL 代理库,本文给出一个简化版 DataSource 包装器。它能统计 JDBC execute、executeQuery、executeUpdate、executeBatch 等方法耗时。

    // src/main/java/com/example/slowapi/infra/SlowSqlDataSourcePostProcessor.java
    package com.example.slowapi.infra;

    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.InvocationTargetException;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    import java.sql.Connection;
    import java.sql.Statement;
    import java.util.Set;

    import javax.sql.DataSource;

    import com.example.slowapi.config.PerformanceProperties;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.config.BeanPostProcessor;
    import org.springframework.stereotype.Component;

    /**
    * 包装 DataSource,统计 JDBC SQL 执行耗时。
    * 这是教学版实现,真实项目可以换成成熟的 JDBC 观测方案或数据库慢查询日志。
    */

    @Component
    public class SlowSqlDataSourcePostProcessor implements BeanPostProcessor {

    private static final Logger log = LoggerFactory.getLogger(SlowSqlDataSourcePostProcessor.class);

    private final PerformanceProperties properties;

    public SlowSqlDataSourcePostProcessor(PerformanceProperties properties) {
    this.properties = properties;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
    if (!(bean instanceof DataSource dataSource)) {
    return bean;
    }

    if (Proxy.isProxyClass(bean.getClass())) {
    return bean;
    }

    log.info("包装 DataSource 用于慢 SQL 统计 beanName={}", beanName);

    return Proxy.newProxyInstance(
    dataSource.getClass().getClassLoader(),
    new Class<?>[]{DataSource.class},
    new DataSourceInvocationHandler(dataSource, properties.getSlowSqlMs())
    );
    }

    private static class DataSourceInvocationHandler implements InvocationHandler {

    private final DataSource target;
    private final long slowSqlMs;

    private DataSourceInvocationHandler(DataSource target, long slowSqlMs) {
    this.target = target;
    this.slowSqlMs = slowSqlMs;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    Object result = invokeTarget(target, method, args);
    if (result instanceof Connection connection) {
    return wrapConnection(connection, slowSqlMs);
    }
    return result;
    }
    }

    private static Connection wrapConnection(Connection connection, long slowSqlMs) {
    return (Connection) Proxy.newProxyInstance(
    connection.getClass().getClassLoader(),
    new Class<?>[]{Connection.class},
    new ConnectionInvocationHandler(connection, slowSqlMs)
    );
    }

    private static class ConnectionInvocationHandler implements InvocationHandler {

    private final Connection target;
    private final long slowSqlMs;

    private ConnectionInvocationHandler(Connection target, long slowSqlMs) {
    this.target = target;
    this.slowSqlMs = slowSqlMs;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    Object result = invokeTarget(target, method, args);
    String methodName = method.getName();

    if (result instanceof Statement statement) {
    String sql = extractSqlFromConnectionMethod(methodName, args);
    return wrapStatement(statement, sql, slowSqlMs);
    }

    return result;
    }

    private String extractSqlFromConnectionMethod(String methodName, Object[] args) {
    if (("prepareStatement".equals(methodName)
    || "prepareCall".equals(methodName))
    && args != null
    && args.length > 0
    && args[0] instanceof String sql) {
    return sql;
    }
    return "<dynamic-sql>";
    }
    }

    private static Statement wrapStatement(Statement statement, String preparedSql, long slowSqlMs) {
    return (Statement) Proxy.newProxyInstance(
    statement.getClass().getClassLoader(),
    statement.getClass().getInterfaces(),
    new StatementInvocationHandler(statement, preparedSql, slowSqlMs)
    );
    }

    private static class StatementInvocationHandler implements InvocationHandler {

    private static final Set<String> EXECUTE_METHODS = Set.of(
    "execute",
    "executeQuery",
    "executeUpdate",
    "executeLargeUpdate",
    "executeBatch",
    "executeLargeBatch"
    );

    private final Statement target;
    private final String preparedSql;
    private final long slowSqlMs;

    private StatementInvocationHandler(Statement target, String preparedSql, long slowSqlMs) {
    this.target = target;
    this.preparedSql = preparedSql;
    this.slowSqlMs = slowSqlMs;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if (!EXECUTE_METHODS.contains(method.getName())) {
    return invokeTarget(target, method, args);
    }

    long start = System.nanoTime();
    try {
    return invokeTarget(target, method, args);
    } finally {
    long costMs = (System.nanoTime() start) / 1_000_000;
    String sql = resolveSql(args);

    if (costMs >= slowSqlMs) {
    log.warn("慢 SQL detected costMs={} sql={}", costMs, normalize(sql));
    } else {
    log.debug("SQL 完成 costMs={} sql={}", costMs, normalize(sql));
    }
    }
    }

    private String resolveSql(Object[] args) {
    if (args != null && args.length > 0 && args[0] instanceof String sql) {
    return sql;
    }
    return preparedSql;
    }

    private String normalize(String sql) {
    if (sql == null) {
    return "";
    }
    return sql.replaceAll("\\\\s+", " ").trim();
    }
    }

    private static Object invokeTarget(Object target, Method method, Object[] args) throws Throwable {
    try {
    return method.invoke(target, args);
    } catch (InvocationTargetException ex) {
    throw ex.getTargetException();
    }
    }
    }

    代码解析

    这段代码比较长,但思路不复杂:

  • Spring 创建 DataSource Bean 后,我们用 BeanPostProcessor 包装它。
  • 当业务代码获取 Connection 时,返回一个 Connection 代理。
  • 当 Connection 创建 Statement / PreparedStatement 时,继续包装 Statement。
  • 当执行 SQL 方法时,统计耗时。
  • 超过阈值就输出慢 SQL 日志。
  • 这不是要替代数据库慢查询日志。真实线上项目中,我更推荐同时启用:

    • 数据库原生慢查询日志。
    • SQL 执行计划分析。
    • 应用侧 SQL 耗时日志。
    • 连接池指标。
    • Repository / Service 方法耗时日志。

    为什么还要应用侧慢 SQL 日志?因为数据库慢查询只能告诉你 SQL 慢,应用日志能通过 traceId 把慢 SQL 和某次 HTTP 请求关联起来。

    9.8 RestTestClient 集成测试

    Spring Framework 7 提供了 RestTestClient,它包装 RestClient,用于测试服务端应用,既可以做端到端测试,也可以绑定到 Spring MVC 应用上下文。官方文档说明它可以用于测试服务器应用,并能通过 MockMvc 测试 Spring MVC 应用。

    // src/test/java/com/example/slowapi/ArticleControllerRestTest.java
    package com.example.slowapi;

    import com.example.slowapi.dto.CreateArticleRequest;
    import org.junit.jupiter.api.BeforeEach;
    import org.junit.jupiter.api.Test;

    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.boot.test.web.server.LocalServerPort;
    import org.springframework.http.MediaType;
    import org.springframework.test.web.servlet.client.RestTestClient;

    @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
    class ArticleControllerRestTest {

    @LocalServerPort
    int port;

    RestTestClient client;

    @BeforeEach
    void setUp() {
    client = RestTestClient.bindToServer()
    .baseUrl("http://localhost:" + port)
    .build();
    }

    @Test
    void createArticleShouldReturnSuccess() {
    CreateArticleRequest request = new CreateArticleRequest(
    "Spring Boot 4.x 测试文章",
    "这是一篇用于测试的文章内容。"
    );

    client.post()
    .uri("/api/v1/articles")
    .contentType(MediaType.APPLICATION_JSON)
    .body(request)
    .exchange()
    .expectStatus().isOk()
    .expectBody()
    .jsonPath("$.code").isEqualTo(0)
    .jsonPath("$.data.title").isEqualTo("Spring Boot 4.x 测试文章");
    }

    @Test
    void createArticleShouldValidateRequest() {
    CreateArticleRequest request = new CreateArticleRequest("", "");

    client.post()
    .uri("/api/v1/articles")
    .contentType(MediaType.APPLICATION_JSON)
    .body(request)
    .exchange()
    .expectStatus().isBadRequest()
    .expectBody()
    .jsonPath("$.code").isEqualTo(400);
    }
    }

    代码解析

    这里的 @LocalServerPort 导入路径是:

    import org.springframework.boot.test.web.server.LocalServerPort;

    不要复制很老的 org.springframework.boot.web.server.LocalServerPort。 测试客户端使用 org.springframework.test.web.servlet.client.RestTestClient,这是 Spring Framework 7 测试体系里非常值得关注的新工具。

    为什么不用 RestTemplate 做测试? 因为新项目里我们应该逐步把测试客户端也迁移到更现代、更统一的 API 上。RestTestClient 的断言风格对 REST API 测试更友好。

    十、核心源码与配置解析

    10.1 一次慢接口日志应该怎么看

    假设你请求:

    curl "http://localhost:8080/api/v1/ping/slow?delayMs=900"

    你可能看到:

    WARN [traceId] SlowRequestInterceptor – 慢接口 detected method=GET uri=/api/v1/ping/slow status=200 costMs=905

    这说明接口总耗时超过阈值。下一步要看:

    • 是否有 SlowMethodLogAspect 的慢方法日志。
    • 是否有 SlowSqlDataSourcePostProcessor 的慢 SQL 日志。
    • 是否有线程池拒绝异常。
    • Actuator /actuator/metrics 是否显示线程池队列堆积。
    • /actuator/threaddump 是否有大量线程阻塞在同一位置。

    10.2 慢接口定位流程图

    相关示意图绘制如下,仅供参考:

    这张图是排查慢接口的实战路线。 我建议初学者不要一上来就背很多 JVM 参数,先把这条路径跑熟:入口耗时 → 方法耗时 → SQL 耗时 → 线程池状态 → JVM/系统资源。

    10.3 慢 SQL 的真实优化思路

    应用侧发现慢 SQL 后,不要只停留在日志层面。真正的优化要回到数据库。

    常见策略:

    • 给过滤字段加合适索引。
    • 避免 like '%keyword%' 直接扫大表。
    • 大表查询必须分页。
    • 避免一次返回过多字段。
    • 避免循环查库造成 N+1。
    • 复杂统计考虑预聚合。
    • 排序字段要结合索引设计。
    • 事务尽量短,不要在事务里调用慢外部接口。

    本文的 findByTitleContainingIgnoreCaseOrderByCreatedAtDesc 在小数据量下没问题,但如果文章表有千万级数据,这就是高风险查询。真实项目中可能要改成全文索引、搜索引擎或更明确的前缀搜索。

    10.4 线程池耗尽的真实优化思路

    线程池耗尽不要只想着“调大线程数”。调大线程数可能让问题更严重,因为更多线程会抢 CPU、抢数据库连接、抢下游接口。

    更合理的排查顺序是:

  • 任务为什么慢?
  • 是否应该同步等待?
  • 是否缺少超时?
  • 是否可以限流?
  • 队列容量是否过大?
  • 拒绝策略是否合理?
  • 是否需要拆分不同业务线程池?
  • 是否有下游依赖拖慢任务?
  • 是否有线程池指标和告警?
  • 我一般会把不同类型任务拆开:

    • 短 CPU 任务一个池。
    • 慢 I/O 任务一个池。
    • 报表任务一个池。
    • 消息消费一个池。
    • 定时任务一个池。

    不要所有异步任务共用一个默认线程池。

    十一、常见问题与踩坑总结

    1. 为什么 Spring Boot 4.x 至少要求 Java 17?

    因为 Spring Boot 4.x 建立在新一代 Spring Framework 7.x 和现代 Java 基线之上。官方文档显示 Spring Boot 4.0.6 至少需要 Java 17。继续使用 Java 8 或 Java 11 创建 Boot 4 项目,本身就是错误起点。

    2. Spring Boot 4.x 和 Spring Framework 7.x 是什么关系?

    Spring Framework 提供底层能力,Spring Boot 提供自动配置、Starter、运行时整合和工程化体验。Boot 4.x 基于 Framework 7.x。你写 Controller、Service、Repository 时,看起来是在写 Boot 项目,底层请求处理、Bean 管理、事务、MVC 都来自 Spring Framework。

    3. 为什么 Spring Boot 4.x 中不建议继续把 spring-boot-starter-web 作为主推荐依赖?

    因为 Spring Boot 4.x 更强调模块化 Starter。官方 Starter 列表已经提供 spring-boot-starter-webmvc,并说明旧 spring-boot-starter-web 偏向迁移到 webmvc。新项目直接选 webmvc 更清晰。

    4. spring-boot-starter-webmvc 和 spring-boot-starter-web 有什么区别?

    spring-boot-starter-webmvc 明确表达你要构建 Spring MVC + Tomcat 应用。 spring-boot-starter-web 是旧时代常用入口,在 Boot 4.x 中不应再作为新教程主推荐。依赖选得越明确,自动配置越可控。

    5. 为什么 javax.validation 在 Spring Boot 4.x 中不能作为主写法?

    因为 Jakarta EE 命名空间已经迁移到 jakarta.*。Spring Boot 4.x 主代码应该使用 jakarta.validation.*。复制旧教程的 javax.validation.* 很容易编译失败或依赖混乱。

    6. jakarta.validation、jakarta.persistence、jakarta.annotation 分别对应什么场景?

    jakarta.validation 用于参数校验,比如 @NotBlank、@Valid。 jakarta.persistence 用于 JPA 实体映射,比如 @Entity、@Id。 jakarta.annotation 用于通用生命周期注解,比如 @PostConstruct。

    7. Controller 中是否应该直接返回 Entity?

    不建议。Entity 是数据库模型,不是 API 契约。直接返回 Entity 会带来字段泄漏、懒加载、循环引用、接口版本难演进等问题。建议用 VO 返回给前端。

    8. DTO、VO、Entity 有什么区别?

    DTO 接收入参,关注校验。 VO 返回出参,关注前端展示。 Entity 映射数据库,关注持久化。 三者分离是工程可维护性的基础。

    9. 全局异常处理为什么没有生效?

    常见原因有: 类没有被组件扫描到;没有加 @RestControllerAdvice;异常类型不匹配;异常在 Filter 层提前被吞掉;Controller 返回的不是 REST 响应;测试没有加载完整 Spring 上下文。

    10. application.yml 配置为什么没有读取到?

    常见原因是缩进错误、属性前缀写错、没有启用 @EnableConfigurationProperties、配置类没有 setter、环境 profile 不对。YAML 对缩进很敏感,初学者一定要注意。

    11. 参数校验注解为什么没有触发?

    请求体校验要在参数上加 @Valid。 方法参数校验通常要在 Controller 类上加 @Validated。 同时必须引入 spring-boot-starter-validation。不要只写注解不加依赖。

    12. 为什么加了 @Valid 还是没有进入校验?

    可能是你没有使用 @RequestBody,或者 DTO 字段没有校验注解,或者导入了错误的 javax.* 包,或者测试请求没有设置 Content-Type: application/json。

    13. Service 接口和实现类是否必须拆分?

    不是必须。小项目可以直接写 Service 类。但教学和多人协作项目中,接口 + 实现有助于表达边界。不要为了形式而形式,关键是保持职责清晰。

    14. Spring Boot 4.x 项目如何选择 Maven 依赖版本?

    优先使用 Spring Boot Parent 或 BOM 管理版本。Spring Boot 官方文档也建议不要手动指定 Spring Framework 版本,因为每个 Boot 版本都对应一个基础 Spring Framework 版本。

    15. 为什么测试类里的旧导入路径会报错?

    因为部分测试工具包路径在不同版本中变化过。比如 @LocalServerPort 应使用 org.springframework.boot.test.web.server.LocalServerPort。不要直接复制多年前教程里的导入路径。

    16. MockMvc、RestTestClient、WebTestClient 应该怎么选?

    Spring MVC 项目中,MockMvc 仍然能用。Spring Framework 7 的 RestTestClient 提供了更统一的 REST 测试体验,既能绑定 MVC 应用上下文,也能连接真实服务器。WebTestClient 更常用于 WebFlux 或响应式场景。

    17. Jackson 3 对老项目有什么影响?

    老项目如果有 Jackson 2 的自定义序列化器、反序列化器、模块注册、ObjectMapper 配置,迁移到 Boot 4.x 时需要重新验证。Spring Boot 4.x 默认偏向 Jackson 3。

    18. RestTemplate 还能不能用?新项目为什么更推荐 RestClient?

    RestTemplate 还存在,但新项目更推荐 RestClient、HTTP Service Client 或 WebClient。RestClient 是同步 fluent API,使用方式更现代,也更适合统一配置超时、拦截器和链路追踪。

    19. 初学者如何判断自己的项目结构是否合理?

    看 Controller 是否只做请求接收和响应返回;Service 是否承载业务规则;Repository 是否只负责数据访问;DTO / VO / Entity 是否分离;异常是否统一;日志是否能定位一次请求。能做到这些,结构基本就不差。

    20. 从 Spring Boot 3.x 升级到 4.x 时最应该先检查什么?

    先检查 JDK 版本、Starter 依赖、javax.* 到 jakarta.*、Jackson 2 到 Jackson 3、自定义自动配置、测试依赖、第三方 Starter 兼容性。不要只改 <version>。

    十二、项目开发最佳实践

    12.1 包结构设计

    我推荐按业务和分层结合组织:

    com.example.slowapi
    ├── common
    ├── config
    ├── controller
    ├── dto
    ├── entity
    ├── exception
    ├── infra
    ├── repository
    ├── service
    │ └── impl
    └── vo

    初学者不要把所有类都堆在一个包里。项目小的时候看不出来,项目稍微大一点就会乱。

    12.2 Controller 设计规范

    Controller 只做四件事:

  • 接收请求。
  • 触发校验。
  • 调用 Service。
  • 返回 VO。
  • 不要在 Controller 里写 SQL,不要写复杂业务规则,不要直接操作线程池。

    12.3 DTO / VO / Entity 使用边界

    DTO 面向输入,VO 面向输出,Entity 面向数据库。 不要把 Entity 直接暴露给前端,也不要把前端请求 DTO 直接当数据库实体保存。

    12.4 统一响应格式

    统一响应不是为了形式,而是为了前后端协作、异常处理、traceId 排查、接口文档生成都更稳定。

    12.5 全局异常处理

    业务异常、参数校验异常、线程池拒绝异常、系统异常要分开处理。 不要所有异常都返回 500。 线程池耗尽这类问题更适合返回 429 或业务定义的“系统繁忙”。

    12.6 参数校验

    参数校验应尽量前置。 必填、长度、格式、范围,能用 Jakarta Validation 解决的就不要写一堆 if。 但复杂业务规则不要硬塞到 DTO 注解里,应放在 Service。

    12.7 日志记录

    日志必须包含 traceId。 慢接口日志要有 method、uri、status、costMs。 慢 SQL 日志要有 costMs、SQL 摘要。 线程池日志要有 active、queue、completed。

    12.8 配置管理

    所有阈值都不要硬编码。 慢请求阈值、慢 SQL 阈值、线程池大小、队列容量,都应该放到配置文件。 不同环境使用不同 profile。

    12.9 Maven 依赖管理

    使用 Spring Boot Parent 或 BOM。 不要手动给 Spring Framework、Jackson、Hibernate、JUnit 指定版本,除非你非常清楚自己在做什么。

    12.10 Spring Boot 4.x Starter 选择

    Web MVC 选 spring-boot-starter-webmvc。 Validation 选 spring-boot-starter-validation。 JPA 选 spring-boot-starter-data-jpa。 测试选对应模块的 test starter。 HTTP 同步客户端可考虑 spring-boot-starter-restclient。 WebFlux 才选 spring-boot-starter-webflux。

    12.11 单元测试

    Service 纯业务逻辑可以写单元测试。 不要所有测试都启动 Spring 容器。 业务规则越复杂,越应该写单元测试。

    12.12 集成测试

    Controller + Validation + ExceptionHandler + JSON 序列化,适合用 RestTestClient 做集成测试。 它能验证真实 HTTP 行为,不只是调用 Java 方法。

    12.13 接口文档

    本文没有引入 springdoc-openapi 或 Knife4j,是因为第三方组件必须确认 Spring Boot 4.x 兼容版本。真实项目可以接入,但不要随便复制 Boot 3.x 的版本号。

    12.14 API Versioning

    即使初学阶段先用 /api/v1,也要有接口演进意识。 不要随便改已有字段含义。 不要删除线上客户端仍在使用的字段。 重大变更应该通过版本管理处理。

    12.15 可观测性

    上线后不能只看日志。 至少要有健康检查、指标、慢接口日志、慢 SQL 日志、线程池指标。 团队项目建议接入 Prometheus、Grafana 和 OpenTelemetry。

    12.16 初学者学习路线建议

    先把 Web MVC、Validation、JPA、异常处理、测试跑通。 再学 Actuator、日志、线程池、数据库优化。 最后再深入 Spring Security、缓存、消息队列、微服务、链路追踪。

    不要一开始就追求“大而全”,但每一步都要写成真实项目的样子。

    十三、扩展知识点:API Versioning、RestClient、Jackson 3、JSpecify、OpenTelemetry

    13.1 Spring Framework 7 的 API Versioning

    Spring Framework 7 的 Spring MVC 支持 API Versioning。官方文档说明,可以通过 WebMvcConfigurer 的 configureApiVersioning 启用,并支持从请求头、请求参数、路径片段、媒体类型参数中解析版本。

    示例:

    // src/main/java/com/example/slowapi/config/ApiVersionConfig.java
    package com.example.slowapi.config;

    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.ApiVersionConfigurer;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

    /**
    * API 版本配置示例。
    * 初学阶段可以先不用打开,理解设计思路即可。
    */

    @Configuration
    public class ApiVersionConfig implements WebMvcConfigurer {

    @Override
    public void configureApiVersioning(ApiVersionConfigurer configurer) {
    configurer
    .useRequestHeader("API-Version")
    .setDefaultVersion("1.0")
    .addSupportedVersions("1.0", "2.0");
    }
    }

    代码解析:

    这段配置表示从 API-Version 请求头读取版本。 如果客户端不传版本,默认使用 1.0。 真实项目里,你可以选择请求头版本,也可以继续使用 /api/v1 路径版本。

    初学阶段不必把版本管理做复杂,但要知道接口不是写完就永远不变的。线上客户端、移动端、小程序、第三方系统可能同时依赖旧接口,所以版本演进必须提前考虑。

    13.2 RestClient 与 HTTP Service Client

    新项目中,如果要调用外部 HTTP 服务,可以优先考虑 RestClient。

    // src/main/java/com/example/slowapi/config/HttpClientConfig.java
    package com.example.slowapi.config;

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.client.RestClient;

    /**
    * RestClient 示例。
    * 真实项目应配置超时、认证、重试、日志脱敏等。
    */

    @Configuration
    public class HttpClientConfig {

    @Bean
    RestClient githubRestClient(RestClient.Builder builder) {
    return builder
    .baseUrl("https://api.github.com")
    .defaultHeader("Accept", "application/json")
    .build();
    }
    }

    代码解析:

    RestClient.Builder 可以由 Spring 管理,后续接入观测、拦截器、统一超时更方便。 不要在业务代码里到处 RestClient.create(),更不要随便 new HTTP 客户端。

    13.3 Jackson 3 与接口响应

    Spring Boot 4.x 默认进入 Jackson 3 体系后,至少要关注:

    • record 序列化。
    • Instant / LocalDateTime 格式。
    • 枚举输出策略。
    • 空字段是否返回。
    • 老项目自定义序列化器迁移。

    本文用 Instant 是为了表达绝对时间。真实项目中如果面向国内前端,也可以统一输出带时区的字符串,关键是全项目保持一致。

    13.4 JSpecify Null-Safety

    Spring Framework 7 使用 JSpecify 注解声明自身 API 的 nullability。官方文档说明,这一安排的目标是通过构建期检查和明确的 null 契约,减少运行时 NPE;同时旧的 org.springframework.lang 空安全注解在 Framework 7 中已转向 JSpecify。

    对初学者来说,不需要一开始就把 NullAway 配到极致,但要养成习惯:

    • 方法返回值能为空就明确表达。
    • 不要随便返回 null。
    • DTO 字段用校验约束。
    • Repository 查询为空时用 Optional 或清晰异常。
    • 不要把 NPE 当成“小问题”。

    13.5 Actuator、Micrometer 与 OpenTelemetry

    Actuator 是线上诊断的入口。 Micrometer 是指标门面。 OpenTelemetry 是链路追踪、指标、日志语义整合的重要标准。

    Spring Boot Actuator 的 Observability 文档说明,可观测性由 logging、metrics、traces 三大支柱组成;Spring Boot 使用 Micrometer Observation 支撑 metrics 和 traces。

    对本文主题来说:

    • 慢接口:看 HTTP 请求耗时指标。
    • 慢 SQL:看数据库慢查询和应用 SQL 耗时。
    • 线程池耗尽:看线程池 active、queue、completed。
    • 跨服务慢:看 trace,定位哪一个服务段慢。

    初学阶段你先掌握 /actuator/health、/actuator/metrics、/actuator/threaddump。 等项目上线,再接 Prometheus、Grafana、Tempo、Jaeger 或其他链路追踪平台。

    十四、完整代码结构回顾

    最终项目结构如下:

    slow-api-lab
    ├── pom.xml
    ├── src
    │ ├── main
    │ │ ├── java
    │ │ │ └── com/example/slowapi
    │ │ │ ├── SlowApiLabApplication.java
    │ │ │ ├── common
    │ │ │ │ └── ApiResponse.java
    │ │ │ ├── config
    │ │ │ │ ├── ApiVersionConfig.java
    │ │ │ │ ├── DiagnosticExecutorConfig.java
    │ │ │ │ ├── DiagnosticExecutorProperties.java
    │ │ │ │ ├── HttpClientConfig.java
    │ │ │ │ ├── PerformanceProperties.java
    │ │ │ │ └── WebMvcConfig.java
    │ │ │ ├── controller
    │ │ │ │ ├── ArticleController.java
    │ │ │ │ ├── DiagnosticController.java
    │ │ │ │ └── PingController.java
    │ │ │ ├── dto
    │ │ │ │ └── CreateArticleRequest.java
    │ │ │ ├── entity
    │ │ │ │ └── Article.java
    │ │ │ ├── exception
    │ │ │ │ ├── BusinessException.java
    │ │ │ │ └── GlobalExceptionHandler.java
    │ │ │ ├── infra
    │ │ │ │ ├── SlowMethodLogAspect.java
    │ │ │ │ ├── SlowRequestInterceptor.java
    │ │ │ │ ├── SlowSqlDataSourcePostProcessor.java
    │ │ │ │ └── TraceIdFilter.java
    │ │ │ ├── repository
    │ │ │ │ └── ArticleRepository.java
    │ │ │ ├── service
    │ │ │ │ ├── ArticleService.java
    │ │ │ │ ├── DiagnosticService.java
    │ │ │ │ └── impl
    │ │ │ │ └── ArticleServiceImpl.java
    │ │ │ └── vo
    │ │ │ ├── ArticleView.java
    │ │ │ └── ThreadPoolSnapshot.java
    │ │ └── resources
    │ │ ├── application.yml
    │ │ └── data.sql
    │ └── test
    │ └── java
    │ └── com/example/slowapi
    │ └── ArticleControllerRestTest.java

    你可以按下面顺序验证:

    mvn test
    mvn spring-boot:run
    curl http://localhost:8080/api/v1/ping
    curl "http://localhost:8080/api/v1/ping/slow?delayMs=900"
    curl "http://localhost:8080/api/v1/articles/search?keyword=Spring"
    curl http://localhost:8080/api/v1/diagnostics/executor
    curl -X POST "http://localhost:8080/api/v1/diagnostics/pressure?tasks=10&sleepMs=3000"
    curl http://localhost:8080/actuator/health
    curl http://localhost:8080/actuator/metrics
    curl http://localhost:8080/actuator/threaddump

    十五、总结

    这一节我们围绕“线上接口变慢如何定位”做了一个完整的 Spring Boot 4.x 实战案例。

    你学到的不只是几个接口,而是一套排查思路:

    第一,接口慢要先看总耗时。 通过 TraceIdFilter 和 SlowRequestInterceptor,我们能知道哪次请求慢、哪个 URI 慢、耗时多少。

    第二,接口慢不等于 SQL 慢。 通过 SlowMethodLogAspect,我们能进一步判断慢点是不是发生在 Service 层。

    第三,慢 SQL 要应用侧和数据库侧一起看。 本文给了一个教学版 DataSource 包装器,用于把 SQL 耗时和 traceId 联系起来;真实项目中还要结合数据库慢查询日志、执行计划和索引优化。

    第四,线程池耗尽是线上慢接口的高频原因。 我们配置了一个可观测的 ThreadPoolTaskExecutor,模拟了队列堆积和任务拒绝。你应该记住:线程池不是越大越好,队列也不是越大越安全。

    第五,Spring Boot 4.x 的写法要跟上版本体系。 Web MVC 使用 spring-boot-starter-webmvc,测试使用 spring-boot-starter-webmvc-test,校验和 JPA 使用 jakarta.*,JSON 默认考虑 Jackson 3,HTTP 客户端优先 RestClient / HTTP Service Client / WebClient。

    我建议你一定要把本文代码本地跑一遍。只看文章会觉得“我懂了”,但真正动手时,你会遇到依赖、包名、配置缩进、测试导入、日志阈值这些细节。把这些细节踩一遍,才算真的掌握。

    下一节可以继续学习:

    Spring Boot 4.x 中如何做接口限流、超时控制与降级保护

    这会和本节自然衔接:当你已经能定位慢接口,下一步就是让系统在压力下不被慢接口拖垮。加油,你已经从“会写接口”往“会维护线上系统”迈出关键一步了。

    ok,同学们,本节课就上到这儿,下课~

    🧧 学习福利 · 限时开放 🧧

    当然,无论你是计算机专业在读学生,还是对编程充满兴趣的入门者,都强烈建议系统学习SpringBoot全体系专栏:👉 「滚雪球学 Spring Boot」;涵盖SpringBoot所有教学内容。

    该专栏以“循序渐进 + 实战驱动”为核心理念,从基础到进阶到就业到架构师逐层展开,帮助你快速建立完整的 Spring Boot 技术体系,带你玩转SpringBoot框架。

    📌 学习承诺: 通过该专栏,你将能够:

    • 快速掌握 Spring Boot 核心开发能力
    • 构建完整的后端项目认知体系
    • 实现从“入门”到“独立开发”的跃迁

    就像“滚雪球”一样,知识不断积累、能力持续放大,实现指数级成长 🚀

    最后,如果这篇文章对你有所帮助,帮忙给作者来个一键三连,关注、点赞、收藏,您的支持就是我坚持写作最大的动力。

    同时欢迎大家关注技术号:「猿圈奇妙屋」 ,以便学习更多同类型的技术文章,免费白嫖最新BAT互联网公司面试题、4000G PDF编程电子书、简历模板、技术文章Markdown文档等海量资料。

    ps:本文涉及所有源代码,均已上传至Gitee开源,供同学们直接对照学习 Gitee传送门,同时,原创开源不易,欢迎给个star🌟,想体验下被🌟的感jio,非常感谢❗

    🫵 Who am I?

    我是 bug菌,一名深耕 Java 后端领域数十年的一线研发老兵,曾担任独角兽企业后端技术经理、研发架构师等职位,长期专注于 Java 后端、分布式架构、微服务治理、高并发系统、工程效能与研发管理等方向。

    目前活跃于多个主流技术社区,包括:

    CSDN|稀土掘金|InfoQ|51CTO|华为云开发者社区|阿里云开发者社区|腾讯云开发者社区|开源中国|博客园|墨天轮 等平台。

    曾获得:

    • CSDN 博客之星 Top30
    • 华为云多年度十佳博主 & 卓越贡献奖
    • 掘金多年度人气作者 Top40
    • CSDN、掘金、InfoQ、51CTO 等平台签约作者 / 优质作者

    截至目前,全网技术内容累计影响读者众多,全网粉丝已超过 30w+。

    如果你也关注 Java 后端、架构设计、技术成长、职场进阶与研发管理,欢迎关注我的技术内容合集入口:👉 点击查看 👈️

    硬核技术号 「猿圈奇妙屋」 期待你的加入。

    这里不仅分享技术干货,也记录一线研发人的成长、踩坑、思考与进阶路径。

    愿我们一起打怪升级,在技术路上持续进阶。

    – End –

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 【SpringBoot 4.x 第209节】线上接口变慢如何定位:慢 SQL、慢接口与线程池耗尽排查实战!
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!