写在前面
在开发中,经常会有一个自定义输出数据格式的场景,此时如果用到 ResponseBodyAdvice
做全局的数据格式控制,在响应纯字符串数据时可能会遇到某些奇怪的问题,本文描述了两个和 String
相关的问题的解决方案,内容可能和网上的其他有些不同,问题较简单,纯记录贴。
XXXX cannot be cast to java.lang.String
问题;- 响应的字符串携带双引号问题。
本文 SpringBoot
环境:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
</parent>
<!-- SpringCloud项目 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR8</version>
<type>pom</type>
<scope>import</scope>
</dependency>
本文场景:普通的 Controller
使用 ResponseBodyAdvice
进行全局格式控制,微服务间相互调用的部分接口没有进行格式控制,否则调用完还需要再进行一次格式解析,较麻烦。
1. 问题解决
先提供问题解决方案,后续再描述原理,ResponseBodyAdvice
类如下这么写即可(不需要调整消息转换器)。
package com.nineya.user.handler;
import com.alibaba.fastjson.JSONObject;
import com.nineya.tool.restful.ResponseResult;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
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;
/**
* @author 殇雪话诀别
* 2020/12/7
*/
@ControllerAdvice("com.nineya.user.controller.api")
public class ResponseResultHandler implements ResponseBodyAdvice {
@Override
public boolean supports(MethodParameter methodParameter, Class aClass) {
return true;
}
@Override
public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class aClass,
ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
serverHttpResponse.getHeaders().setContentType(MediaType.parseMediaType(MediaType.APPLICATION_JSON_VALUE));
ResponseResult result = ResponseResult.access(o);
if (o == null || o instanceof String) {
return JSONObject.toJSONString(result);
}
return result;
}
}
2. 问题解决步骤
- 首先遇到的是
.ResponseResult cannot be cast to java.lang.String
字符串类型转换问题,查看源码发现了如下这么一串代码,主要关注 3 点注释。
Object body;
Class<?> valueType;
Type targetType;
// 1. 此处判断 body 的数据类型
if (value instanceof CharSequence) {
body = value.toString();
valueType = String.class;
targetType = String.class;
}
else {
body = value;
valueType = getReturnValueType(body, returnType);
targetType = GenericTypeResolver.resolveType(getGenericType(returnType), returnType.getContainingClass());
}
省略亿点代码......
if (selectedMediaType != null) {
selectedMediaType = selectedMediaType.removeQualityValue();
for (HttpMessageConverter<?> converter : this.messageConverters) {
GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ?
(GenericHttpMessageConverter<?>) converter : null);
if (genericConverter != null ?
((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) :
converter.canWrite(valueType, selectedMediaType)) {
// 2. 此处调用 ResponseBodyAdvice 的代码进行格式控制
body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,
(Class<? extends HttpMessageConverter<?>>) converter.getClass(),
inputMessage, outputMessage);
if (body != null) {
Object theBody = body;
LogFormatUtils.traceDebug(logger, traceOn ->
"Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]");
addContentDispositionHeader(inputMessage, outputMessage);
// 3. 此处进行消息转换
if (genericConverter != null) {
genericConverter.write(body, targetType, selectedMediaType, outputMessage);
}
else {
((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);
}
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Nothing to write: null body");
}
}
return;
}
}
}
可以到在使用 ResponseBodyAdvice
进行格式控制之前已经获取了返回值类型 valueType
,默认的消息转换器列表 String
转换器在 Json
转换器前面,所以此处会被 String
转换器处理,泛型将会把 body
强转为 String
。如果在进行格式转换时 String
被更换为其他对象,就会引发强转失败报错。
-
网上的教程只看到了一个版本,就是将
MappingJackson2HttpMessageConverter
顺序移到第一个,让其在String
之前进行处理,由于该处理器也支持处理String
,所以String
处理器就不会再进行处理,从而不会抛出异常。但是这个做法是存在问题的,当有部分接口没有进行格式处理、同时返回的是纯
String
时,响应结果将会多出两个双引号(如下),这是由于被Json
转换器处理带来的结果,要解决这个问题,就不能将Json
处理器放在最前面。"c6c8020a9220421593c4d7042611168e"
-
要解决强转问题
String
转换器必须在后面,要解决双引号问题Sting
又必须在前面,貌似有点矛盾。但是明白了原由想要解决就简单了,首先为了解决没有进行格式处理、同时返回的是纯String
的部分接口的双引号问题,不能调整处理器顺序。而强转String
失败问题要解决则非常容易,在ResponseBodyAdvice
中判断body
的数据类型是不是String
,如果是字符串类型,我们手动转成String
在返回即可。所以只需要以下这三行代码即可解决这两个问题:
if (o == null || o instanceof String) { return JSONObject.toJSONString(result); }
如果传入值原本就不是
String
响应数据也不能是String
哦,否则Json
转换器将会为其加上双引号。