萍雨说-响应规范 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 模板的法律文书交付给前端,这样前端就美滋滋了

下面具体来看一个例子哈

  1. 你的代码 (Service):

    Java

    text
    复制代码
    public void withdrawMoney(int amount) { if (amount > 1000) { // “主动”抛出一个业务异常 throw new BusinessException("取款金额过大"); } if (db.isOffline()) { // “被动”抛出一个运行时异常 throw new NullPointerException("数据库连接失败"); } }
  2. 你的全局异常处理器 (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)

  1. 前端(浏览器)请求 GET /user/999/username

  2. Controller 调用 Service

  3. Service 执行到 if (id == 999),条件成立。

  4. Service 主动、优雅地创建了一个“失败信封”:return ResultUtils.error(40400, "用户不存在");

  5. Controller 忠实地把这个“信封”原样返回给前端。

前端(浏览器 F12 网络面板)看到的结果:

  • HTTP 状态码: 200 OK

  • Response Body (JSON):

    JSON

    text
    复制代码
    { "code": 40400, "data": null, "message": "用户不存在" }
  • 系统状态: 稳定。 前端 App 拿到了 JSON,解析了 code,并向用户显示了“用户不存在”的提示。一切尽在掌控。


案发现场 2:意料之外的异常 (调用 id = 1)

  1. 前端请求 GET /user/1/username

  2. Controller 调用 Service

  3. Service 执行到 if (id == 1),条件成立。

  4. 代码尝试执行 user.getUsername(),但 usernull

  5. “嘣!” 一个 NullPointerException(空指针异常)“爆炸”了。

  6. Service 层没有 try...catch 它。

  7. 异常“尖叫”着被抛出 Service 层,回到了 Controller

  8. Controller 也没有 try...catch 它。

  9. 因为没有 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>

系统状态: 灾难。

  1. 前端崩溃: 前端 App 期望收到一个 JSON,但它收到了一个 HTML。它的 JSON 解析器会当场失败,导致前端 App 白屏或卡死。

  2. 安全泄露: 你把服务器的内部错误(java.lang.NullPointerException、类名、方法名)赤裸裸地暴露给了潜在的黑客。

  3. 系统不可靠: 你没有给用户任何有用的提示(“服务器开小差了”),只给了他一个 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); } }

使用说明:

  1. 替换包名: 将第一行的 com.example.yourproject.common 改成你项目的实际包路径。

  2. 依赖 ErrorCode: 这个模板假设你有一个 ErrorCode 枚举,并且该枚举包含 getCode(), getMessage(), getDescription() 方法,以及一个代表成功的常量(比如 SUCCESS,其 code 为 0,message 为 "ok")。你需要确保你的 ErrorCode.java 文件符合这个约定。

  3. 依赖 Lombok: 确保你的 pom.xml 中有 Lombok 依赖,并且 IDE 安装了 Lombok 插件。

  4. 在 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); // } // } // --- 模板结束 ---

如何使用这个模板:

  1. 修改 package 声明 (TODO 1)。

  2. 确认/修改 import 路径 (TODO 2, 3, 4)。 确保它指向你项目中正确的 BaseResponse, ErrorCode, BusinessException 类。

  3. 修改 @author (TODO 8)。

  4. 确认/修改 BusinessException 处理逻辑: 检查 businessExceptionHandler 中记录日志的方式(log.warn vs log.error)是否符合你的需求,以及从异常 e 中获取信息的代码(e.getCode(), e.getMessage(), e.getDescription())是否正确。

  5. 确认/修改 RuntimeException 处理逻辑: 确认 runtimeExceptionHandler 中使用的 ErrorCode.SYSTEM_ERROR (TODO 9) 是你 ErrorCode 枚举中定义的、代表通用系统错误的那个常量。

  6. (可选) 添加更多处理器: 根据项目需要,可以仿照现有方法,添加对其他特定异常(如参数校验异常、权限异常等)的处理逻辑。

把这个文件放在你的 exception 包下,它就会自动开始工作,拦截 Controller 抛出的异常并返回统一的 BaseResponse 了!

0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
花萍雨
下载 APP