1.问题
一个项目里利用ResponseAdvisor对Controller的响应结果做了统一封装,代码如下;
@RestControllerAdvice(basePackages = "com.restkeeper")
public class ResponseAdvisor implements ResponseBodyAdvice<Object> {
/**
* 判断哪些需要拦截
*/
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
/**
* 返回结果包装
*
* @param body controller返回的数据
* @param returnType
* @param selectedContentType
* @param selectedConverterType
* @param request
* @param response
* @return
*/
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
Object result = null;
if (body instanceof Result) {
result = body;
}else if (body instanceof Boolean) {
result = new BaseResponse<Boolean>((boolean) body);
}else if (body instanceof PageVO) {
result = new BaseResponse<>(body);
}else if (body instanceof ExceptionResponse) {
result = new BaseResponse<>(400, ((ExceptionResponse) body).getMsg());
}else {
result = new BaseResponse<>(body);
}
return result;
}
}
有位同事写了一个接口,返回的数据是String,按照一般情况前端接受到的应该是data属性为接口返回值的BaseResponse对象,但实际情况是前端接收到异常的报错的信息。
{
"success": false,
"message": "com.restkeeper.response.BaseResponse cannot be cast to java.lang.String",
"code": 400
}
2.分析
先看了眼日志,可以看到接口返回的数据确实先被封装成统一的BaseResponse,但在后续执行writing方法时报了类型转换错误。
注意看这两行,一个是 Using ‘application/json’,另一个是Using ‘text/html’,所以一开始以为是这个地方出了问题。
开始debug,研究这个地方为什么不一样,相关源码在类AbstractMessageConverterMethodProcessor的writeWithMessageConverters方法中,看名字就知道是转换返回到前端的数据类型的,主要看这几行代码
HttpServletRequest request = inputMessage.getServletRequest();
//获取接口发起端能接受的数据类型
List<MediaType> acceptableTypes = getAcceptableMediaTypes(request);
而getAcceptableMediaTypes方法最终会调用resolveMediaTypes方法,
public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException {
for (ContentNegotiationStrategy strategy : this.strategies) {
List<MediaType> mediaTypes = strategy.resolveMediaTypes(request);
if (mediaTypes.equals(MEDIA_TYPE_ALL_LIST)) {
continue;
}
return mediaTypes;
}
return MEDIA_TYPE_ALL_LIST;
}
这里最终获取的是接口调用时设置的accept请求头,我是在浏览器上直接访问的,所以获取的也就是默认值。
可以看到accept里有很多个媒体数据类型,然后执行下面的代码根据各种类型的q值等条件选择其中一个。
for (MediaType mediaType : mediaTypesToUse) {
if (mediaType.isConcrete()) {
selectedMediaType = mediaType;
break;
}
else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) {
selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
break;
}
}
可以看到最后选的是text/html
既然是accept请求头的问题,那么很简单,在发起请求的时候手动设置下即可,然后就用postman重新测试,selectMediaType已经变为application/json。
然而依然还是报类型转换错误
只能继续往下调试,如图
遍历SpringMVC提供了类型转换器,执行canWrite方法判断是否能处理接口返回值,如果可以继续往下执行
valueType的值为String,ByteArrayHttpMessageConverter不能处理,遍历到第二个转换器StringHttpMessageConverter,往下执行,下面这段代码实际上就是执行我们实现的ResponseAdvisor的beforeBodyWrite方法。
继续向下执行,执行converter.write方法的时候报了类型转换异常,“com.restkeeper.response.BaseResponse cannot be cast to java.lang.String”。
3.解决
调试到这其实问题已经很明显了,就是因为接口的返回值是String类型,所以在向前端输出响应结果是选择了StringHttpMessageConverter转换器,然后由于项目做了统一的结果封装,执行到这的时候body值已经是BaseResponse对象,然后在类型转换时抛出异常。
解决办法也很简单,既然这里需要String类型的body,那么在封装响应结果时再多加一个判断,在beforeBodyWrite方法最后加上下面一端代码即可。
if (body instanceof String) {
//这里注意是将result转换为json字符串
result = JSON.toJSONString(result);
}
重新调试,响应结果就正常了。
{
"code": 200,
"data": "Hello Nacos Discovery sayhi, i am from port 8083",
"message": "success",
"success": true
}
在网上看到有人直接把MessageConverters里的StringHttpMessageConverter删除了,看自己的选择。
@Configuration
public class MyWebmvcConfiguration implements WebMvcConfigurer {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
//删除springboot默认的StringHttpMessageConverter解析器
converters.removeIf(x -> x instanceof StringHttpMessageConverter);
}
}