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 的默认适配器,无需重写所有方法,仅重写需要的方法):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
| 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 public class DecryptRequestBodyAdvice extends RequestBodyAdviceAdapter {
@Override public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { return methodParameter.hasParameterAnnotation(Encrypt.class); }
@Override public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException { InputStream inputStream = inputMessage.getBody(); byte[] encryptedBytes = inputStream.readAllBytes(); String encryptedBody = new String(encryptedBytes, StandardCharsets.UTF_8);
byte[] decryptedBytes = Base64.getDecoder().decode(encryptedBody); String decryptedBody = new String(decryptedBytes, StandardCharsets.UTF_8); System.out.println("解密后的请求体:" + decryptedBody);
return new HttpInputMessage() { @Override public InputStream getBody() throws IOException { return new ByteArrayInputStream(decryptedBody.getBytes(StandardCharsets.UTF_8)); }
@Override public HttpHeaders getHeaders() { return inputMessage.getHeaders(); } }; }
@Override public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { if (body instanceof User) { User user = (User) body; user.setCreateTime(System.currentTimeMillis()); return user; } return super.afterBodyRead(body, inputMessage, parameter, targetType, converterType); }
@Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) public @interface Encrypt {}
static class User { private String name; private Integer age; private Long createTime; } }
|
步骤 2:在 Controller 中使用
在需要解密的 @RequestBody 参数上添加自定义 @Encrypt 注解,触发 Advice:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController;
@RestController public class DecryptController {
@PostMapping("/decrypt/user") public String receiveDecryptUser( @RequestBody @DecryptRequestBodyAdvice.Encrypt DecryptRequestBodyAdvice.User user) { System.out.println("最终处理的用户:" + user); 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:定义统一响应格式类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| import lombok.Data;
@Data public class CommonResponse<T> { private int code; private String msg; private T data; private long timestamp;
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; }
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 实现类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| 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 public class CommonResponseAdvice implements ResponseBodyAdvice<Object> {
@Override public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { return !returnType.getParameterType().equals(CommonResponse.class) && (returnType.hasMethodAnnotation(ResponseBody.class) || returnType.getContainingClass().isAnnotationPresent(RestController.class)); }
@Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { if (body != null) { return CommonResponse.success(body); }
return CommonResponse.success(null); } }
|
步骤 3:在 Controller 中使用(无需修改原有代码)
原有 Controller 方法直接返回业务数据,Advice 会自动封装为统一格式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Arrays; import java.util.List;
@RestController public class UserController {
@GetMapping("/user") public User getUser() { User user = new User(); user.setName("张三"); user.setAge(25); return user; }
@GetMapping("/user/list") public List<User> getUserList() { User user1 = new User("张三", 25); User user2 = new User("李四", 30); return Arrays.asList(user1, user2); }
@GetMapping("/user/empty") public void emptyResponse() { }
static class User { private String name; private Integer age; public User() {} public User(String name, Integer age) { this.name = name; this.age = age; } } }
|
步骤 4:最终响应效果
访问/user,响应:
1 2 3 4 5 6
| { "code": 0, "msg": "success", "data": {"name": "张三", "age": 25}, "timestamp": 1719888888888 }
|
访问/user/list,响应:
1 2 3 4 5 6
| { "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 字符串:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { if (body instanceof String) { try { 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| @Order(1) @ControllerAdvice public class LogRequestBodyAdvice extends RequestBodyAdviceAdapter { @Override public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { return methodParameter.hasParameterAnnotation(RequestBody.class); }
@Override 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() { @Override public InputStream getBody() throws IOException { return new ByteArrayInputStream(bodyBytes); } @Override public HttpHeaders getHeaders() { return inputMessage.getHeaders(); } }; } }
@Order(2) @ControllerAdvice public class DecryptRequestBodyAdvice extends RequestBodyAdviceAdapter { }
|
2. 按包 / 注解限制生效范围
通过 @ControllerAdvice 的属性(basePackages/annotations),可限制 Advice 仅作用于指定范围的 Controller:
1 2 3 4 5 6 7 8 9 10 11
| @ControllerAdvice(basePackages = "com.example.admin") public class AdminRequestBodyAdvice extends RequestBodyAdviceAdapter { }
@ControllerAdvice(annotations = Api.class) public class ApiResponseBodyAdvice implements ResponseBodyAdvice<Object> { }
|
总结
RequestBodyAdvice 和 ResponseBodyAdvice 是 Spring MVC 中处理请求 / 响应增强的 “利器”,它们弥补了传统拦截器的不足,实现了Java 对象级别的数据加工,核心价值在于:
- 解耦横切关注点:将加密解密、日志记录、格式封装等逻辑从业务代码中剥离,降低耦合;
- 全局统一处理:一次配置,全局生效,避免重复代码;
- 灵活可控:通过
supports() 和 @Order 精确控制生效范围和执行顺序