0%

RequestBodyAdvice和ResponseBodyAdvice

Spring MVC 之 RequestBodyAdvice 与 ResponseBodyAdvice:请求 / 响应增强的核心实践

在 Spring MVC 4.0+ 中,RequestBodyAdviceResponseBodyAdvice 是两个强大的请求 / 响应增强接口,分别用于在 @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 实现类

需继承 RequestBodyAdviceAdapterRequestBodyAdvice 的默认适配器,无需重写所有方法,仅重写需要的方法):

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:全局生效(作用于所有 @RequestBody 参数)
@ControllerAdvice
public class DecryptRequestBodyAdvice extends RequestBodyAdviceAdapter {

/**
* 步骤1:判断是否生效(仅对标注 @Encrypt 注解的方法参数生效,可自定义条件)
*/
@Override
public boolean supports(MethodParameter methodParameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
// 自定义条件:仅处理添加了 @Encrypt 注解的参数(@Encrypt 为自定义注解)
return methodParameter.hasParameterAnnotation(Encrypt.class);
}

/**
* 步骤2:解析请求体前,解密请求体(核心逻辑)
*/
@Override
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() {
@Override
public InputStream getBody() throws IOException {
// 将解密后的字符串转为输入流
return new ByteArrayInputStream(decryptedBody.getBytes(StandardCharsets.UTF_8));
}

@Override
public HttpHeaders getHeaders() {
// 保留原始请求头
return inputMessage.getHeaders();
}
};
}

/**
* 步骤3:解析后的对象增强(可选,如补充默认属性)
*/
@Override
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);
}

// 自定义注解:标记需要加密的请求参数
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Encrypt {}

// 示例:目标 Java 对象
static class User {
private String name;
private Integer age;
private Long createTime; // 解密后补充的属性
// getter/setter + toString()
}
}
步骤 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 {

// 前端请求体:Base64 加密后的 JSON(如 "eyJuYW1lIjoi5byg5LiJIiwiYWdlIjoyNX0=" → 解密后 {"name":"张三","age":25})
@PostMapping("/decrypt/user")
public String receiveDecryptUser(
@RequestBody @DecryptRequestBodyAdvice.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:定义统一响应格式类
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> {
// 状态码: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 实现类
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:全局生效(作用于所有 @ResponseBody 响应)
@ControllerAdvice
public class CommonResponseAdvice implements ResponseBodyAdvice<Object> {

/**
* 步骤1:判断是否生效(排除 CommonResponse 本身,避免循环封装)
*/
@Override
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:统一封装响应格式(核心逻辑)
*/
@Override
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 会自动封装为统一格式:

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 {

// 原始返回:User 对象 → 经过 Advice 后返回 CommonResponse<User>
@GetMapping("/user")
public User getUser() {
User user = new User();
user.setName("张三");
user.setAge(25);
return user;
}

// 原始返回:List<User> → 经过 Advice 后返回 CommonResponse<List<User>>
@GetMapping("/user/list")
public List<User> getUserList() {
User user1 = new User("张三", 25);
User user2 = new User("李四", 30);
return Arrays.asList(user1, user2);
}

// 原始返回:void → 经过 Advice 后返回 CommonResponse<Void>
@GetMapping("/user/empty")
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,响应:

    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) {
    // 处理 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
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
// 优先级 1:入参日志记录(先执行,记录原始密文)
@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();
}
};
}
}

// 优先级 2:入参解密(后执行,基于原始日志后的流解密)
@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
// 仅作用于 com.example.admin 包下的 Controller
@ControllerAdvice(basePackages = "com.example.admin")
public class AdminRequestBodyAdvice extends RequestBodyAdviceAdapter {
// 仅处理 admin 模块的请求入参
}

// 仅作用于标注了 @Api 注解的 Controller
@ControllerAdvice(annotations = Api.class)
public class ApiResponseBodyAdvice implements ResponseBodyAdvice<Object> {
// 仅处理 API 接口的响应
}

总结

RequestBodyAdviceResponseBodyAdvice 是 Spring MVC 中处理请求 / 响应增强的 “利器”,它们弥补了传统拦截器的不足,实现了Java 对象级别的数据加工,核心价值在于:

  1. 解耦横切关注点:将加密解密、日志记录、格式封装等逻辑从业务代码中剥离,降低耦合;
  2. 全局统一处理:一次配置,全局生效,避免重复代码;
  3. 灵活可控:通过 supports()@Order 精确控制生效范围和执行顺序

欢迎关注我的其它发布渠道