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

Spring MVC 统一响应格式:ResponseBodyAdvice 从浅入深

在构建 RESTful API 时,保持响应格式的一致性是一个重要的设计原则。无论是成功响应还是错误响应,统一的格式可以让前端开发人员更容易处理数据,也使 API 文档更加清晰。Spring MVC 提供了 ResponseBodyAdvice 接口,它允许我们在响应体写入之前对其进行拦截和修改,是实现统一响应格式的理想工具。本文将全面介绍 ResponseBodyAdvice 的使用方法、实现原理和注意点。

一、基础概念

1.1 什么是 ResponseBodyAdvice

ResponseBodyAdvice 是 Spring MVC 框架中的一个接口,它允许在 Controller 处理完请求后、响应体被写入 HTTP 响应之前,对响应体进行修改或包装。这个接口定义在 org.springframework.web.servlet.mvc.method.annotation 包中,是 Spring 4.1 版本引入的特性。

1.2 为什么需要统一响应格式

在实际项目开发中,统一响应格式有以下几个好处:

  1. 前后端协作更加顺畅:前端开发人员可以期望所有 API 返回相同结构的数据
  2. 错误处理更加一致:无论是业务逻辑错误还是系统异常,都可以用相同的格式返回
  3. 简化客户端代码:客户端可以用统一的方式解析响应,减少特殊情况处理
  4. 提高 API 的可维护性:当需要修改响应格式时,只需要修改一处代码

二、基本实现

2.1 定义统一响应类

首先,我们需要定义一个统一的响应类,用于包装所有的 API 响应:

为了让我们的统一响应处理更加健壮,我们可以优化 ResponseAdvice 的实现,使其能够处理 null 返回值,并对 String 类型响应的 Content-Type 进行精确设置。

public class Result<T> {private Integer code;      // 状态码private String message;    // 消息private T data;           // 数据// 成功静态方法public static <T> Result<T> success(T data) {Result<T> result = new Result<>();result.setCode(200);result.setMessage("操作成功");result.setData(data);return result;}// 失败静态方法public static <T> Result<T> error(Integer code, String message) {Result<T> result = new Result<>();result.setCode(code);result.setMessage(message);return result;}// getter 和 setter 方法省略
}

2.2 实现 ResponseBodyAdvice 接口

接下来,我们实现 ResponseBodyAdvice 接口,将所有的响应包装成统一格式:

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;// 通过 basePackages 属性,我们可以指定对哪些包下的 Controller 进行拦截
// 这样可以提高性能,并避免意外拦截到 Spring Boot Actuator 或其他框架的接口
@ControllerAdvice(basePackages = "com.example.controller") // 请替换为你的项目包名
public class ResponseAdvice implements ResponseBodyAdvice<Object> {private final ObjectMapper objectMapper = new ObjectMapper();/*** 判断是否要执行 beforeBodyWrite 方法。* true:执行,false:不执行*/@Overridepublic boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {// 检查方法或类上是否有 @IgnoreResponseAdvice 注解if (returnType.getContainingClass().isAnnotationPresent(IgnoreResponseAdvice.class) ||(returnType.getMethod() != null && returnType.getMethod().isAnnotationPresent(IgnoreResponseAdvice.class))) {return false;}// 返回 true 表示拦截所有响应return true;}/*** 在响应体写入前的处理*/@Overridepublic Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {// 如果响应已经是 Result 类型,则无需再包装if (body instanceof Result) {return body;}// 如果 Controller 方法返回类型是 void,Spring Boot 会返回 null// 我们将其包装成成功的响应if (body == null) {return Result.success(null);}// 特殊处理 String 类型,因为 Spring MVC 会使用 StringHttpMessageConverter// 如果我们直接返回 Result 对象,会导致类型转换异常if (body instanceof String) {// 需要手动将 Result 对象序列化为 JSON 字符串response.getHeaders().setContentType(MediaType.APPLICATION_JSON);try {return objectMapper.writeValueAsString(Result.success(body));} catch (JsonProcessingException e) {// 序列化异常时,返回一个标准的错误响应// 这里最好也序列化为字符串,以避免再次触发转换问题try {return objectMapper.writeValueAsString(Result.error(500, "响应序列化失败"));} catch (JsonProcessingException ex) {// 极端情况,记录日志并返回一个简单的错误 JSONreturn "{\"code\":500, \"message\":\"响应序列化失败\"}";}}}// 对于其他所有类型,直接包装成 Result.successreturn Result.success(body);}
}
  • supports方法表示哪些请求需要被拦截
  • beforeBodyWrite表示在返回之前需要执行的逻辑,比如进行返回结果的包装

2.3 @ControllerAdvice 注解

在上面的代码中,我们使用了 @ControllerAdvice 注解。这个注解表明该类是一个全局的控制器增强类,它可以应用于所有的 Controller。这意味着所有 Controller 返回的响应都会经过这个类的处理。

三、深入理解 ResponseBodyAdvice

3.1 supports 方法详解

supports 方法决定了是否应该调用 beforeBodyWrite 方法来处理响应体。它接收两个参数:

  • MethodParameter returnType:表示处理器方法的返回类型,包含了方法的返回值类型、方法本身的信息等
  • Class<? extends HttpMessageConverter<?>> converterType:用于将返回值转换为 HTTP 响应的转换器类型

通过这两个参数,我们可以根据返回类型、转换器类型等条件来决定是否需要处理响应体。例如,我们可以只处理特定包下的 Controller 返回的响应:

@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {// 只处理 com.example.controller 包下的 Controller 返回的响应return returnType.getDeclaringClass().getPackage().getName().startsWith("com.example.controller");
}

3.2 beforeBodyWrite 方法详解

beforeBodyWrite 方法在响应体被写入之前被调用,允许我们修改或包装响应体。它接收以下参数:

  • Object body:原始的响应体对象
  • MethodParameter returnType:处理器方法的返回类型
  • MediaType selectedContentType:选择的内容类型,如 application/json
  • Class<? extends HttpMessageConverter<?>> selectedConverterType:选择的转换器类型
  • ServerHttpRequest request:当前请求
  • ServerHttpResponse response:当前响应

在这个方法中,我们可以根据需要修改响应体,例如添加额外的信息、修改状态码等。

3.3 处理 String 类型的特殊情况

beforeBodyWrite 方法中,我们对 String 类型的响应进行了特殊处理。这是因为 Spring MVC 在处理 String 类型的返回值时,使用的是 StringHttpMessageConverter,而不是 MappingJackson2HttpMessageConverter。如果我们直接返回 Result 对象,Spring 会尝试将其转换为 String,这会导致类型转换异常。

因此,我们需要手动将 Result 对象转换为 JSON 字符串,以确保类型一致性:

if (body instanceof String) {try {return objectMapper.writeValueAsString(Result.success(body));} catch (Exception e) {return Result.error(500, "响应处理异常");}
}

四、高级应用

4.1 排除特定的响应

在某些情况下,我们可能不希望对所有的响应都进行包装。例如,文件下载、特定的 API 版本可能需要保持原始格式。我们可以通过自定义注解来标记不需要包装的控制器或方法:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface IgnoreResponseAdvice {
}

然后在 supports 方法中检查是否存在这个注解:

@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {// 如果类或方法上有 @IgnoreResponseAdvice 注解,则不进行处理if (returnType.getContainingClass().isAnnotationPresent(IgnoreResponseAdvice.class) ||(returnType.getMethod() != null && returnType.getMethod().isAnnotationPresent(IgnoreResponseAdvice.class))) {return false;}return true;
}

4.2 处理 null 值

当控制器返回 null 时,我们也应该将其包装成统一格式:

@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {// 处理 null 值if (body == null) {return Result.success(null);}// 其他处理逻辑...
}

4.3 与全局异常处理结合

ResponseBodyAdvice 可以与 Spring 的全局异常处理机制结合,提供统一的错误响应格式:

@ControllerAdvice
public class GlobalExceptionHandler {@ExceptionHandler(Exception.class)@ResponseBodypublic Result<String> handleException(Exception e) {return Result.error(500, e.getMessage());}@ExceptionHandler(BusinessException.class)@ResponseBodypublic Result<String> handleBusinessException(BusinessException e) {return Result.error(e.getCode(), e.getMessage());}
}

这样,无论是正常响应还是异常响应,都会使用统一的格式。

4.4 注意事项与不适用场景

虽然 ResponseBodyAdvice 是实现统一响应的利器,但它并非万能。在某些特定场景下,我们应该禁用它,否则会破坏预期的功能。

  1. 文件下载或二进制流响应:文件下载接口通常直接返回 Resourcebyte[] 或直接操作 HttpServletResponseOutputStream。如果将这些二进制内容包装进 JSON 对象,会导致文件下载失败。对于这类接口,务必使用 @IgnoreResponseAdvice 注解进行排除。

  2. Server-Sent Events (SSE):SSE (服务器发送事件) 是一种服务端推送技术,其 Content-Typetext/event-stream,且连接是持久的。统一响应包装会将其内容格式改为 application/json,并立即关闭连接,从而破坏 SSE 机制。

  3. 非 JSON 格式的 API:本文的实现是围绕 JSON 进行的。如果你的 API 需要支持 XML、Protobuf 等其他数据格式,那么当前的 ObjectMapper 序列化方式将不再适用。你需要根据不同的 Content-Type 提供不同的处理逻辑,或者禁用此功能。

五、实现原理

5.1 Spring MVC 请求处理流程

要理解 ResponseBodyAdvice 的工作原理,我们需要先了解 Spring MVC 的请求处理流程:

  1. 请求到达 DispatcherServlet
  2. DispatcherServlet 根据请求找到对应的 HandlerMapping
  3. HandlerMapping 找到处理请求的 HandlerHandlerAdapter
  4. HandlerAdapter 调用 Handler 处理请求
  5. Handler 返回 ModelAndView 或者通过 @ResponseBody 直接返回响应体
  6. 如果是直接返回响应体,则使用 HttpMessageConverter 将返回值转换为 HTTP 响应

5.2 ResponseBodyAdvice 的工作时机

ResponseBodyAdvice 在第 6 步中发挥作用。当 Spring MVC 使用 HttpMessageConverter 将返回值转换为 HTTP 响应之前,会先调用所有符合条件的 ResponseBodyAdvicebeforeBodyWrite 方法,对返回值进行处理。

具体来说,RequestResponseBodyMethodProcessor 类在处理 @ResponseBody 注解的方法返回值时,会调用 ResponseBodyAdvice 的方法:

// RequestResponseBodyMethodProcessor 类的 writeWithMessageConverters 方法(简化版)
protected <T> void writeWithMessageConverters(T value, MethodParameter returnType, ...) {// ...// 调用 ResponseBodyAdvice 的 beforeBodyWrite 方法if (advice != null) {body = advice.beforeBodyWrite(body, returnType, selectedContentType, selectedConverterType, request, response);}// 使用 HttpMessageConverter 将 body 写入响应selectedConverter.write(body, selectedContentType, outputMessage);// ...
}

5.3 @ControllerAdvice 的作用

@ControllerAdvice 注解是 Spring MVC 提供的一种机制,用于定义全局的控制器增强类。它可以包含 @ExceptionHandler@InitBinder@ModelAttribute 方法,这些方法会应用到所有的 @Controller 类中。

当我们在实现 ResponseBodyAdvice 接口的类上添加 @ControllerAdvice 注解时,Spring 会自动将这个类注册为一个全局的响应体增强器,应用到所有的控制器方法上。

http://www.xdnf.cn/news/1180279.html

相关文章:

  • redis常用数据类型
  • 智慧工厂网络升级:新型 SD-WAN 技术架构与应用解析
  • Leetcode 07 java
  • 13-C语言:第13天笔记
  • C++第一节课入门
  • 基础NLP | 02 深度学习基本原理
  • PDF转Markdown - Python 实现方案与代码
  • 爬虫逆向--Day12--DrissionPage案例分析【小某书评价数据某东评价数据】
  • 使用爬虫获取游戏的iframe地址
  • 2025最新MySQL面试题实战记录,互联网公司常问题目
  • Mac电脑开发Python(基于vs code)
  • M²IV:面向大型视觉-语言模型中高效且细粒度的多模态上下文学习
  • 数字系统自动设计:从C++到门级网表
  • 如何使用 pdfMake 中文字体
  • 排序初识(上)-- 讲解超详细
  • Unity 多人游戏框架学习系列九
  • nuxt更改页面渲染的html,去除自定义属性、
  • 在Ubuntu上使用QEMU学习RISC-V程序(2)gdb调试
  • Java面试宝典:Spring专题二
  • 回调后门 函数
  • 如何彻底清除服务器上的恶意软件与后门
  • 基于Matlab图像处理的水果分级系统
  • Compose 适配 - 键鼠模式
  • Linux和Windows基于V4L2和TCP的QT监控
  • JavaWeb学习打卡13(JSP原理解析)
  • 【0基础PS】PS(Photoshop)与Ai( Illustrator )等相似软件区别
  • 内网IM:BeeWorks私有化部署的安全通讯解决方案
  • Linux命令基础完结篇
  • Windows 11下纯软件模拟虚拟机的设备模拟与虚拟化(仅终端和网络)
  • 【C++】类和对象(1)