萍雨说-响应规范 1.0
在现代的开发规范当中 "BaseResponse 静态方法 + ErrorCode 枚举" 是更好的响应规范原则,但我还整理到哪里,所以就先把一部分 "老旧" 的笔记先发出来吧,后续会去聊 "BaseResponse 静态方法 + ErrorCode 枚举" 这个更好的响应规范原则,就这样,这是我的第一篇算是技术的笔记吧,这件事情想了很久了,就先很粗糙的开始吧
ps:这篇是有 AI 生成的成分在的,且比例蛮高的,主要是在 “没有设置全局异常处理器” 和 “模板” 部分,昨天刷知乎看到很多人在吐槽 AI 生成内容,但我对 AI 生成内容的观点是:我不是很在乎内容到底是谁生产的,我只在乎生产的内容是否对我有用,仅此一点
1. 为什么需要 BaseResponse
这个问题其实也可以换个问法,就是 BaseResponse 的作用是什么,它的作用是规范后端返回给前端的数据格式,以保证前端所接受的数据格式是相同的,不能是千奇百怪的,这样可读性不好,会让前端感觉到很混乱
2. BaseResponse 的固定数据格式是什么样子的
大概分为了下面的三个部分
-
code-业务码——其实和状态码是差不多的,只不过相比状态码有更多的细分,比如 0 代表成功,40100 代表未登录,后面考虑更加详细的展开
-
data-数据——我们真正的数据,比如一个 User 对象或者一个 Boolean
-
message-信息——这是给人看到的,一般就是中文或者英文,反正是人可以看得懂的
==在我们的所有 Controller 层接口当中,返回值都应该是 BaseResponse< T >==
3. 为什么需要 ResultUtils
前面的 BaseResponse 是一个模板嘛,那么是谁来生产具体的信封内容呢?就是 ResultUtils
如果我们成功的话,就把需要的数据给寄过去
ResultUtils.success(data);
如果我们失败的话,就把需要的错误信息寄过去,包括给系统看的code 和给人看的 message
ResultUtils.error(code, message);
这里有一个很关键的点,就是如果没有正常执行,出现的一些异常之类的,是我们主动捕获的,我们预见了一些情况的发生,然后去处理了这些情况,但是总有一些情况是我们无法预见到的,那么该如何处理这些没有预见到的异常呢?这就引出了我们的全局异常处理器——GlobalExceptionHander,下面就来介绍一下
4. 全局异常处理器——GlobalExceptionHandler
它是一种兜底机制,用来处理那些我们没有预见和捕获的异常,比如 Service 和 Controller 中未被捕获的异常
举个例子,如果我们的 Service 层报出了一个 NullPointerException(空指针异常)的话,它会沿着 Service 层到 Controller 层,再通过 Controller 层到前端,因为这不是经过我们的 BaseResponse 模板规范的,所以就会出现我们在做 BaseResponse 时最不想看到的情况,就是让前端看到了各种数据格式不一的报错信息
GlobalExcepitionHandler 做的事情就是:守在 Controller 层的最外面,当有意料之外的异常尖叫着奔向 Controller 层的时候,GlobalExceptionHandler 就会立即把它给拦截下来,就不会进入 Controller 层,就更不会进入前端,影响秩序了
6. 三者的关系是什么样子的
当我们的 GlobalExceptionHandler 在捕获到一个异常之后,就会立即调用 ResultUtils,让它根据异常的具体信息生成一份符合 BaseResponse 模板的法律文书交付给前端,这样前端就美滋滋了
下面具体来看一个例子哈
-
你的代码 (Service):
Java
▼text复制代码public void withdrawMoney(int amount) { if (amount > 1000) { // “主动”抛出一个业务异常 throw new BusinessException("取款金额过大"); } if (db.isOffline()) { // “被动”抛出一个运行时异常 throw new NullPointerException("数据库连接失败"); } } -
你的全局异常处理器 (GlobalExceptionHandler):
Java
▼text复制代码@RestControllerAdvice // (声明自己是“最高法院”) public class GlobalExceptionHandler { // (审理“业务异常”法庭) @ExceptionHandler(BusinessException.class) public BaseResponse handleBusinessException(BusinessException e) { log.warn("业务异常: {}", e.getMessage()); // 它在这里“调用”了 ResultUtils! return ResultUtils.error(40001, e.getMessage()); // e.g., "取款金额过大" } // (审理“运行时异常”法庭) @ExceptionHandler(RuntimeException.class) public BaseResponse handleRuntimeException(RuntimeException e) { log.error("系统运行时异常!", e); // (严重错误,要记录堆栈) // 它也“调用”了 ResultUtils! return ResultUtils.error(50000, "服务器开小差了,请稍后再试"); }
总结一下:
-
BaseResponse:是“目标”(我们要返回的东西)。 -
ResultUtils:是“工具”(我们_主动_创建目标时用)。 -
GlobalExceptionHandler:是“保险”(当_被动_出错时,它会来接管,并_使用_ResultUtils这个工具,来返回BaseResponse这个目标)。
7. 如果我们没有设置全局异常处理器呢?
因为我有主意到前面有提到过说 ResultUtils 其实是一种对于所遇见情况的一种主动的采取举措,然后 GlobalExceptionHandler 是对于意料之外的异常的一种兜底,也就是说兜底是可以没有的,对吧?当然在实际的开发规范当中肯定是需要有兜底的,不能因为一个意料之外的异常就把整个系统搞崩了嘛,而且意料之外的异常是一定存在的
所以说如果没有设置全局异常处理器的话,只有 BaseResponse 和 ResultUtils 也是可以工作的,只是抗风险能力是很弱的,在实际调用中是什么样的呢?下面来举一个例子:
我们的“案犯”代码 (在 UserService 中):
Java
▼text复制代码// 我们在 Service 里写了一个有潜在 BUG 的方法 public BaseResponse<String> getUsernameById(Long id) { // 1. 可预见的业务逻辑 if (id == 999) { // 这是一个“可预见的”失败,我们“主动”用 ResultUtils 处理 return ResultUtils.error(40400, "用户不存在"); } // 2. 意料之外的 BUG! if (id == 1) { User user = null; // 假设这里因为某个复杂逻辑,user 意外地变成了 null return ResultUtils.success(user.getUsername()); // <-- 这里将抛出 NullPointerException! } // 3. 正常流程 return ResultUtils.success("侦探张三"); }
我们的 Controller:
Java
▼text复制代码@GetMapping("/user/{id}/username") public BaseResponse<String> getUsername(@PathVariable Long id) { // Controller 只是一个“传声筒” return userService.getUsernameById(id); }
案发现场 1:可预见的失败 (调用 id = 999)
-
前端(浏览器)请求
GET /user/999/username。 -
Controller调用Service。 -
Service执行到if (id == 999),条件成立。 -
Service主动、优雅地创建了一个“失败信封”:return ResultUtils.error(40400, "用户不存在");。 -
Controller忠实地把这个“信封”原样返回给前端。
前端(浏览器 F12 网络面板)看到的结果:
-
HTTP 状态码:
200 OK -
Response Body (JSON):
JSON
▼text复制代码{ "code": 40400, "data": null, "message": "用户不存在" } -
系统状态: 稳定。 前端 App 拿到了 JSON,解析了
code,并向用户显示了“用户不存在”的提示。一切尽在掌控。
案发现场 2:意料之外的异常 (调用 id = 1)
-
前端请求
GET /user/1/username。 -
Controller调用Service。 -
Service执行到if (id == 1),条件成立。 -
代码尝试执行
user.getUsername(),但user是null。 -
“嘣!” 一个
NullPointerException(空指针异常)“爆炸”了。 -
Service层没有try...catch它。 -
异常“尖叫”着被抛出
Service层,回到了Controller。 -
Controller也没有try...catch它。 -
因为没有
GlobalExceptionHandler(最高法院)来兜底,这个“尖叫的异常”直接冲出了 Spring Boot 应用。
前端(浏览器 F12 网络面板)看到的结果:
-
HTTP 状态码:
500 Internal Server Error -
Response Body (HTML):
HTML
▼text复制代码<html> <body> <h1>Whitelabel Error Page</h1> <p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p> <hr/> <div> <div>Timestamp: Mon Nov 03 19:25:10 PST 2025</div> <div>Status: 500</div> <div>Error: Internal Server Error</div> <div> Message: java.lang.NullPointerException: Cannot invoke "com.pingyu.codematebackend.model.User.getUsername()" because "user" is null </div> <div>Path: /user/1/username</div> </div> </body> </html>
系统状态: 灾难。
-
前端崩溃: 前端 App 期望收到一个 JSON,但它收到了一个 HTML。它的 JSON 解析器会当场失败,导致前端 App 白屏或卡死。
-
安全泄露: 你把服务器的内部错误(
java.lang.NullPointerException、类名、方法名)赤裸裸地暴露给了潜在的黑客。 -
系统不可靠: 你没有给用户任何有用的提示(“服务器开小差了”),只给了他一个 500 错误。
总结:GlobalExceptionHandler 的价值
现在,想象一下。如果我们有 GlobalExceptionHandler。
在“案发现场 2”的第 9 步,当那个 NullPointerException 准备冲出 Controller 时,GlobalExceptionHandler(最高法院)会一把抓住它。
GlobalExceptionHandler 会说:“站住!不许你用 HTML 吓唬用户!”
然后,它会(在它的 @ExceptionHandler 方法里)自己去调用 ResultUtils:
return ResultUtils.error(50000, "哎呀,服务器开小差了");
于是,前端最终收到的仍然是:
-
HTTP 状态码:
200 OK(或者 500,这取决于你的配置,但 Body 一定是 JSON) -
Response Body (JSON):
JSON
▼text复制代码{ "code": 50000, "data": null, "message": "哎呀,服务器开小差了" }
这就是“兜底”的全部意义:确保任何灾难,最终都能被包装成一个前端可预期的、统一的 BaseResponse JSON,而不是一个赤裸裸的、破坏性的 500 HTML 错误。
8. BaseResponse 模板
BaseResponse 的模板——[[BaseResponse.java 模板]]
▼text复制代码package com.example.yourproject.common; // <-- TODO: 修改为你项目的实际包名 import lombok.Data; // 引入 Lombok 简化代码 import java.io.Serializable; /** * 通用返回类 (Unified Response Body) - 可复用模板 * * 用于封装 API 接口的响应结果,提供统一的结构。 * 无论成功或失败,都返回此对象实例。 * * @param <T> 响应数据的类型 (data 字段的类型) * @author [你的名字/团队名] */ @Data // Lombok 注解:自动生成 getter, setter, toString, equals, hashCode public class BaseResponse<T> implements Serializable { private static final long serialVersionUID = 1L; // 保证序列化兼容性 /** * 状态码 (来自 ErrorCode.getCode()) * 约定:通常 0 代表成功,其他非 0 代表失败。 */ private int code; /** * 响应数据 (泛型,成功时携带的数据) */ private T data; /** * 响应消息 (成功或失败时的提示信息, 通常来自 ErrorCode.getMessage()) */ private String message; /** * (可选) 详细错误描述 (通常只在失败时携带, 用于调试或日志记录) */ private String description; // --- 构造函数 (私有,强制使用静态工厂方法创建,保证对象创建的规范性) --- /** * 全参数私有构造函数 * @param code 状态码 * @param data 数据 * @param message 消息 * @param description 描述 */ private BaseResponse(int code, T data, String message, String description) { this.code = code; this.data = data; this.message = message; this.description = description; } // --- 静态工厂方法 (推荐使用,代码可读性更高) --- /** * 创建成功的响应 (携带数据,使用默认成功消息) * @param data 成功时携带的数据 * @param <T> 数据类型 * @return 成功的 BaseResponse 实例 (code=0, message="ok") */ public static <T> BaseResponse<T> success(T data) { // 假设 ErrorCode.SUCCESS 定义为 (0, "ok", "") return new BaseResponse<>(0, data, "ok", ""); // 或者如果你定义了 ErrorCode.SUCCESS: // return new BaseResponse<>(ErrorCode.SUCCESS.getCode(), data, ErrorCode.SUCCESS.getMessage(), ErrorCode.SUCCESS.getDescription()); } /** * 创建成功的响应 (携带数据,使用自定义成功消息) * @param data 成功时携带的数据 * @param message 自定义成功消息 * @param <T> 数据类型 * @return 成功的 BaseResponse 实例 (code=0) */ public static <T> BaseResponse<T> success(T data, String message) { return new BaseResponse<>(0, data, message, ""); // Description 通常为空 } /** * 创建成功的响应 (不携带数据,例如:删除成功) * @return 成功的 BaseResponse 实例 (code=0, data=null, message="ok") */ public static <T> BaseResponse<T> success() { return new BaseResponse<>(0, null, "ok", ""); } // --- 创建失败响应的方法 --- /** * 创建失败的响应 (根据 ErrorCode 枚举实例) * @param errorCode 错误码枚举实例 (必须包含 getCode(), getMessage(), getDescription() 方法) * @return 失败的 BaseResponse 实例 (data=null) */ public static <T> BaseResponse<T> error(ErrorCode errorCode) { return new BaseResponse<>(errorCode.getCode(), null, errorCode.getMessage(), errorCode.getDescription()); } /** * 创建失败的响应 (根据 ErrorCode 和 自定义错误消息) * @param errorCode 错误码枚举实例 * @param message 自定义错误消息 (覆盖 errorCode 的默认 message) * @return 失败的 BaseResponse 实例 (data=null) */ public static <T> BaseResponse<T> error(ErrorCode errorCode, String message) { return new BaseResponse<>(errorCode.getCode(), null, message, errorCode.getDescription()); } /** * 创建失败的响应 (根据 ErrorCode, 自定义消息, 和 自定义描述) * @param errorCode 错误码枚举实例 * @param message 自定义错误消息 * @param description 自定义详细描述 * @return 失败的 BaseResponse 实例 (data=null) */ public static <T> BaseResponse<T> error(ErrorCode errorCode, String message, String description) { return new BaseResponse<>(errorCode.getCode(), null, message, description); } }
使用说明:
-
替换包名: 将第一行的
com.example.yourproject.common改成你项目的实际包路径。 -
依赖
ErrorCode: 这个模板假设你有一个ErrorCode枚举,并且该枚举包含getCode(),getMessage(),getDescription()方法,以及一个代表成功的常量(比如SUCCESS,其code为 0,message为 "ok")。你需要确保你的ErrorCode.java文件符合这个约定。 -
依赖 Lombok: 确保你的
pom.xml中有 Lombok 依赖,并且 IDE 安装了 Lombok 插件。 -
在 Controller 中使用:
-
成功时:
return BaseResponse.success(yourData);或return BaseResponse.success(yourData, "操作成功"); -
失败时 (Controller 层捕获):
return BaseResponse.error(ErrorCode.PARAMS_ERROR, "参数不能为空"); -
失败时 (全局异常处理器捕获
BusinessException): 在处理器中return BaseResponse.error(e.getErrorCode(), e.getMessage(), e.getDescription());
-
9. GlobalExceptionHandler 模板
Java
▼text复制代码// --- 全局异常处理器模板 --- package com.example.yourproject.exception; // <-- TODO: 1. 修改为你的项目的 exception 包路径 import com.example.yourproject.common.BaseResponse; // <-- TODO: 2. 确认 BaseResponse 导入路径 import com.example.yourproject.common.ErrorCode; // <-- TODO: 3. 确认 ErrorCode 导入路径 // import com.example.yourproject.common.exception.BusinessException; // <-- TODO: 4. 确认 BusinessException 导入路径 import lombok.extern.slf4j.Slf4j; // 5. 确保导入 Slf4j import org.springframework.web.bind.annotation.ExceptionHandler; // 6. 确保导入 ExceptionHandler import org.springframework.web.bind.annotation.RestControllerAdvice; // 7. (核心) 确保导入 RestControllerAdvice /** * 全局异常处理器 - 可复用模板 * * 使用 @RestControllerAdvice 统一拦截 Controller 层抛出的异常。 * 针对不同类型的异常返回统一的 BaseResponse 格式。 * * @author [你的名字] // <-- TODO: 8. 修改作者名 */ @RestControllerAdvice // (核心) 声明这是一个全局异常处理组件,针对 @RestController @Slf4j // 开启 Lombok 的日志功能 public class GlobalExceptionHandler { /** * 【模板部分 - 处理自定义业务异常】 * 捕获并处理自定义的 BusinessException。 * 你项目中自定义的业务异常类可能名称不同,只需修改 @ExceptionHandler 中的类即可。 * * @param e 捕获到的 BusinessException 实例 * @return 包含具体业务错误码和消息的 BaseResponse */ @ExceptionHandler(BusinessException.class) // 指定处理 BusinessException 及其子类 public BaseResponse<?> businessExceptionHandler(BusinessException e) { // 记录日志:包含错误码、错误消息、(可选)描述信息。 // 使用 log.warn 或 log.error 取决于你认为业务异常的严重程度。 // 通常,由客户端参数错误等引起的业务异常用 warn 即可。 log.warn("BusinessException: code={}, message={}, description={}", e.getCode(), e.getMessage(), e.getDescription()); // (可选) 如果需要详细堆栈用于调试,可以取消下面这行注释,但这在生产环境可能产生过多日志 // log.warn("BusinessException StackTrace: ", e); // 从 BusinessException 中提取信息,构建 BaseResponse 并返回给前端 // 这里假设 BusinessException 有 getErrorCode(), getMessage(), getDescription() 方法 return BaseResponse.error(e.getErrorCode(), e.getMessage(), e.getDescription()); } /** * 【模板部分 - 处理系统运行时异常】 * 捕获并处理 RuntimeException (或其他更通用的 Exception) 作为最后的防线。 * 这用于捕获代码中未预料到的错误,例如 NullPointerException 等。 * * @param e 捕获到的 RuntimeException 实例 * @return 包含通用“系统错误”信息的 BaseResponse */ @ExceptionHandler(RuntimeException.class) // 捕获所有未被上面更精确处理的 RuntimeException public BaseResponse<?> runtimeExceptionHandler(RuntimeException e) { // 记录【严重错误】日志:必须包含完整的堆栈跟踪信息 e,以便排查问题! log.error("RuntimeException occurred: {}", e.getMessage(), e); // 将 e 作为最后一个参数传入,Slf4j 会自动打印堆栈 // 返回给前端一个【统一】且【模糊】的系统错误响应 // 不要将 e.getMessage() 或 e.toString() 直接暴露给用户,可能包含敏感信息 // 使用预定义的 ErrorCode.SYSTEM_ERROR // <-- TODO: 9. 确认你的 ErrorCode 中有 SYSTEM_ERROR 或类似的通用系统错误码 return BaseResponse.error(ErrorCode.SYSTEM_ERROR, "系统内部异常,请联系管理员", ""); } // --- (可选) 添加更多异常处理器 --- // 你可以根据需要添加更多 @ExceptionHandler 方法来处理特定的其他异常类型, // 例如 Spring 的参数绑定异常 (BindException), 权限校验异常 (AccessDeniedException) 等, // 并将它们转换为对应的 BaseResponse。 // // @ExceptionHandler(BindException.class) // public BaseResponse<?> bindExceptionHandler(BindException e) { // log.warn("Parameter binding error: {}", e.getMessage()); // // 从 e 中提取具体的参数校验失败信息,构建更友好的错误消息 // String errorMessage = extractBindingErrors(e); // 需要自己实现这个方法 // return BaseResponse.error(ErrorCode.PARAMS_ERROR, errorMessage); // } // } // --- 模板结束 ---
如何使用这个模板:
-
修改
package声明 (TODO 1)。 -
确认/修改
import路径 (TODO 2, 3, 4)。 确保它指向你项目中正确的BaseResponse,ErrorCode,BusinessException类。 -
修改
@author(TODO 8)。 -
确认/修改
BusinessException处理逻辑: 检查businessExceptionHandler中记录日志的方式(log.warnvslog.error)是否符合你的需求,以及从异常e中获取信息的代码(e.getCode(),e.getMessage(),e.getDescription())是否正确。 -
确认/修改
RuntimeException处理逻辑: 确认runtimeExceptionHandler中使用的ErrorCode.SYSTEM_ERROR(TODO 9) 是你ErrorCode枚举中定义的、代表通用系统错误的那个常量。 -
(可选) 添加更多处理器: 根据项目需要,可以仿照现有方法,添加对其他特定异常(如参数校验异常、权限异常等)的处理逻辑。
把这个文件放在你的 exception 包下,它就会自动开始工作,拦截 Controller 抛出的异常并返回统一的 BaseResponse 了!
