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