欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2022-03-14 08:13:24
目录o(╯□╰)o这标题看起来有点奇怪,所以先以一个小小的案例来说明一下本文要描述和解决的问题问题案例假设有一个controller方法如下 @requestmapping(value = "t...

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字样,没有任何问题。

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

但是仔细一看的话,你会发现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是默认加载的。

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

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

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

通过阅读源码,我们知道,此处对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前后新增双引号。

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

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

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

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

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

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

接下来就是看mappingjackson2httpmessageconverter的代码了。该类的父类abstractjackson2httpmessageconverter确实继承了abstractgenerichttpmessageconverter并实现了writeinternal方法。

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

所以,方法很简单,我们只需要把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,回车。双引号没有了,正是我们想要的结果。

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

再看一下日志:

三月 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 进行统一的返回值转换,我就断定可以找到方法解决这个问题的,也确实解决了。

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