Appearance
Spring MVC 统一包装返回结构
1. 背景介绍
在项目开发中,我们希望接口返回统一的结构体:
java
@Data
@AllArgsConstructor
@NoArgsConstructor
public class R<T> {
private int code;
private String msg;
private T data;
public static <T> R ok(){
return new R<T>(1, "success", null);
}
public static <T> R ok(T data){
return new R<T>(1, "success", data);
}
public static <T> R error(String msg){
return new R<T>(-1, msg, null);
}
public static <T> R error(String msg, T data){
return new R<T>(-1, msg, data);
}
public static <T> R error(int code, String msg, T data){
return new R<T>(code, msg, data);
}
}当然,我们可以在Controller的方法中显式地声明返回值类型为R:
java
@RestController
public class TestController {
@GetMapping("/calc")
public R calc(@RequestParam("num1") int num1,
@RequestParam("num2") int num2)
{
int result = num1 / num2;
return R.ok(result);
}
}但是,使用上面的方法,会导致每个方法的返回值都是R,对于代码理解不直观。我们希望方法直接返回目标类型,例如:
java
@RestController
public class GoodsController {
@GetMapping("/goods/{id}")
public Goods getGoodsById(@PathVariable("id") int id) {
Goods goods = new Goods(
id,
"1001",
"手机",
"部",
"12GB+1TB",
new BigDecimal(6000),
new BigDecimal(4800),
1000
);
return goods;
}
}如上,在Controller的方法中直接返回数据类型(Goods),然后通过其他方法包装为R。
2. 解决方案
我们可以通过实现ResponseBodyAdvice接口来达到上述目的:
java
@ControllerAdvice
public class GlobalResponseHandlerBasic implements ResponseBodyAdvice {
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
@Override
public Object beforeBodyWrite(Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class selectedConverterType,
ServerHttpRequest request,
ServerHttpResponse response) {
return R.ok(body);
}
}该接口有两个方法:
supports(MethodParameter returnType, Class converterType):表示可以处理的方法。此处表示默认处理所有Controller接口方法。beforeBodyWrite(...):实际处理方法,用于将Controller方法的返回值进行包装,第一个参数Object object就是Controller方法的返回值。 注意,ResponseBodyAdvice接口的实现需要加注解@ControllerAdvice,表示用于增强Controller。
此时,我们请求/goods/{id}接口,返回如下:

可以看到接口返回内容已经统一为R了。但是,此时还有一个问题:

如果Controller中的接口方法返回R,那么再包一层就会造成结构冗余。所以,我们可以在supports()方法中进行判断,如果接口方法已经返回R了,那么就不要进行包装:
java
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
Class<?> returnClasstype = returnType.getMethod().getReturnType();
if(returnClasstype == R.class) {
return false;
}
return true;
}
3. 和@JsonView结合的问题
我们先在Goods类中定义视图,然后在接口方法中使用视图:
java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Goods {
@JsonView(InnerJsonView.class)
private int id;
@JsonView(OuterJsonView.class)
private String goodsCode;
@JsonView(OuterJsonView.class)
private String goodsName;
@JsonView(OuterJsonView.class)
private String unit;
@JsonView(OuterJsonView.class)
private String spec;
@JsonView(OuterJsonView.class)
private BigDecimal price;
// 成本价
@JsonView(InnerJsonView.class)
private BigDecimal costPrice;
// 库存
@JsonView(OuterJsonView.class)
private int stock;
public static class OuterJsonView{}
public static class InnerJsonView extends OuterJsonView{}
}java
@GetMapping("/goods/{id}")
@JsonView(Goods.OuterJsonView.class)
public Goods getGoodsById(@PathVariable("id") int id) {
Goods goods = new Goods(
id,
"1001",
"手机",
"部",
"12GB+1TB",
new BigDecimal(6000),
new BigDecimal(4800),
1000
);
return goods;
}之后请求接口,结果如下:

发现结果为空!这显然是不对的。
4. 解决和@JsonView结合的问题
要解决和@JsonView结合的问题,需要我们自己定义一个注解来代替@JsonView,并在ResponseBodyAdvice接口实现中进行处理。
首先自定义一个注解来代替@jsonView,我们命名为@NestedJsonView:
java
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface NestedJsonView {
// 指定视图
Class<?> value() ;
}然后在ResponseBodyAdvice接口实现中进行处理:
java
@ControllerAdvice
public class GlobalResponseHandler implements ResponseBodyAdvice {
@Resource
private ObjectMapper objectMapper;
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
// 如果接口方法返回值类型为R,不再进行包装
Class<?> returnClassType = returnType.getMethod().getReturnType();
if (returnClassType == R.class)
return false;
return true;
}
@SneakyThrows
@Override
public Object beforeBodyWrite(Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class selectedConverterType,
ServerHttpRequest request,
ServerHttpResponse response) {
String json;
NestedJsonView methodAnnotation = returnType.getMethodAnnotation(NestedJsonView.class);
if(methodAnnotation != null && methodAnnotation.value() != null){
// 如果接口方法标注了NestedJsonView,则根据视图进行json序列化
json = objectMapper.writerWithView(methodAnnotation.value()).writeValueAsString(body);
}else{
// 如果接口方法没有标注NestedJsonView或NestedJsonView的值为空,则直接进行json序列化
json = objectMapper.writeValueAsString(body);
}
// 包装成统一返回结构体
return R.ok(json);
}
}然后再接口方法上标注自定义注解@NestedJsonView:
java
@GetMapping("/goods/{id}")
// @JsonView(Goods.OuterJsonView.class)
@NestedJsonView(Goods.OuterJsonView.class)
public Goods getGoodsById(@PathVariable("id") int id) {
Goods goods = new Goods(
id,
"1001",
"手机",
"部",
"12GB+1TB",
new BigDecimal(6000),
new BigDecimal(4800),
1000
);
return goods;
}**注意:**我们需要在统一返回结构体的数据字段中标注@JsonRawValue注解:
java
public class R<T> {
private int code;
private String msg;
@JsonRawValue
private T data;
}结果如下:

5. 原理分析
我们通过分析原理来回答为什么和@JsonView结合会出现问题。首先先看以下大概的流程图:

假设现在我们在接口方法中标注了@JsonView,并在ResponseBodyAdvice中统一包装返回值:
java
@GetMapping("/goods/{id}")
@JsonView(Goods.OuterJsonView.class)
public Goods getGoodsById(@PathVariable("id") int id) {
Goods goods = new Goods(
id,
"1001",
"手机",
"部",
"12GB+1TB",
new BigDecimal(6000),
new BigDecimal(4800),
1000
);
return goods;
}java
@Override
public Object beforeBodyWrite(Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class selectedConverterType,
ServerHttpRequest request,
ServerHttpResponse response) {
return R.ok(body);
}那么当请求接口/goods/1后,接口方法返回值是Goods对象:
txt
Goods(id=1, goodsCode=1001, goodsName=手机, unit=部, spec=12GB+1TB, price=6000, costPrice=4800, stock=1000)然后接口方法返回值进入RequestResponseBodyMethodProcessor的handleReturnValue方法进行处理。注意RequestResponseBodyMethodProcessor中存在字段advice,包括返回值处理器链responseBodyAdvice,其中就有我们定义的GlobalResponseHandlerBasic以及Spring自带的JsonViewResponseBodyAdvice。我们自定义的GlobalResponseHandlerBasic处在第一个。

在RequestResponseBodyMethodProcessor的handleReturnValue方法中跟踪writeWithMessageConverters方法,最终会到RequestResponseBodyAdviceChain的processBody方法:
java
private <T> Object processBody(@Nullable Object body, MethodParameter returnType, MediaType contentType,
Class<? extends HttpMessageConverter<?>> converterType,
ServerHttpRequest request, ServerHttpResponse response) {
for (ResponseBodyAdvice<?> advice : getMatchingAdvice(returnType, ResponseBodyAdvice.class)) {
if (advice.supports(returnType, converterType)) {
body = ((ResponseBodyAdvice<T>) advice).beforeBodyWrite((T) body, returnType,
contentType, converterType, request, response);
}
}
return body;
}可以看到该方法就是依次调用ResponseBodyAdvice的实现类。根据我们程序的顺序,先调用我们自定义的GlobalResponseHandlerBasic,包装成R后返回:
java
R(code=1, msg=success, data=Goods(id=1, goodsCode=1001, goodsName=手机, unit=部, spec=12GB+1TB, price=6000, costPrice=4800, stock=1000))然后调用JsonViewResponseBodyAdvice中的方法,由于我们的接口方法标注了@JsonView,所以会进行处理:
java
public class JsonViewResponseBodyAdvice extends AbstractMappingJacksonResponseBodyAdvice {
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
// 标注了JsonView注解
return super.supports(returnType, converterType) && returnType.hasMethodAnnotation(JsonView.class);
}
@Override
protected void beforeBodyWriteInternal(MappingJacksonValue bodyContainer, MediaType contentType,
MethodParameter returnType, ServerHttpRequest request, ServerHttpResponse response) {
JsonView ann = returnType.getMethodAnnotation(JsonView.class);
Assert.state(ann != null, "No JsonView annotation");
Class<?>[] classes = ann.value();
if (classes.length != 1) {
throw new IllegalArgumentException(
"@JsonView only supported for response body advice with exactly 1 class argument: " + returnType);
}
// 设置序列化视图
bodyContainer.setSerializationView(classes[0]);
}
}可以看到,JsonViewResponseBodyAdvice并没有直接序列化我们的返回结构R。
之后,包装后返回值继续走到AbstractJackson2HttpMessageConverter中的writeInternal方法进行处理:

可以看到使用了JsonViewResponseBodyAdvice中配置的视图Goods.OuterJsonView来序列化返回值R,所以结果为空!
6. 贴一下完整的全局返回值处理器
由于在data上标注了@JsonRawValue,所以如果接口方法直接返回R,也需要处理:
java
@ControllerAdvice
public class GlobalResponseHandler implements ResponseBodyAdvice {
@Resource
private ObjectMapper objectMapper;
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
@SneakyThrows
@Override
public Object beforeBodyWrite(Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class selectedConverterType,
ServerHttpRequest request,
ServerHttpResponse response) {
Class<?> returnClassType = returnType.getMethod().getReturnType();
// 如果返回String,那么返回类型也应该是String,否则会报错
// 因为@JsonRawValue,所以需要将字符串再包一层objectMapper.writeValueAsString(body)
if (returnClassType == String.class){
return objectMapper.writeValueAsString(R.ok(objectMapper.writeValueAsString(body)));
}
if (returnClassType == R.class){
R r = (R)body;
// 如果返回异常对象,会导致异常,并且会暴露内部实现,所以永远不要直接返回异常对象
if(r.getData() != null && r.getData() instanceof Throwable){
r.setData(null);
}
// 由于在data上标注了@jsonRawValue,所以需要将其内容转换为字符串
r.setData(objectMapper.writeValueAsString(r.getData()));
return r;
}
String json;
NestedJsonView methodAnnotation = returnType.getMethodAnnotation(NestedJsonView.class);
if(methodAnnotation != null && methodAnnotation.value() != null){
// 如果接口方法标注了NestedJsonView,则根据视图进行json序列化
json = objectMapper.writerWithView(methodAnnotation.value()).writeValueAsString(body);
}else{
// 如果接口方法没有标注NestedJsonView或NestedJsonView的值为空,则直接进行json序列化
json = objectMapper.writeValueAsString(body);
}
// 包装成统一返回结构体
return R.ok(json);
}
}注:如果接口方法直接返回字符串,建议在响应头中标注Content-Type:
java
@GetMapping(value = "/", produces = "application/json")
String main(){
return "Hello World";
}