SpringMVC修改返回值類型后的消息轉(zhuǎn)換器處理方式
o(╯□╰)o這標(biāo)題看起來有點(diǎn)奇怪,所以先以一個(gè)小小的案例來說明一下本文要描述和解決的問題
問題案例
假設(shè)有一個(gè)Controller方法如下
@RequestMapping(value = "test")
@ResponseBody
public Object test() {
Map<String,String> param = new HashMap<>();
param.put("name","userwyh");
return param;
}
然后我們通過實(shí)現(xiàn)ResponseBodyAdvice接口對(duì)返回值再輸出之前進(jìn)行了修改,此處我們把它變成了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方法返回什么值,我們都會(huì)把它變成hello,world輸出。想想也沒有什么不對(duì),仔細(xì)確認(rèn)了代碼,它就是這樣做的。
于是,啟動(dòng)項(xiàng)目,打開瀏覽器,地址欄輸入localhost:8080/test,回車。
我們確實(shí)看到了hello,world字樣,沒有任何問題。

但是仔細(xì)一看的話,你會(huì)發(fā)現(xiàn)hello,world前后都多了一個(gè)引號(hào)。這顯然不是我們想要的返回值?。。。?/p>
為什么?
這時(shí)候標(biāo)題提到的SpringMVC的消息轉(zhuǎn)換器HttpMessageConverter就該出場(chǎng)了。
HttpMessageConverter源碼剖析可以移步 SpringMVC源碼剖析-消息轉(zhuǎn)換器HttpMessageConverter 進(jìn)行查看。我們這里就不對(duì)源碼進(jìn)行詳細(xì)的解讀了。
首先SpringMVC會(huì)加載在spring-servlet.xml配置好的消息轉(zhuǎn)換器到messageConverters里。
protected final List<HttpMessageConverter<?>> messageConverters;
debug時(shí)發(fā)現(xiàn)SpringMVC不止加載了我們配置好的消息轉(zhuǎn)換器,它還加載了另外7個(gè)默認(rèn)的消息轉(zhuǎn)換器,即便7個(gè)之中你在配置文件中配置了,它依然會(huì)再次加載一次。如圖,0和1是配置的,2-8是默認(rèn)加載的。

上圖中的方法writeWithMessageConverters就是在Controller方法執(zhí)行之后就進(jìn)入的,在抽象類AbstractMessageConverterMethodProcessor的第164行處。這個(gè)方法也正是SpringMVC為當(dāng)前返回值選擇合適的消息轉(zhuǎn)換器,選擇的順序就是messageConverters的轉(zhuǎn)換器順序。

通過閱讀源碼,我們知道,此處對(duì)messageConverters進(jìn)行了遍歷,先判斷當(dāng)前的轉(zhuǎn)換器對(duì)當(dāng)前返回類型是否能寫canWrite,如果能得話就會(huì)調(diào)用beforeBodyWrite方法,然后把beforeBodyWrite的返回值通過write方法進(jìn)行輸出。如果不能的話就選擇下一個(gè)轉(zhuǎn)換器。如果最終沒有一個(gè)合適的,就會(huì)拋出一個(gè)異常。
了解問題原因及分析
有了上面對(duì)HttpMessageConverter的簡(jiǎn)單描述,我們大概可以得到一個(gè)結(jié)論:
因?yàn)樵贑ontroller中的返回值類型是java.util.HashMap,所以在writeWithMessageConverters方法中SpringMVC選定的轉(zhuǎn)換器并不是StringHttpMessageConverter,而是MappingJackson2HttpMessageConverter。
我們可以通過在MyResponseBodyAdvice類beforeBodyWrite方法中打印參數(shù)得以證明確實(shí)當(dāng)前SpringMVC選擇的轉(zhuǎn)換器就是MappingJackson2HttpMessageConverter。
然后我們?cè)赽eforeBodyWrite執(zhí)行返回了String類型的hello,world。而此時(shí)選定的轉(zhuǎn)換器已經(jīng)是MappingJackson2HttpMessageConverter了,所以通過該轉(zhuǎn)換器進(jìn)行轉(zhuǎn)換輸出。
我們?cè)偻ㄟ^一個(gè)實(shí)例說明MappingJackson2HttpMessageConverter會(huì)把String前后新增雙引號(hào)。

通過上面的分析,相信大家已經(jīng)大概知道問題的來龍去脈了。
所以第一個(gè)反應(yīng)當(dāng)然就是重寫MappingJackson2HttpMessageConverter的某個(gè)方法咯。
不過在這之前,我們還需要對(duì)SpringMVC的源碼進(jìn)行進(jìn)一步分析。
上面提到它會(huì)把beforeBodyWrite的返回值通過write方法進(jìn)行輸出,所以我們需要了解這個(gè)write方法。它是一個(gè)接口,由具體的消息轉(zhuǎn)換器進(jìn)行實(shí)現(xiàn)。SpringMVC自己提供了一個(gè)抽象類AbstractGenericHttpMessageConverter進(jìn)行了實(shí)現(xiàn),但把具體的write任務(wù)交給了抽象方法writeInternal。如下圖

接下來就是看MappingJackson2HttpMessageConverter的代碼了。該類的父類AbstractJackson2HttpMessageConverter確實(shí)繼承了AbstractGenericHttpMessageConverter并實(shí)現(xiàn)了writeInternal方法。

所以,方法很簡(jiǎn)單,我們只需要把jackson的writeInternal重寫一下就可以了。
解決方法
1、創(chuàng)建一個(gè)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消息轉(zhuǎn)換器的writeInternal方法
* SpringMVC選定了具體的消息轉(zhuǎn)換類型后,會(huì)調(diào)用具體類型的write方法,將Java對(duì)象轉(zhuǎn)換后寫入返回內(nèi)容
*/
@Override
protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
if (object instanceof String){
logger.info("在MyResponseBodyAdvice進(jìn)行轉(zhuǎn)換時(shí)返回值變成String了,不能用原來選定消息轉(zhuǎn)換器進(jìn)行轉(zhuǎn)換,直接使用StringHttpMessageConverter轉(zhuǎn)換");
//StringHttpMessageConverter中就是用以下代碼寫的
Charset charset = this.getContentTypeCharset(outputMessage.getHeaders().getContentType());
StreamUtils.copy((String)object, charset, outputMessage.getBody());
}else{
logger.info("返回值不是String類型,還是使用之前選擇的轉(zhuǎn)換器進(jìn)行消息轉(zhuǎn)換");
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" />
此時(shí)SpringMVC啟動(dòng)時(shí),messageConverters的順序就是ByteArrayHttpMessageConverter,mappingJackson2HttpMessageConverterFactory,然后另外7個(gè)默認(rèn)的,共9個(gè)。如第一張截圖所示即為配置后的效果。
3、啟動(dòng)項(xiàng)目,打開瀏覽器,地址欄輸入localhost:8080/test,回車。雙引號(hào)沒有了,正是我們想要的結(jié)果。

再看一下日志:
三月 05, 2017 11:01:51 下午 com.userwyh.spring.controller.MappingJackson2HttpMessageConverterFactory$1 writeInternal 信息: 在MyResponseBodyAdvice進(jìn)行轉(zhuǎn)換時(shí)返回值變成String了,不能用原來選定消息轉(zhuǎn)換器進(jìn)行轉(zhuǎn)換,直接使用StringHttpMessageConverter轉(zhuǎn)換
結(jié)語
其實(shí)你可以直接在Controller中直接返回String類型的?
其實(shí)你可以針對(duì)在MyResponseBodyAdvice 中確認(rèn)要返回不同類型的,直接在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;
}
可能,因?yàn)橄矚g折騰,既然可以在MyResponseBodyAdvice 進(jìn)行統(tǒng)一的返回值轉(zhuǎn)換,我就斷定可以找到方法解決這個(gè)問題的,也確實(shí)解決了。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
springboot實(shí)現(xiàn)用戶名查找用戶功能
本文主要介紹了springboot實(shí)現(xiàn)用戶名查找用戶功能,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-04-04
springboot之redis cache TTL選項(xiàng)的使用
這篇文章主要介紹了springboot之redis cache TTL選項(xiàng)的使用方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-07-07
Mybatis注解方式操作Oracle數(shù)據(jù)庫詳解
這篇文章主要介紹了Mybatis注解方式操作Oracle數(shù)據(jù)庫詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-11-11
Spring事物基礎(chǔ)知識(shí)及AOP相關(guān)陷阱分析
這篇文章主要介紹了Spring事物基礎(chǔ)知識(shí)及AOP相關(guān)陷阱,在平時(shí)的實(shí)際開發(fā)中經(jīng)常會(huì)遇到,只有深入了解了其中的原理,才會(huì)在工作中能夠有效應(yīng)對(duì)2021-09-09
Java中l(wèi)ist.foreach不能使用字符串拼接的問題
這篇文章主要介紹了Java中l(wèi)ist.foreach不能使用字符串拼接的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09
Maven項(xiàng)src/main/java目錄下配置文件無法被導(dǎo)出或者生效的問題和處理方案
這篇文章主要介紹了Maven項(xiàng)src/main/java目錄下配置文件無法被導(dǎo)出或者生效的問題和處理方案,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-11-11
使用socket進(jìn)行服務(wù)端與客戶端傳文件的方法
這篇文章主要介紹了使用socket進(jìn)行服務(wù)端與客戶端傳文件的方法,需要的朋友可以參考下2017-08-08

