不知道有没有同学在开发过程中遇到过这样的一个问题:写好了一个接口,然后在请求的时候后台报错,详细异常栈信息:
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 public String publishConfig (String namespace) {} 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)) { headers.setContentType(type); } } super .addDefaultHeaders(headers, s, type); }
给①处打个断点,再请求一次后发现请求并没有执行到断点所在行,也就意味着请求并没有进入到该方法,那么推断问题出在方法参数上,巧了,方法参数还真有一个String
类型,那么就找一下该方法的调用方:
发现有两处调用,不知道是哪一个,然后我们来看异常信息的第二行:
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; 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)) { body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType, (Class<? extends HttpMessageConverter<?>>) converter.getClass(), inputMessage, outputMessage); if (body != null ) { addContentDispositionHeader(inputMessage, outputMessage); 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 ; } } } }
从上面的分析可以知道在第①步之后,body
为String
类型,,但在第⑤步之后body
类型变为了BaseResult
,但是当前converter为StringHttpMessageConverter
,并且在第④步中的converter.canWrite(valueType, selectedMediaType)
已经使用String
类型校验通过了,所以这里会进入到AbstractHttpMessageConverter#write
方法中,这就和异常信息的第二行对上了,那么问题就很明了了,我们把StringHttpMessageConverter
从messageConverters
中拿掉,然后换成相对应的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 @Slf 4j@Configuration @EnableWebMvc public class HttpConverterConfig implements WebMvcConfigurer { @Override public void configureMessageConverters (List<HttpMessageConverter<?>> converters) { MappingJackson2HttpMessageConverter jacksonConverter = mappingJackson2HttpMessageConverter(); 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); } }); converters.add(stringHttpMessageConverter); converters.add(jacksonConverter); log.info("configureMessageConverters加载完毕" ); } }
我们稍微留意一下第③步,因为messageConverter
通过new ArrayList<>()
创建的,所以元素顺序是就是存入顺序,那么在迭代的时候也是先使用StringHttpMessageConverter
做转换,那么我们是不是把顺序调整一下就可以了?先用MappingJackson2HttpMessageConverter
进行处理,是不是就可以了?经测试,确实可以!
说了这么多,如果我们不自定义converter会咋样?把该类从spring容器中拿掉看下:
出现了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<>(); 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) { } 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