springmvc的@ResponseBody返回类型的底层处理和乱码问题解决

使用过springmvc都知道,我们只需编写Handler的逻辑和返回结果,而返回的结果可以是任意类型的,springmvc是如何处理这些类型的呢。

一 首先了解两个概念:

1 返回值处理器(HandlerMethodReturnValueHandler):对于请求访问,不同的请求,需要不用的Handler,同样的道理,对于Handler的不同返回值,也需要不同的返回值处理器(HandlerMethodReturnValueHandler)来处理。HandlerMethodReturnValueHandler集合配置在适配器的List<HandlerMethodReturnValueHandler> customReturnValueHandlers属性里面。
默认的返回值处理器有:

private List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers() {
        List<HandlerMethodReturnValueHandler> handlers = new ArrayList();
        handlers.add(new ModelAndViewMethodReturnValueHandler());
        handlers.add(new ModelMethodProcessor());
        handlers.add(new ViewMethodReturnValueHandler());
        handlers.add(new ResponseBodyEmitterReturnValueHandler(this.getMessageConverters(), this.reactiveAdapterRegistry, this.taskExecutor, this.contentNegotiationManager));
        handlers.add(new StreamingResponseBodyReturnValueHandler());
        handlers.add(new HttpEntityMethodProcessor(this.getMessageConverters(), this.contentNegotiationManager, this.requestResponseBodyAdvice));
        handlers.add(new HttpHeadersReturnValueHandler());
        handlers.add(new CallableMethodReturnValueHandler());
        handlers.add(new DeferredResultMethodReturnValueHandler());
        handlers.add(new AsyncTaskMethodReturnValueHandler(this.beanFactory));
        handlers.add(new ModelAttributeMethodProcessor(false));
        handlers.add(new RequestResponseBodyMethodProcessor(this.getMessageConverters(), this.contentNegotiationManager, this.requestResponseBodyAdvice));
        handlers.add(new ViewNameMethodReturnValueHandler());
        handlers.add(new MapMethodProcessor());
        if (this.getCustomReturnValueHandlers() != null) {
            handlers.addAll(this.getCustomReturnValueHandlers());
        }

        if (!CollectionUtils.isEmpty(this.getModelAndViewResolvers())) {
            handlers.add(new ModelAndViewResolverMethodReturnValueHandler(this.getModelAndViewResolvers()));
        } else {
            handlers.add(new ModelAttributeMethodProcessor(true));
        }

        return handlers;
    }
image.png

常用的有:

  • ModelAndViewMethodReturnValueHandler:支持返回值是ModelAndView类型的。
  • ModelMethodProcessor:支持返回值是Model的。
  • ViewMethodReturnValueHandler:支持返回值是View的。
  • HttpEntityMethodProcessor:支持返回值是HttpEntity的。
  • RequestResponseBodyMethodProcess:支持类上或者方法上含有@ResponseBody注解的(这里主要讲解这个返回值处理器)。
  • ViewNameMethodReturnValueHandler:支持返回类型是void或者String。

获取返回值处理器的规则是从上到下返回第一个匹配的处理器。

2 返回值转换器(HttpMessageConverter):当找到合适的返回值处理器(HandlerMethodReturnValueHandler)后,处理器还需要调用对应的返回值转换器来对返回值进行转换,然后才输出到响应体里面。需要使用默认转换器的主要是ResponseBodyEmitterReturnValueHandler、HttpEntityMethodProcessor、RequestResponseBodyMethodProcessor这三个返回值处理器。

特别提示:返回值转换器配置在适配器RequestMappingHandlerAdapter的private List<HttpMessageConverter<?>> messageConverters属性里面。

默认的返回值转换器有:

public RequestMappingHandlerAdapter() {
        StringHttpMessageConverter stringHttpMessageConverter 
                        = new StringHttpMessageConverter();
        stringHttpMessageConverter.setWriteAcceptCharset(false);
        this.messageConverters = new ArrayList(4);
        this.messageConverters.add(new ByteArrayHttpMessageConverter());
        this.messageConverters.add(stringHttpMessageConverter);

        try {
            this.messageConverters.add(new SourceHttpMessageConverter());
        } catch (Error var3) {
        }

        this.messageConverters
        .add(new AllEncompassingFormHttpMessageConverter());
    }

转换器转换规则--遍历返回值转换器集合,使用返回值转换器的canWrite(Class<?> clazz, @Nullable MediaType mediaType)方法判断是否匹配,在里面主要会进行类型是否支持和媒体是否支持的判断,两者同时满足表示匹配成功,使用该转换器处理返回值,canWrite(Class<?> clazz, @Nullable MediaType mediaType)如下所示。

public boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {
        //this.supports(clazz) 判断类型是否匹配,
        //this.canWrite(mediaType)判断媒体是否匹配
        return this.supports(clazz) && this.canWrite(mediaType);
    }

常用类型转换器匹配规则:

  • ByteArrayHttpMessageConverter:匹配 byte[]数组,默认匹配媒体类型有 [application/octet-stream, */*]
  • StringHttpMessageConverter:匹配String类型,默认匹配媒体类型集合有 [text/plain, */*]
  • SourceHttpMessageConverter:匹配DOMSource、SAXSource、StAXSource、StreamSource、Source类型,默认匹配媒体类型集合有[application/xml, text/xml, application/*+xml]
  • AllEncompassingFormHttpMessageConverter:匹配MultiValueMap类型或是其子类,匹配媒体类型集合有[application/x-www-form-urlencoded, multipart/form-data]

注意:媒体mediaType有两个属性,分别是type和subtype,比如媒体类型application/octet-stream的type=application、subtype=octet-stream。两个媒体的type和subtype属性相等即匹配成功。另外,*/*表示通配任意type与subtype的值,所以默认匹配媒体类型如果有*/* ,那么媒体类型肯定可以匹配成功,我们只需关注this.supports(clazz)即类型判断就行了。还有就是,媒体类型首先是从response对象里面获取content-type的header值,如果没有获取到,即响应没有设置content-type的header值,springmvc会自己尝试给定一个可接受的媒体值用于媒体判断。当然,如果不想使用默认媒体类型,还可以自定义媒体类型,只需在自定义返回值转换器的时候指定supportedMediaTypes即可。

注意:返回值转换器默认的编码是ISO-8859-1,在将返回值写入响应体里面时会使用该编码方式编码后再写入所以会出现乱码问题。什么意思,意思就是假如你的返回值是utf-8编码,然后也告诉了浏览器返回值的编码方式是utf-8(通过response对象告诉浏览器),最后发现显示时任然是乱码,就是因为返回值转换器在将返回值写入响应体之前还要对其进行一次编码,而默认使用的是ISO-8859-1编码方式,导致乱码问题。
配置返回值转换器的媒体类型与编码方式(utf-8):

<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
        <property name="messageConverters">
            <list>
                <bean class="org.springframework.http.converter.StringHttpMessageConverter" >
                    <property name="defaultCharset" value="utf-8"></property>
                    <property name="supportedMediaTypes">
                        <list>
                            <value>text/html</value>
                            <value>*/*</value>
                        </list>
                    </property>
                </bean>
            </list>
        </property>
    </bean>

二 @ResponseBody字符串中文乱码问题解决

由以上知道,使用了@ResponseBody的handler使用的是RequestResponseBodyMethodProcess返回值处理器,该处理器需要使用默认返回值转换器集合中的转换器来处理返回值。
知道了返回值的处理规则后,这里来说一下如何处理返回字符串中文乱码问题。首先我们要知道,出现乱码就是因为从返回值到浏览器显示时编码方式不一致导致的,就是因为转换器的默认编码方式为iso-8859-1。知道原因过后修改转换器的编码方式就行了。
字符串返回值使用的是StringHttpMessageConverter转换器,并且我们知道转换器集合配置在适配器的private List<HttpMessageConverter<?>> messageConverters属性里面,解决办法就是自己配置messageConverters里面的转换器,并为转换器的defaultCharset属性配置编码为utf-8,需要注意的是自己配置后以前默认的转换器就不存在了:

<!--配置方式一-->
    <mvc:annotation-driven>
        <mvc:message-converters>
            <bean class="org.springframework.http.converter.StringHttpMessageConverter">
                <property name="defaultCharset" value="utf-8"></property>
            </bean>
        </mvc:message-converters>
    </mvc:annotation-driven>
<!--配置方式二-->
    <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
        <property name="messageConverters">
            <list>
                <bean class="org.springframework.http.converter.StringHttpMessageConverter" >
                    <property name="defaultCharset" value="utf-8"></property>
                </bean>
            </list>
        </property>
    </bean>

两种配置是根据引入适配器与映射器方式的不同而不同,但是原理都一样,都是配置适配器的messageConverters属性。

三 @ResponseBody返回json字符串

我们知道,springmvc默认的返回值转换器只能匹配有限类型,其中就不包括我们定义的一些实体类,如果返回值是一些实体类型,会抛出没有对应的返回值转换器异常。

org.springframework.http.converter.HttpMessageNotWritableException: No converter found for return value of type: class com.dahuici.zyb.entity.User
org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:233)  org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.handleReturnValue(RequestResponseBodyMethodProcessor.java:180)
org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite.handleReturnValue(HandlerMethodReturnValueHandlerComposite.java:82)
org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:119)
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895)
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:800)
org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)    org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1038)
org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:942) org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1005)
org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:897)
javax.servlet.http.HttpServlet.service(HttpServlet.java:634)
org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:882)
javax.servlet.http.HttpServlet.service(HttpServlet.java:741)
org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)

springmvc为我们准备了很多转换器,如下图所示


image.png

这里我们使用的是json里面的MappingJackson2HttpMessageConverter


image.png

MappingJackson2HttpMessageConverter 默认匹配的媒体类型有 [application/json, application/*+json],至于对于返回值类型来说,唯一的要求好像就是需要有set与get方法,当然,如果是String类型不需要set与get(这一段的类型判断超出技术水平了)。
另外,MappingJackson2HttpMessageConverter使用到了其他的包(jackson-databind,jackson-core最少要导入这两个),所以需要导入如下依赖:
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.9.5</version>
    </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-core</artifactId>
      <version>2.9.5</version>
    </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-annotations</artifactId>
      <version>2.9.5</version>
    </dependency>

然后将其配置给适配器的messageConverters属性即可,需要注意的是也要配置编码方式为utf-8,不然返回的json字符串会出现乱码。

<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
        <property name="messageConverters">
            <list>
                <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter" >
                    <property name="defaultCharset" value="utf-8"></property>
                </bean>
            </list>
        </property>
    </bean>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,651评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,468评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,931评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,218评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,234评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,198评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,084评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,926评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,341评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,563评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,731评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,430评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,036评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,676评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,829评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,743评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,629评论 2 354

推荐阅读更多精彩内容