Spring 统一数据返回格式是使用 Spring 进行开发时很常用的一个功能,但是当其处理返回类型原先为 String 类型的时候就会出错报错,需要我们额外对 String 类型进行处理。
例如:现在我开发一个项目,项目中我想要统一返回下述的 Result 类型:
@Data
public class Result {
private ResultCodeEnum code;
private String errMsg;
private Object data;
public static Result success(Object data) {
Result result = new Result();
result.setCode(ResultCodeEnum.SUCCESS);
result.setErrMsg("");
result.setData(data);
return result;
}
}
此时我就使用到 Spring 的统一数据返回格式功能:
@ControllerAdvice
public class ResponseAdvice 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) {
if(body instanceof Result) {
return body;
}
return Result.success(body);
}
}
但是当我处理原本的返回类型为 String 的时候就出现以下报错:
@RestController
@RequestMapping("/test")
public class TestController {
@RequestMapping("/m1")
public String m1() {
return "hello";
}
}
其中涉及到的原因就得通过查看 Spring 的源码来进行分析:
SpringMVC 在进行初始化的时候,默认会注册⼀些⾃带的 HttpMessageConverter,MessageConverter 意思是转换器。并且会把它们存进一个名为 messageConverters 的 List 中,因此这些 HttpMessageConverter 存储起来是有先后顺序的,排列之后分别为:
1)ByteArrayHttpMessageConverter
2)StringHttpMessageConverter
3)SourceHttpMessageConverter
4)AllEncompassingFormHttpMessageConverter
这些从 RequestMappingHandlerAdapter 类中的构造方法即可知道:
( 该类正是对应着 SpringMVC 经常使用的路由映射 @RequestMapping 注解 )
其中 AllEncompassingFormHttpMessageConverter 会根据项⽬依赖情况添加对应的 HttpMessageConverter :
在依赖中引⼊jackson相关的 jar 包信息后,容器会把 MappingJackson2HttpMessageConverter ⾃动注册到 messageConverters 这个 List 的末尾。
Spring 会根据我们方法的返回值类型,从 messageConverters 中按顺序找到合适的 HttpMessageConverter 来进行转换。
因此报错原因就很容易找到了:由于 messageConverters 中,String 类的转换器比 JSON 数据格式对应的转换器顺序较前,因此当返回的数据是 String 时, StringHttpMessageConverter 会先被遍历到,这时会使用 StringHttpMessageConverter 。但是实际我们是需要封装成为一个 Result 类来进行返回,而 Spring 对于返回值为对象类型的都会处理返回成 JSON 字符串,因而应该对应的是 JSON 格式数据的转换器 MappingJackson2HttpMessageConverter 。由于转换器使用上出现了错误,势必就导致了使用时出现报错:
我们需要在 AbstractMessageConverterMethodProcessor 这个类中找到 writeWithMessageConverters 这个方法。我们使用统一返回数据格式处理这个功能时,所实现的 ResponseBodyAdvice 接口,具体实现逻辑就是在这个方法内:
找到这个方法中如下图所示的代码处:
上图中标识处就是使用 getAdvice 获取我们最开始代码中定义的 ResponseAdvice 类,并调用我们在该类中实现的 beforeBodyWrite 方法。调用之后一直来到下图中的蓝色标记处就出现了报错:
图中红色标记处,执行完 beforeBodyWrite 方法之后,body 就变成了 Result 类型( 因为我们在beforeBodyWrite 中定义好返回值就是 Result 类型 )。
因而到了蓝色标记处,调用父类( HttpMessageConverter )的 write 方法时,传入的 body 类型就是 Result 类型。
进入 write 方法,并选择 AbstractHttpMessageConverter 所实现的那个:
在这个方法中,使用的是泛型来接收我们传入的 body 参数,因此形参 t 也是 Result 类型,然后再调用 addDefaultHeaders 方法时传入 t 。
进入 addDefaultHeaders 方法,由于 AbstractHttpMessageConverter 的子类 StringHttpMessageConverter 重写了该方法,因此当调用该方法的时候,调用的是子类重写之后的方法( 因为是 StringHttpMessageConverter 引用来进行调用 ):
下述遍历 messageConverters 这个 List ,根据前面所说先遍历到了 StringHttpMessageConverter ,因此此时第二个红色标记处的 converter 指向的是 StringHttpMessageConverter 引用,因此是使用 StringHttpMessageConverter 引用来调用 write 方法,进而 write 内也是使用 StringHttpMessageConverter 引用来调用 addDefaultHeaders 方法。
从上图我们就知道了报错原因:StringHttpMessageConverter 类实现的 addDefaultHeaders 方法的第二个参数是使用 String 类型来进行接收,但是调用处传入的第二个参数 t,根据我们的一步步推断,是一个 Result 类型,类型不匹配,因此出现报错。
因此我们只需对返回值为 String 类型的进行特殊处理即可,使其正常使用 JSON 数据格式对应的转换器而不去使用 String 类型的转换器,类型就不会匹配错误,就不会报错:
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
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) {
if(body instanceof Result) {
return body;
}
if(body instanceof String) {
objectMapper.writeValueAsBytes(body);
}
return Result.success(body);
}
}
上述代码中,判断了如果 body 是指向了 String 类型引用,就使用 Spring 内置的 ObjectMapping 对象将其转化为 JSON 字符串再返回。
从 AllEncompassingFormHttpMessageConverter 的构造方法中可以看到,如果返回值是一个 JSON 字符串,则会使用 JSON 字符串对应的转换器,问题解决。
解析重点概括如下: