通常我们在 Spring Boot 的项目中,会使用一个类来作为统一的接口返回,比如这样:

import lombok.Data;

@Data
public class Result<T> {
private int code;
private String message;
private T data;

public static <T> Result<T> success(T t) {
Result<T> result = new Result<>();
result.code = 200;
result.message = “ok”;
result.data = t;
return result;
}

public static Result<Object> failed(String message) {
Result<Object> result = new Result<>();
result.code = 500;
result.message = message;
return result;
}
}

 
但是我们又不想在每一个接口都写一遍Result.success(obj)这样的代码,比如这样:

@GetMapping(“test”)
public Result<String> test() {
return Result.success(“ok”);
}
 
这个时候,我们就可以使用org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice#beforeBodyWrite来实现统一的接口返回类型。

使用方法也特别简单,如下所示:

@RestControllerAdvice
@Slf4j
public class WebAdvice implements ResponseBodyAdvice<Object> {

@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);
}
}
 
这段代码的逻辑也特别简单,就是判断接口的返回值是不是Result类型,不是的话就用Result包装后进行返回,这样就达到了统一的接口返回类型。

当我们测试普通的引用类型时,是没有什么问题的。

但是当我们要返回的值类型为String类型时,就会发生异常:

java.lang.ClassCastException: com.xx.Result cannot be cast to java.lang.String
at org.springframework.http.converter.StringHttpMessageConverter.getContentLength(StringHttpMessageConverter.java:44) ~[spring-web-5.1.16.RELEASE.jar:5.1.16.RELEASE]
at org.springframework.http.converter.AbstractHttpMessageConverter.addDefaultHeaders(AbstractHttpMessageConverter.java:260) ~[spring-web-5.1.16.RELEASE.jar:5.1.16.RELEASE]
at org.springframework.http.converter.AbstractHttpMessageConverter.write(AbstractHttpMessageConverter.java:211) ~[spring-web-5.1.16.RELEASE.jar:5.1.16.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:294) ~[spring-webmvc-5.1.16.RELEASE.jar:5.1.16.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.handleReturnValue(RequestResponseBodyMethodProcessor.java:181) ~[spring-webmvc-5.1.16.RELEASE.jar:5.1.16.RELEASE]
at org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite.handleReturnValue(HandlerMethodReturnValueHandlerComposite.java:82) ~[spring-web-5.1.16.RELEASE.jar:5.1.16.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:123) ~[spring-webmvc-5.1.16.RELEASE.jar:5.1.16.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:893) ~[spring-webmvc-5.1.16.RELEASE.jar:5.1.16.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:798) ~[spring-webmvc-5.1.16.RELEASE.jar:5.1.16.RELEASE]
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.1.16.RELEASE.jar:5.1.16.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040) ~[spring-webmvc-5.1.16.RELEASE.jar:5.1.16.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943) ~[spring-webmvc-5.1.16.RELEASE.jar:5.1.16.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) [spring-webmvc-5.1.16.RELEASE.jar:5.1.16.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898) [spring-webmvc-5.1.16.RELEASE.jar:5.1.16.RELEASE]
 
现在我们通过源码来分析这个问题是怎么产生的
首先,我们要知道
浏览器传给服务器和服务器返回给浏览器的数据分别是输入流和输出流,
然后 spring 框架有个HttpMessageConverter类,就是专门用来处理流和接口的参数类型或返回值类型之间的转换的。方法也很好理解,就是判断能不能进行读操作,能的话就进行读操作;能不能进行写操作,能的话就进行写操作。

 

 

 现在,我们启动一个项目,跟踪断点进行排查。
首先我们要找到发生异常的地址,从刚才控制台打印的异常信息,我们可以找到这个地方,并打上一个断点

 

 

然后,我们通过该断点找到调用这个write的方法

 

 

我们可以发现这个converter是一个StringHttpMessageConverter类型,然后我们进入到addDefaultHeaders方法

 

 

然后,当代码执行到这个方法的 260 行时,就会发生刚才我们说的那个ClassCastException异常。

 

 

最后,我们终于找到了发生异常的原因,因为 260 的代码会执行getContentLength(t, headers.getContentType())这个方法,而这个方法会去执行StringHttpMessageConverter的getContentLength方法,如下图所示:

 

 

但是这个时候 t的类型已经被我们用beforeBodyWrite方法转为Result类型了,所以就发生了类型转换异常的错误。


我们再来说说这个异常为啥会发生。

首先,spring 框架默认给我们注入了一些转换器,如下图所示:

 

 

然后,我们通过源码就能发现,这个转换的执行过程是:
1.遍历所有的转换器,判断当前转换器能不能使用
2.如果可以使用,才调用我们写的beforeBodyWrite进行处理

 

 

最后,我们就发现了原来是因为处理过后的body已经从String类型转为Result类型,然后在调用实现类即StringHttpMessageConverter#getContentLength(String str, @Nullable MediaType contentType)方法时,第一个参数str发生了类型转换错误的异常。

最后,我们来说说怎么解决这个问题
1.第一种方式
就是我们在我们自己写的beforeBodyWrite的方法中,将返回值用 Result 包装过后再将Result 转为 String 类型进行返回。

@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if (body instanceof String) {
ObjectMapper om = new ObjectMapper();
return om.writeValueAsString(Result.success(body));
}
if (body instanceof Result) {
return body;
}

return Result.success(body);
}
 
2.第二种方式,处理 spring 框架提供的转换器
如下代码中的方式,任选其一即可。

@Configuration
public class WebConfiguration implements WebMvcConfigurer {

@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// 第一种方式是将 json 处理的转换器放到第一位,使得先让 json 转换器处理返回值,这样 String转换器就处理不了了。
// converters.add(0, new MappingJackson2HttpMessageConverter());
// 第二种就是把String类型的转换器去掉,不使用String类型的转换器
// converters.removeIf(httpMessageConverter -> httpMessageConverter.getClass() == StringHttpMessageConverter.class);
}
}
————————————————
版权声明:本文为CSDN博主「一梦喂马.」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_37170583/article/details/107470337