好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

SpringMVC修改返回值类型后的消息转换器处理方式

o(╯□╰)o这标题看起来有点奇怪,所以先以一个小小的案例来说明一下本文要描述和解决的问题

问题案例

假设有一个Controller方法如下

     @RequestMapping (value =  "test" )
     @ResponseBody 
     public   Object  test() {
         Map < String , String > param =  new  HashMap<>();
        param.put( "name" , "userwyh" );
         return  param;
    }

然后我们通过实现ResponseBodyAdvice接口对返回值再输出之前进行了修改,此处我们把它变成了String类型,直接返回hello,world。

@ControllerAdvice
 public    class   MyResponseBodyAdvice   implements   ResponseBodyAdvice < Object >  {
    @Override
     public   boolean  supports(MethodParameter methodParameter,   Class <?  extends   HttpMessageConverter <?>>  aClass )  {
         return   true ;
    }
    @Override
     public   Object  beforeBodyWrite( Object  o, MethodParameter methodParameter, MediaType mediaType,   Class <?  extends   HttpMessageConverter <?>>  aClass ,  ServerHttpRequest   serverHttpRequest ,  ServerHttpResponse   serverHttpResponse )  {
         return   "hello,world" ;
    }
}

这样不管Controller中的test方法返回什么值,我们都会把它变成hello,world输出。想想也没有什么不对,仔细确认了代码,它就是这样做的。

于是,启动项目,打开浏览器,地址栏输入localhost:8080/test,回车。

我们确实看到了hello,world字样,没有任何问题。

但是仔细一看的话,你会发现hello,world前后都多了一个引号。这显然不是我们想要的返回值啊!!!

为什么?

这时候标题提到的SpringMVC的消息转换器HttpMessageConverter就该出场了。

HttpMessageConverter源码剖析可以移步 SpringMVC源码剖析-消息转换器HttpMessageConverter 进行查看。我们这里就不对源码进行详细的解读了。

首先SpringMVC会加载在spring-servlet.xml配置好的消息转换器到messageConverters里。

 protected   final   List <HttpMessageConverter <? >> messageConverters;

debug时发现SpringMVC不止加载了我们配置好的消息转换器,它还加载了另外7个默认的消息转换器,即便7个之中你在配置文件中配置了,它依然会再次加载一次。如图,0和1是配置的,2-8是默认加载的。

上图中的方法writeWithMessageConverters就是在Controller方法执行之后就进入的,在抽象类AbstractMessageConverterMethodProcessor的第164行处。这个方法也正是SpringMVC为当前返回值选择合适的消息转换器,选择的顺序就是messageConverters的转换器顺序。

通过阅读源码,我们知道,此处对messageConverters进行了遍历,先判断当前的转换器对当前返回类型是否能写canWrite,如果能得话就会调用beforeBodyWrite方法,然后把beforeBodyWrite的返回值通过write方法进行输出。如果不能的话就选择下一个转换器。如果最终没有一个合适的,就会抛出一个异常。

了解问题原因及分析

有了上面对HttpMessageConverter的简单描述,我们大概可以得到一个结论:

因为在Controller中的返回值类型是java.util.HashMap,所以在writeWithMessageConverters方法中SpringMVC选定的转换器并不是StringHttpMessageConverter,而是MappingJackson2HttpMessageConverter。

我们可以通过在MyResponseBodyAdvice类beforeBodyWrite方法中打印参数得以证明确实当前SpringMVC选择的转换器就是MappingJackson2HttpMessageConverter。

然后我们在beforeBodyWrite执行返回了String类型的hello,world。而此时选定的转换器已经是MappingJackson2HttpMessageConverter了,所以通过该转换器进行转换输出。

我们再通过一个实例说明MappingJackson2HttpMessageConverter会把String前后新增双引号。

通过上面的分析,相信大家已经大概知道问题的来龙去脉了。

所以第一个反应当然就是重写MappingJackson2HttpMessageConverter的某个方法咯。

不过在这之前,我们还需要对SpringMVC的源码进行进一步分析。

上面提到它会把beforeBodyWrite的返回值通过write方法进行输出,所以我们需要了解这个write方法。它是一个接口,由具体的消息转换器进行实现。SpringMVC自己提供了一个抽象类AbstractGenericHttpMessageConverter进行了实现,但把具体的write任务交给了抽象方法writeInternal。如下图

接下来就是看MappingJackson2HttpMessageConverter的代码了。该类的父类AbstractJackson2HttpMessageConverter确实继承了AbstractGenericHttpMessageConverter并实现了writeInternal方法。

所以,方法很简单,我们只需要把jackson的writeInternal重写一下就可以了。

解决方法

1、创建一个MappingJackson2HttpMessageConverterFactory类

package com.userwyh.spring.controller;
 import  org.slf4j.Logger;
 import  org.springframework.http.HttpOutputMessage;
 import  org.springframework.http.MediaType;
 import  org.springframework.http.converter.HttpMessageNotWritableException;
 import  org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
 import  org.springframework.util.StreamUtils;
 import  java.io.IOException;
 import  java.lang.reflect.Type;
 import  java.nio.charset.Charset;
 import   static  org.slf4j.LoggerFactory.getLogger;
 /**
 * Created by userwyh on 2017/3/4.
 */ 
 public   class  MappingJackson2HttpMessageConverterFactory {
     private   static  final Logger logger = getLogger(MappingJackson2HttpMessageConverterFactory.class);
     public  MappingJackson2HttpMessageConverter init() {
         return   new  MappingJackson2HttpMessageConverter(){
             /**
             * 重写Jackson消息转换器的writeInternal方法
             * SpringMVC选定了具体的消息转换类型后,会调用具体类型的write方法,将Java对象转换后写入返回内容
             */ 
             @Override 
             protected   void  writeInternal( Object   object , Type  type , HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
                 if  ( object   instanceof   String ){
                    logger.info( "在MyResponseBodyAdvice进行转换时返回值变成String了,不能用原来选定消息转换器进行转换,直接使用StringHttpMessageConverter转换" );
                     //StringHttpMessageConverter中就是用以下代码写的 
                    Charset charset =  this .getContentTypeCharset(outputMessage.getHeaders().getContentType());
                    StreamUtils.copy(( String ) object , charset, outputMessage.getBody());
                } else {
                    logger.info( "返回值不是String类型,还是使用之前选择的转换器进行消息转换" );
                     super .writeInternal( object ,  type , outputMessage);
                }
            }
             private  Charset getContentTypeCharset(MediaType contentType) {
                 return  contentType !=  null  && contentType.getCharset() !=  null ?contentType.getCharset(): this .getDefaultCharset();
            }
        };
    }
}

2、稍微修改一下spring的配置文件

 < mvc:annotation-driven > 
         < mvc:message-converters > 
             < bean   class = "org.springframework.http.converter.ByteArrayHttpMessageConverter" > 
                 < property   name = "supportedMediaTypes" > 
                     < list > 
                         < value > image/jpeg </ value > 
                         < value > image/png </ value > 
                         < value > image/gif </ value > 
                     </ list > 
                 </ property > 
             </ bean > 
             < bean    factory-bean = "mappingJackson2HttpMessageConverterFactory"   factory-method = "init" 
                    class = "org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"  > 
             </ bean > 
         </ mvc:message-converters > 
     </ mvc:annotation-driven > 
     < bean   id = "mappingJackson2HttpMessageConverterFactory"   class  =  "com.userwyh.spring.controller.MappingJackson2HttpMessageConverterFactory"  /> 

此时SpringMVC启动时,messageConverters的顺序就是ByteArrayHttpMessageConverter,mappingJackson2HttpMessageConverterFactory,然后另外7个默认的,共9个。如第一张截图所示即为配置后的效果。

3、启动项目,打开浏览器,地址栏输入localhost:8080/test,回车。双引号没有了,正是我们想要的结果。

再看一下日志:

三月 05, 2017 11:01:51 下午 com.userwyh.spring.controller.MappingJackson2HttpMessageConverterFactory$1 writeInternal 信息: 在MyResponseBodyAdvice进行转换时返回值变成String了,不能用原来选定消息转换器进行转换,直接使用StringHttpMessageConverter转换

结语

其实你可以直接在Controller中直接返回String类型的?

其实你可以针对在MyResponseBodyAdvice 中确认要返回不同类型的,直接在Controller中判断下就行了啊,比如以下这样就可以了,为什么要这么麻烦呢?

     @RequestMapping (value =  "test" )
     @ResponseBody 
     public   Object  test() {
         Map < String , String > param =  new  HashMap<>();
        param.put( "name" , "userwyh" );
         if (condition){
             return   "hello,world" ;
        }
         return  param;
    }

可能,因为喜欢折腾,既然可以在MyResponseBodyAdvice 进行统一的返回值转换,我就断定可以找到方法解决这个问题的,也确实解决了。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持。

原文链接:https://blog.csdn.net/userwyh/article/details/60480364

查看更多关于SpringMVC修改返回值类型后的消息转换器处理方式的详细内容...

  阅读:19次