SpringBoot接口返回值出现ClassCastException

不知道有没有同学在开发过程中遇到过这样的一个问题:写好了一个接口,然后在请求的时候后台报错,详细异常栈信息:

1
2
3
4
5
java.lang.ClassCastException: cc.kevinlu.meeting.global.BaseResult cannot be cast to java.lang.String
at org.springframework.http.converter.StringHttpMessageConverter.addDefaultHeaders(StringHttpMessageConverter.java:44)
at org.springframework.http.converter.AbstractHttpMessageConverter.write(AbstractHttpMessageConverter.java:211)
at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:290)
at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.handleReturnValue(RequestResponseBodyMethodProcessor.java:181)

从异常信息可以看出是出现了ClassCastException,原因是项目中使用了ResponseBodyAdvice对接口返回数据做了一层包装,包装后的对象类型发生了变化,例如下方代码,接口返回的对象类型是String,包装过后则变成了BaseResult

1
2
3
4
5
6
7
8
9
10
11
12
// 1. 接口
public String publishConfig(String namespace) {
}

// 2. 包装
public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class aClass,
ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
if (o instanceof BaseResult) {
return o;
}
return BaseResult.success(o);
}

这好像也没什么问题,为什么会出现ClassCastException呢?出现了异常肯定是要分析异常栈的,我们从异常栈第一行开始看起:

org.springframework.http.converter.StringHttpMessageConverter.addDefaultHeaders(StringHttpMessageConverter.java:44)

问题是从StringHttpMessageConverter#addDefaultHeaders方法抛出来的,进入到方法中,查看此处代码:

1
2
3
4
5
6
7
8
9
10
@Override
protected void addDefaultHeaders(HttpHeaders headers, String s, @Nullable MediaType type) throws IOException {
if (headers.getContentType() == null ) { // ①
if (type != null && type.isConcrete() && type.isCompatibleWith(MediaType.APPLICATION_JSON)) {
// Prevent charset parameter for JSON..
headers.setContentType(type);
}
}
super.addDefaultHeaders(headers, s, type);
}

给①处打个断点,再请求一次后发现请求并没有执行到断点所在行,也就意味着请求并没有进入到该方法,那么推断问题出在方法参数上,巧了,方法参数还真有一个String类型,那么就找一下该方法的调用方:

image-20210226132209847

发现有两处调用,不知道是哪一个,然后我们来看异常信息的第二行:

org.springframework.http.converter.AbstractHttpMessageConverter.write(AbstractHttpMessageConverter.java:211)

这里告诉了我们,是在AbstractHttpMessageConverter#write方法,在类的第211行代码,老规矩,上代码:

1
2
3
4
5
6
7
8
@Override
public final void write(final T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {

final HttpHeaders headers = outputMessage.getHeaders();
addDefaultHeaders(headers, t, contentType);
// 省略无关代码...
}

write接收了一个泛型对象t,这个t可以为任何类型,所以这里还不是异常的根本原因,我们还需要继续往上层调用方冒泡,然后我们根据异常信息第三行找到了AbstractMessageConverterMethodProcessor#writeWithMessageConverters方法,一起来看下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType,
ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {

Object body;
Class<?> valueType;
Type targetType;

// ①校验原始返回值是否为字符串,原始返回值是指接口方法上声明的返回值,在我们这里,这个就是true
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());
}

// 忽略其他代码,包括了流文件判断、校验contentType

// 重点来了,看仔细了哈
if (selectedMediaType != null) {
selectedMediaType = selectedMediaType.removeQualityValue();
// ②迭代messageConverters,挨个儿的进行转换,先卖个关子,这个messageConverters有哪些,是从哪来的?
for (HttpMessageConverter<?> converter : this.messageConverters) {
// ③校验converter是否为GenericHttpMessageConverter的子类,这个GenericHttpMessageConverter又为何物?
GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ?
(GenericHttpMessageConverter<?>) converter : null);
// ④校验返回值类型是否可以被converter修改
if (genericConverter != null ?
((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) :
converter.canWrite(valueType, selectedMediaType)) {
// ⑤获取ResponseBodyAdvice的实现类,并调用beforeBodyWrite方法获取重写后的返回值
body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,
(Class<? extends HttpMessageConverter<?>>) converter.getClass(),
inputMessage, outputMessage);
if (body != null) {
// 添加相关的header数据
addContentDispositionHeader(inputMessage, outputMessage);
// ⑥校验使用哪一种converter输出数据
if (genericConverter != null) {
genericConverter.write(body, targetType, selectedMediaType, outputMessage);
} else {
// ☆异常信息中的290行
((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);
}
} else {
if (logger.isDebugEnabled()) {
logger.debug("Nothing to write: null body");
}
}
return;
}
}
}
}

从上面的分析可以知道在第①步之后,bodyString类型,,但在第⑤步之后body类型变为了BaseResult,但是当前converter为StringHttpMessageConverter,并且在第④步中的converter.canWrite(valueType, selectedMediaType)已经使用String类型校验通过了,所以这里会进入到AbstractHttpMessageConverter#write方法中,这就和异常信息的第二行对上了,那么问题就很明了了,我们把StringHttpMessageConvertermessageConverters中拿掉,然后换成相对应的converter是不是就可以了?那么这些个converter是从哪里来的呢?巧了,在项目中有一个WebMvcConfigurer接口的实现类,里面重写了configureMessageConverters方法,该方法里设置相关的converter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Slf4j
@Configuration
@EnableWebMvc
public class HttpConverterConfig implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// ①定义Jackson转换器,具体代码省略...
MappingJackson2HttpMessageConverter jacksonConverter = mappingJackson2HttpMessageConverter();
// ②定义String转换器,具体代码省略...
StringHttpMessageConverter stringHttpMessageConverter = stringHttpMessageConverter();
jacksonConverter.setSupportedMediaTypes(new LinkedList<MediaType>() {
{
add(MediaType.TEXT_HTML);
add(MediaType.APPLICATION_JSON);
}
});
stringHttpMessageConverter.setSupportedMediaTypes(new LinkedList<MediaType>() {
{
add(MediaType.TEXT_HTML);
add(MediaType.APPLICATION_JSON);
}
});
// ③将转换器放入到messageConverter中【注意顺序】
converters.add(stringHttpMessageConverter);
converters.add(jacksonConverter);
log.info("configureMessageConverters加载完毕");
}
}

我们稍微留意一下第③步,因为messageConverter通过new ArrayList<>()创建的,所以元素顺序是就是存入顺序,那么在迭代的时候也是先使用StringHttpMessageConverter做转换,那么我们是不是把顺序调整一下就可以了?先用MappingJackson2HttpMessageConverter进行处理,是不是就可以了?经测试,确实可以!


说了这么多,如果我们不自定义converter会咋样?把该类从spring容器中拿掉看下:

image-20210226154106328

出现了10个converter,这10个是从哪里来的?我们只有从messageConverters发赋值区域开始看了,其是在父类AbstractMessageConverterMethodArgumentResolver中定义和最终赋值的,赋值方式为构造器传入,我们通过构造器一层层冒泡,最终找到了RequestMappingHandlerAdapter类,在这个适配器中也有一个一模一样的messageConverters,并且有setter和getter方法,并且惊奇的发现,在该适配器的构造方法中对messageConverters进行了初始化,并塞进去了4/5个转换器,这就完了吗?当然不是,上面可是看到了10个converter,这里最多才有5个,我们找到它的setter方法,然后继续冒泡,就找到了WebMvcConfigurationSupport#requestMappingHandlerAdapter方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
public RequestMappingHandlerAdapter requestMappingHandlerAdapter(
@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
@Qualifier("mvcConversionService") FormattingConversionService conversionService,
@Qualifier("mvcValidator") Validator validator) {

RequestMappingHandlerAdapter adapter = createRequestMappingHandlerAdapter();
// ☆核心代码
adapter.setMessageConverters(getMessageConverters());
// 忽略其他代码
}

/**
* 获取转换器
*/
protected final List<HttpMessageConverter<?>> getMessageConverters() {
if (this.messageConverters == null) {
this.messageConverters = new ArrayList<>();
// 调用WebMvcConfigurer代理类获取自定义的converter,其实就是获取上面HttpConverterConfig#configureMessageConverters里面定义的
// 因为我们把自定义的HttpConverterConfig从spring容器中拿掉了,所以这个方法跑了个寂寞
configureMessageConverters(this.messageConverters);
if (this.messageConverters.isEmpty()) {
// 上面的方法跑了个寂寞,那这里就给寂寞加点芥末,进去看看↓
addDefaultHttpMessageConverters(this.messageConverters);
}
extendMessageConverters(this.messageConverters);
}
return this.messageConverters;
}

/**
* 添加默认的转换器
*/
protected final void addDefaultHttpMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
messageConverters.add(new ByteArrayHttpMessageConverter());
messageConverters.add(new StringHttpMessageConverter());
messageConverters.add(new ResourceHttpMessageConverter());
messageConverters.add(new ResourceRegionHttpMessageConverter());
try {
messageConverters.add(new SourceHttpMessageConverter<>());
} catch (Throwable ex) {
// Ignore when no TransformerFactory implementation is available...
}
messageConverters.add(new AllEncompassingFormHttpMessageConverter());

if (romePresent) {
messageConverters.add(new AtomFeedHttpMessageConverter());
messageConverters.add(new RssChannelHttpMessageConverter());
}

if (jackson2XmlPresent) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.xml();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
}
messageConverters.add(new MappingJackson2XmlHttpMessageConverter(builder.build()));
} else if (jaxb2Present) {
messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
}

if (jackson2Present) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.json();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
}
messageConverters.add(new MappingJackson2HttpMessageConverter(builder.build()));
} else if (gsonPresent) {
messageConverters.add(new GsonHttpMessageConverter());
} else if (jsonbPresent) {
messageConverters.add(new JsonbHttpMessageConverter());
}

if (jackson2SmilePresent) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.smile();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
}
messageConverters.add(new MappingJackson2SmileHttpMessageConverter(builder.build()));
}
if (jackson2CborPresent) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.cbor();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
}
messageConverters.add(new MappingJackson2CborHttpMessageConverter(builder.build()));
}
}

现在就都清楚了,之所以会出现ClassCastException,是因为传入的参数类型和使用的HttpMessageConverter类型不匹配,找到对应的converter即可,所以尽量在一个项目中,接口的返回值类型最好统一进行封装,防止出现此类情况。

扩展

再细细一品,AbstractMessageConverterMethodProcessor#writeWithMessageConverters的代码是不是真的有bug,能不能改造一下呢?比如在调用converter的canWrite方法之前先调用advice的beforeBodyWrite方法,然后重新使用下方语句获取类型,接着继续执行。

1
2
valueType = getReturnValueType(body, returnType);
targetType = GenericTypeResolver.resolveType(getGenericType(returnType), returnType.getContainingClass());

这只是一个简单的想法,具体尚未深究。


凑够2021个字,凑够2021个字,凑够2021个字,凑够2021