RequestBodyAdvice和ResponseBodyAdvice
Spring MVC 之 RequestBodyAdvice 与 ResponseBodyAdvice:请求 / 响应增强的核心实践
在 Spring MVC 4.0+ 中,RequestBodyAdvice 和 ResponseBodyAdvice 是两个强大的请求 / 响应增强接口,分别用于在 @RequestBody 参数解析前 / 后、@ResponseBody 响应写入前对数据进行拦截和处理。它们摆脱了传统拦截器(HandlerInterceptor)只能操作 HttpServletRequest/HttpServletResponse 流的限制,可直接针对Java 对象级别的数据进行加工(如加密解密、日志记录、统一格式封装),是前后端数据交互中 “横切关注点” 处理的最佳方案。
从 “接口作用→核心方法→实现步骤→实战场景” 四个维度,彻底讲透这两个接口的使用逻辑与价值。
核心概念:为什么需要 RequestBodyAdvice/ResponseBodyAdvice?
在传统开发中,若需对 @RequestBody 请求(如 JSON 入参)或 @ResponseBody 响应(如 JSON 出参)进行统一处理(如:
- 请求入参解密(前端加密传输,后端解密后解析);
- 响应出参统一格式封装(所有接口返回
{code:0, msg:"success", data:{}}); - 请求 / 响应日志记录(打印入参、出参详情,便于排查问题);
- 数据校验增强(补充自定义校验逻辑,如 Token 合法性校验);
传统方案(如拦截器 HandlerInterceptor)需手动读取 / 写入请求 / 响应流,存在流不可重复读取、数据格式解析复杂(如 JSON 字符串转 Java 对象)等问题。而 RequestBodyAdvice/ResponseBodyAdvice 直接在 Spring MVC 的参数解析 / 响应写入流程中拦截 Java 对象,无需处理流操作,开发效率和灵活性大幅提升。
RequestBodyAdvice:请求入参增强(@RequestBody 预处理)
RequestBodyAdvice 作用于标注 @RequestBody 的 Controller 方法参数,在参数被 HttpMessageConverter(如 Jackson)解析为 Java 对象前 / 后进行拦截处理,核心是 “加工请求数据,确保解析出的对象符合业务需求”。
1. 接口核心方法解析
RequestBodyAdvice 接口包含 4 个核心方法,执行顺序为:supports() → handleEmptyBody()(可选)→ beforeBodyRead() → afterBodyRead():
| 方法名 | 入参说明 | 核心作用 | 执行时机 |
|---|---|---|---|
supports(...) |
methodParameter:目标方法参数;targetType:参数类型;converterType:使用的消息转换器 |
判断当前 Advice 是否对该请求参数生效(返回 true 则执行后续方法,false 则跳过) |
最前置,仅执行一次 |
handleEmptyBody(...) |
body:空请求体(如 POST 请求无请求体);inputMessage:请求消息流 |
处理空请求体场景(如返回默认对象,避免解析报错) | 仅当请求体为空时执行 |
beforeBodyRead(...) |
inputMessage:原始请求消息流(含请求体) |
拦截原始请求流,可修改请求体内容(如解密)、添加请求头信息 | HttpMessageConverter 解析请求体前执行 |
afterBodyRead(...) |
body:解析后的 Java 对象(如 User) |
对解析后的 Java 对象进行加工(如补充默认属性、自定义校验) | HttpMessageConverter 解析请求体后执行 |
2. 实现步骤(以 “请求入参解密” 为例)
假设前端将 JSON 入参加密为 Base64 字符串传输,后端需解密后再解析为 Java 对象,实现步骤如下:
步骤 1:自定义 RequestBodyAdvice 实现类
需继承 RequestBodyAdviceAdapter(RequestBodyAdvice 的默认适配器,无需重写所有方法,仅重写需要的方法):
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdviceAdapter;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
// @ControllerAdvice:全局生效(作用于所有 @RequestBody 参数)
public class DecryptRequestBodyAdvice extends RequestBodyAdviceAdapter {
/**
* 步骤1:判断是否生效(仅对标注 @Encrypt 注解的方法参数生效,可自定义条件)
*/
public boolean supports(MethodParameter methodParameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
// 自定义条件:仅处理添加了 @Encrypt 注解的参数(@Encrypt 为自定义注解)
return methodParameter.hasParameterAnnotation(Encrypt.class);
}
/**
* 步骤2:解析请求体前,解密请求体(核心逻辑)
*/
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
// 1. 读取原始加密请求体(Base64 字符串)
InputStream inputStream = inputMessage.getBody();
byte[] encryptedBytes = inputStream.readAllBytes();
String encryptedBody = new String(encryptedBytes, StandardCharsets.UTF_8);
// 2. 解密(Base64 解码,实际项目可替换为 AES/RSA 等加密算法)
byte[] decryptedBytes = Base64.getDecoder().decode(encryptedBody);
String decryptedBody = new String(decryptedBytes, StandardCharsets.UTF_8);
System.out.println("解密后的请求体:" + decryptedBody);
// 3. 返回新的 HttpInputMessage(包含解密后的请求体),供后续解析
return new HttpInputMessage() {
public InputStream getBody() throws IOException {
// 将解密后的字符串转为输入流
return new ByteArrayInputStream(decryptedBody.getBytes(StandardCharsets.UTF_8));
}
public HttpHeaders getHeaders() {
// 保留原始请求头
return inputMessage.getHeaders();
}
};
}
/**
* 步骤3:解析后的对象增强(可选,如补充默认属性)
*/
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
// 示例:给 User 对象补充默认的 createTime 属性
if (body instanceof User) {
User user = (User) body;
user.setCreateTime(System.currentTimeMillis());
return user;
}
return super.afterBodyRead(body, inputMessage, parameter, targetType, converterType);
}
// 自定义注解:标记需要加密的请求参数
public Encrypt {}
// 示例:目标 Java 对象
static class User {
private String name;
private Integer age;
private Long createTime; // 解密后补充的属性
// getter/setter + toString()
}
}
步骤 2:在 Controller 中使用
在需要解密的 @RequestBody 参数上添加自定义 @Encrypt 注解,触发 Advice:
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
public class DecryptController {
// 前端请求体:Base64 加密后的 JSON(如 "eyJuYW1lIjoi5byg5LiJIiwiYWdlIjoyNX0=" → 解密后 {"name":"张三","age":25})
public String receiveDecryptUser(
.Encrypt DecryptRequestBodyAdvice.User user) {
// 此时 user 已解密,且 createTime 已补充
System.out.println("最终处理的用户:" + user);
// 输出:User(name=张三, age=25, createTime=1719888888888)
return "接收成功:" + user.getName();
}
}
3. 关键注意事项
supports()条件控制:务必通过该方法限制 Advice 的生效范围(如按注解、参数类型过滤),避免全局生效导致不必要的性能损耗;- 请求流重复读取:
beforeBodyRead()中读取原始inputMessage.getBody()后,需通过ByteArrayInputStream重新包装流,否则后续HttpMessageConverter会因流已关闭而无法读取; - 异常处理:解密过程中若出现异常(如密文格式错误),需抛出
HttpMessageNotReadableException,Spring MVC 会自动转为 400 错误响应。
ResponseBodyAdvice:响应出参增强(@ResponseBody 预处理)
ResponseBodyAdvice 作用于标注 @ResponseBody 或 @RestController 的 Controller 方法返回值,在返回值被 HttpMessageConverter 写入响应流前进行拦截处理,核心是 “统一加工响应数据,确保输出格式符合前端预期”(如统一响应格式、响应加密、日志记录)。
1. 接口核心方法解析
ResponseBodyAdvice<T> 是泛型接口(T 为响应数据类型),包含 2 个核心方法,执行顺序为:supports() → beforeBodyWrite():
| 方法名 | 入参说明 | 核心作用 | 执行时机 |
|---|---|---|---|
supports(...) |
returnType:目标方法返回值类型;converterType:使用的消息转换器 |
判断当前 Advice 是否对该响应生效(返回 true 则执行 beforeBodyWrite(),false 则跳过) |
前置判断,仅执行一次 |
beforeBodyWrite(...) |
body:方法返回的原始 Java 对象;selectedContentType:响应媒体类型(如 application/json);request/response:请求 / 响应对象 |
加工原始响应对象(如统一封装格式、加密),返回加工后的对象供后续写入响应流 | HttpMessageConverter 写入响应流前执行 |
2. 实现步骤(以 “统一响应格式” 为例)
前后端分离项目中,通常要求所有接口返回统一格式(如 {code: 0, msg: "success", data: {...}}),而非直接返回业务数据(如 User 对象)。通过 ResponseBodyAdvice 可全局实现该需求:
步骤 1:定义统一响应格式类
import lombok.Data;
// 统一响应格式
public class CommonResponse<T> {
// 状态码:0-成功,非0-失败(如 1001-参数错误,1002-权限不足)
private int code;
// 提示信息
private String msg;
// 业务数据(成功时返回,失败时为 null)
private T data;
// 时间戳
private long timestamp;
// 成功构造器(默认 code=0,msg="success")
public static <T> CommonResponse<T> success(T data) {
CommonResponse<T> response = new CommonResponse<>();
response.setCode(0);
response.setMsg("success");
response.setData(data);
response.setTimestamp(System.currentTimeMillis());
return response;
}
// 失败构造器(自定义 code 和 msg)
public static <T> CommonResponse<T> fail(int code, String msg) {
CommonResponse<T> response = new CommonResponse<>();
response.setCode(code);
response.setMsg(msg);
response.setData(null);
response.setTimestamp(System.currentTimeMillis());
return response;
}
}
步骤 2:自定义 ResponseBodyAdvice 实现类
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;
// @ControllerAdvice:全局生效(作用于所有 @ResponseBody 响应)
public class CommonResponseAdvice implements ResponseBodyAdvice<Object> {
/**
* 步骤1:判断是否生效(排除 CommonResponse 本身,避免循环封装)
*/
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
// 条件:1. 响应类型不是 CommonResponse(避免重复封装);2. 方法标注了 @ResponseBody 或类标注了 @RestController
return !returnType.getParameterType().equals(CommonResponse.class)
&& (returnType.hasMethodAnnotation(ResponseBody.class)
|| returnType.getContainingClass().isAnnotationPresent(RestController.class));
}
/**
* 步骤2:统一封装响应格式(核心逻辑)
*/
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
// 1. 处理正常响应(body 不为 null)
if (body != null) {
return CommonResponse.success(body);
}
// 2. 处理无返回值响应(如 Controller 方法返回 void)
return CommonResponse.success(null);
}
}
步骤 3:在 Controller 中使用(无需修改原有代码)
原有 Controller 方法直接返回业务数据,Advice 会自动封装为统一格式:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.List;
public class UserController {
// 原始返回:User 对象 → 经过 Advice 后返回 CommonResponse<User>
public User getUser() {
User user = new User();
user.setName("张三");
user.setAge(25);
return user;
}
// 原始返回:List<User> → 经过 Advice 后返回 CommonResponse<List<User>>
public List<User> getUserList() {
User user1 = new User("张三", 25);
User user2 = new User("李四", 30);
return Arrays.asList(user1, user2);
}
// 原始返回:void → 经过 Advice 后返回 CommonResponse<Void>
public void emptyResponse() {
// 业务逻辑(如删除操作)
}
// User 类(简单业务对象)
static class User {
private String name;
private Integer age;
// 构造器、getter/setter
public User() {}
public User(String name, Integer age) {
this.name = name;
this.age = age;
}
}
}
步骤 4:最终响应效果
访问/user,响应:
{ "code": 0, "msg": "success", "data": {"name": "张三", "age": 25}, "timestamp": 1719888888888 }访问/user/list,响应:
{ "code": 0, "msg": "success", "data": [{"name": "张三", "age": 25}, {"name": "李四", "age": 30}], "timestamp": 1719888888888 }
3. 关键注意事项
排除重复封装:
supports()中务必排除CommonResponse类型,否则会出现 “CommonResponse<CommonResponse<User>>” 的循环封装问题;处理 String 类型响应:若 Controller 方法直接返回String(如return “success”),beforeBodyWrite()中需特殊处理(因StringHttpMessageConverter优先于MappingJackson2HttpMessageConverter,直接返回CommonResponse会被当作字符串处理),解决方案是手动将CommonResponse转为 JSON 字符串:
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { // 处理 String 类型响应 if (body instanceof String) { try { // 使用 Jackson 将 CommonResponse 转为 JSON 字符串 return new ObjectMapper().writeValueAsString(CommonResponse.success(body)); } catch (JsonProcessingException e) { throw new RuntimeException("String 响应格式转换失败", e); } } // 其他类型正常封装 return CommonResponse.success(body); }异常响应兼容:若配合全局异常处理器(
@ControllerAdvice + @ExceptionHandler),需让异常响应也返回CommonResponse格式,确保全局响应格式统一。
RequestBodyAdvice vs ResponseBodyAdvice 对比
| 维度 | RequestBodyAdvice | ResponseBodyAdvice |
|---|---|---|
| 作用对象 | @RequestBody 标注的方法参数 | @ResponseBody/@RestController 标注的方法返回值 |
| 核心目标 | 加工请求入参(解密、补充属性、校验) | 加工响应出参(统一格式、加密、日志) |
| 执行时机 | HttpMessageConverter 解析请求体前 / 后 | HttpMessageConverter 写入响应流前 |
| 方法数量 | 4 个(supports/handleEmptyBody/beforeBodyRead/afterBodyRead) | 2 个(supports/beforeBodyWrite) |
| 核心挑战 | 处理请求流重复读取问题 | 处理 String 类型响应的 JSON 转换问题 |
| 常用场景 | 入参加密解密、入参日志、参数校验增强 | 统一响应格式、响应加密、响应日志 |
进阶场景:组合使用与优先级控制
1. 多 Advice 组合使用
Spring MVC 支持同时注册多个 RequestBodyAdvice/ResponseBodyAdvice,执行顺序由 @Order 注解控制(值越小,优先级越高)。
示例:同时实现 “入参日志记录” 和 “入参解密”:
// 优先级 1:入参日志记录(先执行,记录原始密文)
public class LogRequestBodyAdvice extends RequestBodyAdviceAdapter {
public boolean supports(MethodParameter methodParameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
return methodParameter.hasParameterAnnotation(RequestBody.class);
}
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
// 记录原始请求体日志
byte[] bodyBytes = inputStream.readAllBytes();
System.out.println("原始请求体:" + new String(bodyBytes, StandardCharsets.UTF_8));
// 重新包装流
return new HttpInputMessage() {
public InputStream getBody() throws IOException {
return new ByteArrayInputStream(bodyBytes);
}
public HttpHeaders getHeaders() {
return inputMessage.getHeaders();
}
};
}
}
// 优先级 2:入参解密(后执行,基于原始日志后的流解密)
public class DecryptRequestBodyAdvice extends RequestBodyAdviceAdapter {
// 实现解密逻辑(同上)
}
2. 按包 / 注解限制生效范围
通过 @ControllerAdvice 的属性(basePackages/annotations),可限制 Advice 仅作用于指定范围的 Controller:
// 仅作用于 com.example.admin 包下的 Controller
public class AdminRequestBodyAdvice extends RequestBodyAdviceAdapter {
// 仅处理 admin 模块的请求入参
}
// 仅作用于标注了 @Api 注解的 Controller
public class ApiResponseBodyAdvice implements ResponseBodyAdvice<Object> {
// 仅处理 API 接口的响应
}
总结
RequestBodyAdvice 和 ResponseBodyAdvice 是 Spring MVC 中处理请求 / 响应增强的 “利器”,它们弥补了传统拦截器的不足,实现了Java 对象级别的数据加工,核心价值在于:
- 解耦横切关注点:将加密解密、日志记录、格式封装等逻辑从业务代码中剥离,降低耦合;
- 全局统一处理:一次配置,全局生效,避免重复代码;
- 灵活可控:通过
supports()和@Order精确控制生效范围和执行顺序