当前位置: 首页 > article >正文

告别散乱的 @ExceptionHandler:实现统一、可维护的 Spring Boot 错误处理

Spring Boot 的异常处理机制一直都烂得可以。即便到了 2025 年,有了这么多进步和新版本,开发者们发现自己还是在跟 @ControllerAdvice、分散各处的 @ExceptionHandler 方法以及五花八门的响应结构较劲。这真的是一团糟

无论你是在构建 REST API、微服务,还是大型的企业级后端,Spring Boot 默认的异常策略都显得啰嗦、难以维护,而且早就过时了。大多数团队都是在异常映射这块儿打补丁、凑合着用,最终往往导致各个服务之间逻辑重复、错误响应的格式也无法预测。

在这篇文章里,我将剖析为什么传统的方法会失败 —— 并介绍一种现代化的、集中的全局异常策略,它不仅能清理你的代码库,还能为你的整个错误处理系统带来清晰的思路、结构化的组织和更好的可测试性

💥 为什么传统的 Spring Boot 异常处理不行

让我们来细数一下 Spring Boot 默认异常策略的问题:

  • • 逻辑分散 (Scattered logic): 每种异常类型都需要在应用的不同地方手动进行映射处理。

  • • 响应不一致 (Inconsistent responses): 不同微服务、不同团队返回的错误格式各不相同。

  • • 啰嗦且冗余 (Verbose and redundant): 不同的异常处理器之间存在大量重复的样板代码。

  • • 测试困难 (Difficult to test): 需要为每个 Controller 或 Handler 手动进行 Mock,非常麻烦。

  • • 紧耦合 (Tight coupling): 错误响应的格式化逻辑和异常解析处理逻辑混杂在一起。

✅ 现代化异常处理策略的目标

为了解决这些问题,我们希望新的异常处理机制能够:

  1. 1. 集中处理所有错误响应的逻辑。

  2. 2. 确保一致的错误响应结构

  3. 3. 允许异常响应元数据(如 HTTP 状态码、自定义错误码)之间轻松映射

  4. 4. 能够轻松地进行单元测试,无需依赖 Controller 层。

  5. 5. 支持通过自定义异常和日志记录进行扩展

🧱 现代化策略的核心概念

我们将结合使用以下几个元素:

  1. 1. 一个自定义的基础异常类 (ApplicationException)。

  2. 2. 一个(隐式的)集中的异常映射机制,通过基础异常类将异常与其元数据关联起来。

  3. 3. 一个全局异常处理器 (@RestControllerAdvice),动态地格式化并返回响应。

  4. 4. 一个统一的错误响应 DTO (ErrorResponse)。

  5. 5. (可选)一个错误码枚举或注册表,用于标准化错误码。

1. 定义标准的错误响应 DTO 📦
首先,创建一个可复用的 DTO 来封装错误响应信息:

import java.time.Instant;// lombok 注解可以简化 getter/setter
// import lombok.Getter;
// import lombok.Setter;// @Getter
// @Setter
public class ErrorResponse {private String message;    // 错误信息private String errorCode;  // 自定义错误码private int status;        // HTTP 状态码private String timestamp;  // 时间戳public ErrorResponse(String message, String errorCode, int status) {this.message = message;this.errorCode = errorCode;this.status = status;this.timestamp = Instant.now().toString(); // 使用 ISO-8601 格式的时间戳}// Getters 和 Setters 为简洁起见省略// ...
}

2. 创建一个基础应用异常类 ApplicationException 🚨
我们项目中所有自定义的业务异常都应该继承这个基类。

import org.springframework.http.HttpStatus;public abstract class ApplicationException extends RuntimeException { // 继承 RuntimeException// 强制子类提供错误码public abstract String getErrorCode();// 强制子类提供对应的 HTTP 状态码public abstract HttpStatus getHttpStatus();public ApplicationException(String message) {super(message);}// 可以根据需要添加其他构造函数或方法
}

3. 使用基类定义具体的自定义异常 🎯
例如,定义一个“资源未找到”的异常:

import org.springframework.http.HttpStatus;public class ResourceNotFoundException extends ApplicationException {public ResourceNotFoundException(String message) {super(message);}@Overridepublic String getErrorCode() {// 返回预定义的错误码return "ERR_RESOURCE_NOT_FOUND";}@Overridepublic HttpStatus getHttpStatus() {// 返回对应的 HTTP 状态码return HttpStatus.NOT_FOUND; // 404}
}

这种结构使得每个异常都能够**“自我描述”**它应该如何被处理(错误码是什么,状态码是什么)。

4. 使用一个“全捕获”的集中式异常处理器 🧠
这是我们新策略的核心所在。使用 @RestControllerAdvice 来创建一个全局处理器。

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;@RestControllerAdvice // 全局处理 @RestController 抛出的异常
public class GlobalExceptionHandler {// 处理所有继承了 ApplicationException 的自定义异常@ExceptionHandler(ApplicationException.class)public ResponseEntity<ErrorResponse> handleApplicationException(ApplicationException ex) {// 从异常对象中获取信息来构建 ErrorResponseErrorResponse error = new ErrorResponse(ex.getMessage(),         // 异常消息ex.getErrorCode(),       // 自定义错误码ex.getHttpStatus().value() // HTTP 状态码);// 返回包含 ErrorResponse 和对应 HTTP 状态码的 ResponseEntityreturn new ResponseEntity<>(error, ex.getHttpStatus());}// 处理所有未被上面捕获的其他异常(作为兜底)@ExceptionHandler(Exception.class)public ResponseEntity<ErrorResponse> handleGenericException(Exception ex) {// (可选)在这里记录未预料到的异常日志,方便排查问题// log.error("An unexpected error occurred: {}", ex.getMessage(), ex);ex.printStackTrace(); // 临时打印堆栈,生产环境应使用日志框架// 返回一个通用的内部服务器错误响应ErrorResponse error = new ErrorResponse("发生了一个意外错误。",              // 通用错误消息"ERR_INTERNAL_SERVER",           // 通用内部错误码HttpStatus.INTERNAL_SERVER_ERROR.value() // 500 状态码);return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);}// 你还可以根据需要添加处理特定框架异常的方法,// 例如处理 Spring Validation 的 MethodArgumentNotValidException 等// @ExceptionHandler(MethodArgumentNotValidException.class)// public ResponseEntity<ErrorResponse> handleValidationExceptions(...) { ... }
}

看,只需要两个 @ExceptionHandler 方法就够了:

  • • 一个处理所有我们自己定义的、继承自 ApplicationException 的已知错误。

  • • 一个处理所有其他未知的、意外的运行时错误,作为最后的保障。

5. 简化 Controller 代码 🪓
现在,你的 Controller 代码终于可以摆脱异常处理的苦差事,完全专注于业务逻辑了:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
// import com.yourpackage.dto.UserDto;
// import com.yourpackage.exception.ResourceNotFoundException;
// import com.yourpackage.service.UserService;@RestController
// @RequestMapping("/users") // 假设有 RequestMapping
public class UserController {// @Autowired// private UserService userService; // 假设注入了 UserService@GetMapping("/users/{id}")public UserDto getUser(@PathVariable Long id) { // 直接返回 DTO// 业务逻辑:查找用户,如果找不到,直接抛出我们自定义的异常return userService.findById(id).orElseThrow(() -> new ResourceNotFoundException("未找到 ID 为 " + id + " 的用户"));// 异常会被 GlobalExceptionHandler 捕获并处理}
}

看到了吗?Controller 里不再需要返回 ResponseEntity不再需要手动处理状态码,也不需要 try-catch 块了。代码清爽多了!

6. 独立地单元测试异常处理逻辑 🧪
现在,你可以编写单元测试来专门验证你的异常处理逻辑,而无需启动整个 Web 环境或 Mock Controller

import org.junit.jupiter.api.Test;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import static org.junit.jupiter.api.Assertions.assertEquals;
// import com.yourpackage.dto.ErrorResponse;
// import com.yourpackage.exception.ApplicationException;
// import com.yourpackage.exception.GlobalExceptionHandler;
// import com.yourpackage.exception.ResourceNotFoundException;public class GlobalExceptionHandlerTest {@Testvoid shouldReturnProperErrorResponseForKnownException() {// 准备:创建一个 GlobalExceptionHandler 实例和自定义异常实例GlobalExceptionHandler handler = new GlobalExceptionHandler();ApplicationException ex = new ResourceNotFoundException("资源未找到");// 执行:调用处理方法ResponseEntity<ErrorResponse> response = handler.handleApplicationException(ex);// 验证:检查返回的 HTTP 状态码和 ErrorResponse 内容是否符合预期assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()); // 状态码应为 404assertEquals("ERR_RESOURCE_NOT_FOUND", response.getBody().getErrorCode()); // 错误码应正确assertEquals("资源未找到", response.getBody().getMessage()); // 消息应正确// 还可以验证 timestamp 等...}@Testvoid shouldReturnInternalServerErrorForUnknownException() {GlobalExceptionHandler handler = new GlobalExceptionHandler();Exception ex = new RuntimeException("未知错误"); // 模拟一个未知异常ResponseEntity<ErrorResponse> response = handler.handleGenericException(ex);assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode()); // 状态码应为 500assertEquals("ERR_INTERNAL_SERVER", response.getBody().getErrorCode()); // 错误码应为通用内部错误assertEquals("发生了一个意外错误。", response.getBody().getMessage()); // 消息应为通用消息}
}

这种方式使得整个系统的异常处理部分变得模块化,并且可以独立进行测试

🧱 可选:使用枚举来管理错误码
为了进一步标准化错误码,可以定义一个错误码枚举:

public enum ErrorCode {USER_NOT_FOUND("ERR_USER_NOT_FOUND", "用户未找到"),INVALID_INPUT("ERR_INVALID_INPUT", "无效输入"),INTERNAL_ERROR("ERR_INTERNAL", "内部服务器错误");// 可以添加更多错误码...private final String code;private final String defaultMessage; // 可以加一个默认消息ErrorCode(String code, String defaultMessage) {this.code = code;this.defaultMessage = defaultMessage;}public String getCode() { return code; }public String getDefaultMessage() { return defaultMessage; }
}

然后你的自定义异常类可以这样写:

public class UserNotFoundException extends ApplicationException {public UserNotFoundException() {super(ErrorCode.USER_NOT_FOUND.getDefaultMessage()); // 使用枚举的默认消息}// 可以提供接收自定义消息的构造函数public UserNotFoundException(String message) {super(message);}@Overridepublic String getErrorCode() {return ErrorCode.USER_NOT_FOUND.getCode(); // 从枚举获取错误码}@Overridepublic HttpStatus getHttpStatus() {return HttpStatus.NOT_FOUND; // 或者也可以把 HttpStatus 关联到枚举里}
}

这样,所有的错误码都来自于一个单一的事实来源 (single source of truth),更易于管理。

🚀 这种策略的优势

✅ 逻辑集中 (Centralized logic)— 所有的异常映射和响应格式化都在一个地方。
结构一致 (Consistent structure)— 每个 API 错误都遵循可预测的格式。
模块化测试 (Modular testing)— 可以独立于 Web 层测试异常处理逻辑。
易于扩展 (Easy extension)— 添加新的自定义异常类型只需极少的代码。
代码库更整洁 (Cleaner codebase)— Controller 和 Service 层不再需要关心错误格式化。
生产级日志记录 (Production-grade logging)— 可以轻松地在GlobalExceptionHandler 中集成日志记录,对接 Sentry 或 ELK 等工具。

🔚 结语

Spring Boot 很强大,但它默认的异常处理机制对于严肃的、生产级别的应用来说,仍然过于手动化和混乱。到了 2025 年,开发者们需要的是更整洁、更集中化、更易于测试的错误处理策略。

通过将异常视为带有清晰元数据(错误码、状态码、消息)的一等公民,并将错误响应的格式化工作委托给一个集中的处理器,你的应用程序将变得更容易维护、扩展和调试。

Spring Boot 可能没有直接帮你解决好这个问题 —— 但运用这种策略,你可以自己动手搞定它

相关文章:

告别散乱的 @ExceptionHandler:实现统一、可维护的 Spring Boot 错误处理

Spring Boot 的异常处理机制一直都烂得可以。即便到了 2025 年&#xff0c;有了这么多进步和新版本&#xff0c;开发者们发现自己还是在跟 ControllerAdvice、分散各处的 ExceptionHandler 方法以及五花八门的响应结构较劲。这真的是一团糟。 无论你是在构建 REST API、微服务…...

MariaDB 与 MySQL 的关系:从同源到分道扬镳

MariaDB 与 MySQL 的关系&#xff1a;从同源到分道扬镳 1. 起源&#xff1a;MySQL 的辉煌与危机 MySQL 是最流行的开源关系型数据库之一&#xff0c;由瑞典公司 MySQL AB 开发&#xff0c;并于 1995 年 首次发布。由于其高性能、易用性和开源特性&#xff0c;MySQL 迅速成为 L…...

从零开始搭建你的个人博客:使用 GitHub Pages 免费部署静态网站

&#x1f310; 从零开始搭建你的个人博客&#xff1a;使用 GitHub Pages 免费部署静态网站 在互联网时代&#xff0c;拥有一个属于自己的网站不仅是一种展示方式&#xff0c;更是一种技术能力的体现。今天我们将一步步学习如何通过 GitHub Pages 搭建一个免费的个人博客或简历…...

C#串口通信

在C#中使用串口通信比较方便&#xff0c;.Net 提供了现成的类&#xff0c; SerialPort类。 本文不对原理啥的进行介绍&#xff0c;只介绍SerialPort类的使用。 SerialProt类内部是调用了CreateFile&#xff0c;WriteFile等WinAPI函数来实现串口通信。 在后期的Windows编程系…...

Qt 显示QRegExp 和 QtXml 不存在问题

QRegExp 和 QtXml 问题 在Qt6 中 已被弃用&#xff1b; 1&#xff09;QRegExp 已被弃用&#xff0c;改用 QRegularExpression Qt5 → Qt6 重大变更&#xff1a;QRegExp 被移到了 Qt5Compat 模块&#xff0c;默认不在 Qt6 核心模块中。 错误类型解决方法QRegExp 找不到改用 Q…...

【训练】Qwen2.5VL 多机多卡 Grounding Box定位

之前的相关文章&#xff1a; 【深度学习】LLaMA-Factory微调sft Qwen2-VL进行印章识别 https://www.dong-blog.fun/post/1661 使用LLaMA-Factory微调sft Qwen2-VL-7B-Instruct https://www.dong-blog.fun/post/1762 构建最新的LLaMA-Factory镜像 https://www.dong-blog.f…...

服务器配置llama-factory问题解决

在配置运行llama-factory&#xff0c;环境问题后显示环境问题。这边给大家附上连接&#xff0c;我们的是liunx环境但是还是一样的。大家也记得先配置虚拟环境。 LLaMA-Factory部署以及微调大模型_llamafactory微调大模型-CSDN博客 之后大家看看遇到的问题是不是我这样。 AI搜索…...

Spring Boot + Vue 实现在线视频教育平台

一、项目技术选型 前端技术&#xff1a; HTML CSS JavaScript Vue.js 前端框架 后端技术&#xff1a; Spring Boot 轻量级后端框架 MyBatis 持久层框架 数据库&#xff1a; MySQL 5.x / 8.0 开发环境&#xff1a; IDE&#xff1a;Eclipse / IntelliJ IDEA JDK&…...

使用Jmeter进行核心API压力测试

最近公司有发布会&#xff0c;需要对全链路比较核心的API的进行压测&#xff0c;今天正好分享下压测软件Jmeter的使用。 一、什么是Jmeter? JMeter 是 Apache 旗下的基于 Java 的开源性能测试工具。最初被设计用于 Web 应用测试&#xff0c;现已扩展到可测试多种不同的应用程…...

JavaScript中数组和对象不同遍历方法的顺序规则

在JavaScript中&#xff0c;不同遍历方法的顺序规则和适用场景存在显著差异。以下是主要方法的遍历顺序总结&#xff1a; 一、数组遍历方法 for循环 • 严格按数组索引顺序遍历&#xff08;0 → length-1&#xff09; • 支持break和continue中断循环 • 性能最优&#xff0c;…...

【机器学习-线性回归-5】多元线性回归:概念、原理与实现详解

线性回归是机器学习中最基础且广泛应用的算法之一&#xff0c;而多元线性回归则是其重要扩展。本文将全面介绍多元线性回归的核心概念、数学原理及多种实现方式&#xff0c;帮助读者深入理解这一强大的预测工具。 1. 多元线性回归概述 1.1 什么是多元线性回归 多元线性回归(…...

【软件设计师:数据结构】1.数据结构基础(一)

一 线性表 1.线性表定义 线性表是n个元素的有限序列,通常记为(a1,a2,…,an)。 特点: 存在惟一的表头和表尾。除了表头外,表中的每一个元素均只有惟一的直接前驱。除了表尾外,表中的每一个元素均只有惟一的直接后继。2.线性表的存储结构 (1)顺序存储 是用一组地址连续…...

简单面试提问

Nosql非关系型数据库&#xff1a; Mongodb&#xff1a;开源、json形式储存、c编写 Redis&#xff1a;key-value形式储存&#xff0c;储存在内存&#xff0c;c编写 关系型数据库&#xff1a; sqlite;&#xff1a;轻量型、0配置、磁盘存储、支持多种语言 mysql&#xff1a;开源…...

探秘数据中台:五大核心平台的功能全景解析

数据中台作为企业数据资产的 “智慧中枢”&#xff0c;通过整合数据处理全流程的核心功能&#xff0c;实现数据价值的深度挖掘与高效应用。以下从五大核心平台出发&#xff0c;全面拆解数据中台的功能架构与应用价值。 一、数据可视化平台&#xff1a;让数据 “开口说话” 1.…...

leetcode 3342. 到达最后一个房间的最少时间 II 中等

有一个地窖&#xff0c;地窖中有 n x m 个房间&#xff0c;它们呈网格状排布。 给你一个大小为 n x m 的二维数组 moveTime &#xff0c;其中 moveTime[i][j] 表示在这个时刻 以后 你才可以 开始 往这个房间 移动 。你在时刻 t 0 时从房间 (0, 0) 出发&#xff0c;每次可以移…...

redis----通用命令

文章目录 前言一、运行redis二、help [command]三、通用命令 前言 提示&#xff1a;这里可以添加本文要记录的大概内容&#xff1a; 学习一些通用命令 以下操作在windows中演示 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 一、运行redis 我们先c…...

PostgreSQL 查看索引碎片的方法

PostgreSQL 查看索引碎片的方法 在 PostgreSQL 中&#xff0c;索引碎片(Index Fragmentation)是指索引由于频繁的插入、更新和删除操作导致物理存储不连续&#xff0c;从而影响查询性能的情况。以下是几种查看索引碎片的方法&#xff1a; 一 使用 pgstattuple 扩展 1.1 安装…...

pip 常用命令及配置

一、python -m pip install 和 pip install 的区别 在讲解 pip 的命令之前&#xff0c;我们有必要了解一下 python -m pip install 和 pip install 的区别&#xff0c;以便于我们在不同的场景使用不同的方式。 python -m pip install 命令使用 python 可执行文件将 pip 模块作…...

IntelliJ IDEA 保姆级使用教程

文章目录 一、创建项目二、创建模块三、创建包四、创建类五、编写代码六、运行代码注意 七、IDEA 常见设置1、主题2、字体3、背景色 八、IDEA 常用快捷键九、IDEA 常见操作9.1、类操作9.1.1、删除类文件9.1.2、修改类名称注意 9.2、模块操作9.2.1、修改模块名快速查看 9.2.2、导…...

Comfyui 与 SDwebui

ComfyUI和SD WebUI是基于Stable Diffusion模型的两种不同用户界面工具&#xff0c;它们在功能、用户体验和适用场景上各有优劣。 1. 功能与灵活性 ComfyUI&#xff1a;ComfyUI以其节点式工作流设计为核心&#xff0c;强调用户自定义和灵活性。用户可以通过连接不同的模块&…...

Ubuntu Linux系统配置账号无密码sudo

在Linux系统中&#xff0c;配置无密码sudo可以通过修改sudoers文件来实现。以下是具体的配置步骤 一、编辑sudoers文件 输入sudo visudo命令来编辑sudo的配置文件。visudo是一个专门用于编辑sudoers文件的命令&#xff0c;它会在保存前检查语法错误&#xff0c;从而防止可能的…...

WiseAD:基于视觉-语言模型的知识增强型端到端自动驾驶——论文阅读

《WiseAD: Knowledge Augmented End-to-End Autonomous Driving with Vision-Language Model》2024年12月发表&#xff0c;来自新加坡国立和浙大的论文。 在快速发展的视觉语言模型&#xff08;VLM&#xff09;中&#xff0c;一般人类知识和令人印象深刻的逻辑推理能力的出现&a…...

探索SQLMesh中的Jinja宏:提升SQL查询的灵活性与复用性

在数据工程和数据分析领域&#xff0c;SQL是不可或缺的工具。随着项目复杂度的增加&#xff0c;如何高效地管理和复用SQL代码成为了一个重要课题。SQLMesh作为一款强大的工具&#xff0c;不仅支持标准的SQL语法&#xff0c;还引入了Jinja模板引擎的宏功能&#xff0c;极大地提升…...

配置linux自启java程序

配置linux自启java程序 1、切换root用户&#xff0c;并进入自启配置目录 sudo su - cd /etc/systemd/system2、编写启动文件 例如&#xff1a;class-server.service vi class-server.service脚本内容 [Unit] DescriptionClassServer Java Application Afternetwork.target…...

对Redis组件的深入探讨

目录 1、磁盘和内存 1.1、概念 1.2、区别 1.3、联系 2、redis基本特性 2.1、数据结构 2.2、性能 2.3、事件驱动架构 2.4、原子性 3、redis模型 3.1、单线程 3.2、事件驱动模型 3.3、epoll多路复用 4、数据持久化 4.1、RDB快照 4.2、AOF&#xff08;Append Only…...

Uni-app 组件使用

在前端开发领域&#xff0c;能够高效地创建跨平台应用是开发者们一直追求的目标。Uni-app 凭借其 “一次开发&#xff0c;多端部署” 的特性&#xff0c;成为了众多开发者的首选框架。而组件作为 Uni-app 开发的基础单元&#xff0c;合理运用组件能够极大地提升开发效率和代码的…...

k8s pod request/limit 值不带单位会发生什么?

在 Kubernetes 中&#xff0c;Pod 的 resources.requests 和 limits 字段必须显式指定单位。 一、未正确设置requests和limits字段的单位会产生影响&#xff1f; 1. 资源分配严重不足 例如&#xff0c;以下配置存在严重错误&#xff1a; resources:requests:memory: 512 # …...

Ruby 字符串(String)

Ruby 字符串&#xff08;String&#xff09; 引言 在编程语言中&#xff0c;字符串是表示文本数据的一种基本数据类型。在Ruby中&#xff0c;字符串处理是日常编程中非常常见的一项任务。本文将详细介绍Ruby中的字符串&#xff08;String&#xff09;类型&#xff0c;包括其创…...

嵌入式学习笔记 - STM32 SRAM控制器FSMC

一 SRAM控制器内部结构图&#xff1a; 以下以512K SRAM芯片为例 二 SRAM地址矩阵/寻址方式&#xff1a; SRAM的地址寻址方式通过行地址与列地址交互的方式存储数据 三 STM32 地址映射 从STM32的地址映射中可以看出&#xff0c;FSMC控制器支持扩展4块外部存储器区域&#xff0…...

经典算法 求解硬币组成问题

求解硬币组成问题 题目描述 实现一个算法求解组成硬币问题。介绍如下&#xff1a; 假设有面值给定的一些硬币&#xff0c;以及给定的总合值&#xff0c;问构成总合值的方法有多少种。 输入描述 第一行包含两个数字 N, M&#xff1a; N 表示硬币面值的种类数M 表示给定的总合…...