在构建 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 为什么需要统一响应格式
在实际项目开发中,统一响应格式有以下几个好处:
- 前后端协作更加顺畅:前端开发人员可以期望所有 API 返回相同结构的数据
- 错误处理更加一致:无论是业务逻辑错误还是系统异常,都可以用相同的格式返回
- 简化客户端代码:客户端可以用统一的方式解析响应,减少特殊情况处理
- 提高 API 的可维护性:当需要修改响应格式时,只需要修改一处代码
二、基本实现
2.1 定义统一响应类
首先,我们需要定义一个统一的响应类,用于包装所有的 API 响应:
为了让我们的统一响应处理更加健壮,我们可以优化
ResponseAdvice
的实现,使其能够处理null
返回值,并对String
类型响应的Content-Type
进行精确设置。
java
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
接口,将所有的响应包装成统一格式:
java
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:不执行
*/
@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;
}
// 返回 true 表示拦截所有响应
return true;
}
/**
* 在响应体写入前的处理
*/
@Override
public 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) {
// 极端情况,记录日志并返回一个简单的错误 JSON
return "{\"code\":500, \"message\":\"响应序列化失败\"}";
}
}
}
// 对于其他所有类型,直接包装成 Result.success
return 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 返回的响应:
java
@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/jsonClass<? extends HttpMessageConverter<?>> selectedConverterType
:选择的转换器类型ServerHttpRequest request
:当前请求ServerHttpResponse response
:当前响应
在这个方法中,我们可以根据需要修改响应体,例如添加额外的信息、修改状态码等。
3.3 处理 String 类型的特殊情况
在 beforeBodyWrite
方法中,我们对 String
类型的响应进行了特殊处理。这是因为 Spring MVC 在处理 String
类型的返回值时,使用的是 StringHttpMessageConverter
,而不是 MappingJackson2HttpMessageConverter
。如果我们直接返回 Result
对象,Spring 会尝试将其转换为 String
,这会导致类型转换异常。
因此,我们需要手动将 Result
对象转换为 JSON 字符串,以确保类型一致性:
java
if (body instanceof String) {
try {
return objectMapper.writeValueAsString(Result.success(body));
} catch (Exception e) {
return Result.error(500, "响应处理异常");
}
}
四、高级应用
4.1 排除特定的响应
在某些情况下,我们可能不希望对所有的响应都进行包装。例如,文件下载、特定的 API 版本可能需要保持原始格式。我们可以通过自定义注解来标记不需要包装的控制器或方法:
java
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface IgnoreResponseAdvice {
}
然后在 supports
方法中检查是否存在这个注解:
java
@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
时,我们也应该将其包装成统一格式:
java
@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 的全局异常处理机制结合,提供统一的错误响应格式:
java
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
@ResponseBody
public Result<String> handleException(Exception e) {
return Result.error(500, e.getMessage());
}
@ExceptionHandler(BusinessException.class)
@ResponseBody
public Result<String> handleBusinessException(BusinessException e) {
return Result.error(e.getCode(), e.getMessage());
}
}
这样,无论是正常响应还是异常响应,都会使用统一的格式。
4.4 注意事项与不适用场景
虽然 ResponseBodyAdvice
是实现统一响应的利器,但它并非万能。在某些特定场景下,我们应该禁用它,否则会破坏预期的功能。
-
文件下载或二进制流响应 :文件下载接口通常直接返回
Resource
、byte[]
或直接操作HttpServletResponse
的OutputStream
。如果将这些二进制内容包装进 JSON 对象,会导致文件下载失败。对于这类接口,务必使用@IgnoreResponseAdvice
注解进行排除。 -
Server-Sent Events (SSE) :SSE (服务器发送事件) 是一种服务端推送技术,其
Content-Type
为text/event-stream
,且连接是持久的。统一响应包装会将其内容格式改为application/json
,并立即关闭连接,从而破坏 SSE 机制。 -
非 JSON 格式的 API :本文的实现是围绕 JSON 进行的。如果你的 API 需要支持 XML、Protobuf 等其他数据格式,那么当前的
ObjectMapper
序列化方式将不再适用。你需要根据不同的Content-Type
提供不同的处理逻辑,或者禁用此功能。
五、实现原理
5.1 Spring MVC 请求处理流程
要理解 ResponseBodyAdvice
的工作原理,我们需要先了解 Spring MVC 的请求处理流程:
- 请求到达
DispatcherServlet
DispatcherServlet
根据请求找到对应的HandlerMapping
HandlerMapping
找到处理请求的Handler
和HandlerAdapter
HandlerAdapter
调用Handler
处理请求Handler
返回ModelAndView
或者通过@ResponseBody
直接返回响应体- 如果是直接返回响应体,则使用
HttpMessageConverter
将返回值转换为 HTTP 响应
5.2 ResponseBodyAdvice 的工作时机
ResponseBodyAdvice
在第 6 步中发挥作用。当 Spring MVC 使用 HttpMessageConverter
将返回值转换为 HTTP 响应之前,会先调用所有符合条件的 ResponseBodyAdvice
的 beforeBodyWrite
方法,对返回值进行处理。
具体来说,RequestResponseBodyMethodProcessor
类在处理 @ResponseBody
注解的方法返回值时,会调用 ResponseBodyAdvice
的方法:
java
// 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 会自动将这个类注册为一个全局的响应体增强器,应用到所有的控制器方法上。