前言
最近在將 fastjson
升級(jí)到最新版本(1.2.35)時(shí)發(fā)現(xiàn)官方推薦使用 FastJsonHttpMessageConverter
來集成 spring僵控,于是便將 FastJsonHttpMessageConverter4
換成了 FastJsonHttpMessageConverter
其它設(shè)置沒有改變该默,配置如下所示:
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
FastJsonConfig config = new FastJsonConfig();
config.setSerializerFeatures(SerializerFeature.WriteMapNullValue, // 空字段保留
SerializerFeature.WriteNullStringAsEmpty,
SerializerFeature.WriteNullNumberAsZero);
converter.setFastJsonConfig(config);
converter.setDefaultCharset(Charset.forName("UTF-8"));
converters.add(converter);
}
啟動(dòng)后卻發(fā)生了亂碼
于是便查看了下瀏覽器的 response hearders 信息
從這可以看出后臺(tái)返回的就是最普通的 text/html 格式绒疗,連編碼都沒有指定残邀,結(jié)果顯而易見會(huì)亂碼√鹧伲可以確定問題是出在 content-type
這里了关串。
探尋
為了查出問題所在,我們就需要查看 FastJsonHttpMessageConverter
的源碼了踩寇,如果只想看解決方案的朋友可以點(diǎn)這里啄清。
首先,直接點(diǎn)到頂層父類接口.
public interface HttpMessageConverter<T> {
boolean canRead(Class<?> var1, MediaType var2);
boolean canWrite(Class<?> var1, MediaType var2);
List<MediaType> getSupportedMediaTypes();
T read(Class<? extends T> var1, HttpInputMessage var2) throws IOException, HttpMessageNotReadableException;
void write(T var1, MediaType var2, HttpOutputMessage var3) throws IOException, HttpMessageNotWritableException;
}
可以看到其中有個(gè) write(...)
方法是指定 mediaType
(即 content-type
) 的俺孙,進(jìn)入到 FastJsonHttpMessageConverter
中查看辣卒,發(fā)現(xiàn)有復(fù)寫 write(...)
方法掷贾,如下:
public void write(Object t, Type type, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
HttpHeaders headers = outputMessage.getHeaders();
if(headers.getContentType() == null) {
if(contentType == null || contentType.isWildcardType() || contentType.isWildcardSubtype()) {
contentType = this.getDefaultContentType(t);
}
if(contentType != null) {
headers.setContentType(contentType);
}
}
if(headers.getContentLength() == -1L) {
Long contentLength = this.getContentLength(t, headers.getContentType());
if(contentLength != null) {
headers.setContentLength(contentLength.longValue());
}
}
this.writeInternal(t, outputMessage);
outputMessage.getBody().flush();
}
可以很明顯的看出在這里進(jìn)行了 content-type 的編碼操作,而且這里傳入了一個(gè) contentType 荣茫,值是多少呢想帅?打個(gè)斷點(diǎn)跑起來
沒有意外,傳入的就是 text/html ,而且我們也可以看到 headers 的 size 為 0啡莉,也就是說會(huì)進(jìn)入下面這個(gè)語(yǔ)句中
if(headers.getContentType() == null) {
if(contentType == null || contentType.isWildcardType() || contentType.isWildcardSubtype()) {
contentType = this.getDefaultContentType(t);
}
if(contentType != null) {
headers.setContentType(contentType);
}
}
這里先判斷 contentType 是否為 null港准,如果不為 null 的話就直接進(jìn)行 headers.setContentType(contentType)
的操作,也就造成了亂碼咧欣。
知道了問題所在浅缸,那么解決起來就很快了,我們要做的便是改變這個(gè) contentType魄咕,第一件事便是要知道它從何而來疗杉,這就還是要進(jìn)入 FastJsonHttpMessageConverter
的頂層父接口 HttpMessageConverter
中,在這里查看 write(...)
方法在何地被引用蚕礼,由于需要進(jìn)入源碼查詢,因此需要導(dǎo)入源碼包梢什,具體導(dǎo)入過程可以百度查找奠蹬,我用的 idea ,直接點(diǎn)擊反編譯類文件的右上角的 Download Sources
便可以下載和關(guān)聯(lián)源文件嗡午,下載完后雙擊選中 write(...)
方法囤躁,按 CTRL + ALT + H
便可以出現(xiàn)如圖所示引用鏈
第一個(gè)便是我們要找的目標(biāo),進(jìn)入到里面荔睹,直接定位關(guān)鍵代碼:
for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
if (messageConverter instanceof GenericHttpMessageConverter) {
if (((GenericHttpMessageConverter) messageConverter).canWrite(
declaredType, valueType, selectedMediaType)) {
outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType,(Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(),inputMessage, outputMessage);
if (outputValue != null) {
addContentDispositionHeader(inputMessage, outputMessage);
((GenericHttpMessageConverter) messageConverter).write(
outputValue, declaredType, selectedMediaType, outputMessage);
if (logger.isDebugEnabled()) {
logger.debug("Written [" + outputValue + "] as \"" + selectedMediaType +
"\" using [" + messageConverter + "]");
}
}
return;
}
}
這里先從 messageConverters 中取出我們自定義的 FastJsonHttpMessageConverter ,然后調(diào)用 write () 方法狸演,可以看到這里給 mediaType 賦的值是一個(gè)叫做 selectedMediaType 的變量,這個(gè)變量又是什么呢僻他?繼續(xù)搜索宵距,發(fā)現(xiàn)下面這段代碼:
List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(request);
List<MediaType> producibleMediaTypes = getProducibleMediaTypes(request, valueType, declaredType);
if (outputValue != null && producibleMediaTypes.isEmpty()) {
throw new IllegalArgumentException("No converter found for return value of type: " + valueType);
}
Set<MediaType> compatibleMediaTypes = new LinkedHashSet<MediaType>();
for (MediaType requestedType : requestedMediaTypes) {
for (MediaType producibleType : producibleMediaTypes) {
if (requestedType.isCompatibleWith(producibleType)) {
compatibleMediaTypes.add(getMostSpecificMediaType(requestedType, producibleType));
}
}
}
if (compatibleMediaTypes.isEmpty()) {
if (outputValue != null) {
throw new HttpMediaTypeNotAcceptableException(producibleMediaTypes);
}
return;
}
List<MediaType> mediaTypes = new ArrayList<MediaType>(compatibleMediaTypes);
MediaType.sortBySpecificityAndQuality(mediaTypes);
MediaType selectedMediaType = null;
for (MediaType mediaType : mediaTypes) {
if (mediaType.isConcrete()) {
selectedMediaType = mediaType;
break;
}
else if (mediaType.equals(MediaType.ALL) || mediaType.equals(MEDIA_TYPE_APPLICATION)) {
selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
break;
}
}
從這段代碼我們可以清晰的看到 selectedMediaType 就是從 producibleMediaTypes 中獲取的第一個(gè)可以與請(qǐng)求類型 requestedMediaTypes 中某個(gè)類型所相兼容的類型,而所謂的 producibleMediaTypes 就是在 FastJsonHttpMessageConverter 中空參構(gòu)造方法中所設(shè)置的 SupportedMediaTypes
/**
* Returns the media types that can be produced:
* <ul>
* <li>The producible media types specified in the request mappings, or
* <li>Media types of configured converters that can write the specific return value, or
* <li>{@link MediaType#ALL}
* </ul>
* @since 4.2
*/
@SuppressWarnings("unchecked")
protected List<MediaType> getProducibleMediaTypes(HttpServletRequest request, Class<?> valueClass, Type declaredType) {
Set<MediaType> mediaTypes = (Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
if (!CollectionUtils.isEmpty(mediaTypes)) {
return new ArrayList<MediaType>(mediaTypes);
}
else if (!this.allSupportedMediaTypes.isEmpty()) {
List<MediaType> result = new ArrayList<MediaType>();
for (HttpMessageConverter<?> converter : this.messageConverters) {
if (converter instanceof GenericHttpMessageConverter && declaredType != null) {
if (((GenericHttpMessageConverter<?>) converter).canWrite(declaredType, valueClass, null)) {
result.addAll(converter.getSupportedMediaTypes());
}
}
else if (converter.canWrite(valueClass, null)) {
result.addAll(converter.getSupportedMediaTypes());
}
}
return result;
}
else {
return Collections.singletonList(MediaType.ALL);
}
}
// FastJsonHttpMessageConverter 空參構(gòu)造
public FastJsonHttpMessageConverter() {
super(MediaType.ALL);
}
?千回萬轉(zhuǎn)吨拗,最終又回到了原點(diǎn)满哪,這里設(shè)置的參數(shù)是 ALL ,點(diǎn)進(jìn) MediaType 中可以發(fā)現(xiàn) ALL 的類型是 "*/*" 劝篷,也就是說匹配所有類型哨鸭,因此 selectedMediaType 默認(rèn)就為 requestedMediaTypes 中的第一個(gè)類型,即為 "text/html"
到這里娇妓,差不多一切都明了了像鸡,FastJsonHttpMessageConverter
既沒有在指定 contentType 時(shí)設(shè)置 defaultCharset ,也沒有在 supportContentTypes 中設(shè)置 contentType 的具體類型和編碼哈恰,會(huì)亂碼也就不足為奇了只估。
解決
通過對(duì)源碼的一番探尋志群,我們可以很容易的找出解決方案出來,這里提供兩種方法仅乓,可以根據(jù)個(gè)人愛好采用赖舟。
-
方案一,自定義 supportedMediaTypes
@Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter(); FastJsonConfig config = new FastJsonConfig(); config.setSerializerFeatures(SerializerFeature.WriteMapNullValue, // 空字段保留 SerializerFeature.WriteNullStringAsEmpty, SerializerFeature.WriteNullNumberAsZero); converter.setFastJsonConfig(config); List<MediaType> types = new ArrayList<MediaType>(); types.add(MediaType.APPLICATION_JSON_UTF8); converter.setSupportedMediaTypes(types); converter.setDefaultCharset(Charset.forName("UTF-8")); converters.add(converter); }
方案二(針對(duì) springboot )夸楣,在 application.properties 中添加
spring.http.encoding.force=true
這一行配置宾抓,表示強(qiáng)制使用 defaultCharset(因此也還是需要設(shè)置 defaultCharset)。
思考
兩種解決方案豫喧,相比之下石洗,第一種更明了也更靈活一點(diǎn),畢竟 springboot 的思想便是零配置紧显。springmvc 中默認(rèn)的 AbstractJackson2HttpMessageConverter
便是采用了這種配置讲衫。
/**
* Construct a new {@link MappingJackson2HttpMessageConverter} with a custom {@link ObjectMapper}.
* You can use {@link Jackson2ObjectMapperBuilder} to build it easily.
* @see Jackson2ObjectMapperBuilder#json()
*/
public MappingJackson2HttpMessageConverter(ObjectMapper objectMapper) {
super(objectMapper, MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));
}
配置都是一樣的,為什么 FastJsonHttpMessageConverter
需要額外的配置孵班,而 FastJsonHttpMessageConverter4
就不需要呢涉兽?通過繼承關(guān)系我們就可以明白,FastJsonHttpMessageConverter
直接繼承了 AbstractHttpMessageConverter
篙程,而 FastJsonHttpMessageConverter4
則是繼承了 AbstractHttpMessageConverter
的直接子類AbstractGenericHttpMessageConverter
枷畏,因此并沒有重寫 write 方法,也就是說 contentType 是由其父類 AbstractGenericHttpMessageConverter
配置的虱饿,代碼如下:
/**
* This implementation sets the default headers by calling {@link #addDefaultHeaders},
* and then calls {@link #writeInternal}.
*/
public final void write(final T t, final Type type, MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
final HttpHeaders headers = outputMessage.getHeaders();
addDefaultHeaders(headers, t, contentType);
if (outputMessage instanceof StreamingHttpOutputMessage) {
StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
@Override
public void writeTo(final OutputStream outputStream) throws IOException {
writeInternal(t, type, new HttpOutputMessage() {
@Override
public OutputStream getBody() throws IOException {
return outputStream;
}
@Override
public HttpHeaders getHeaders() {
return headers;
}
});
}
});
}
else {
writeInternal(t, type, outputMessage);
outputMessage.getBody().flush();
}
}
addDefaultHeaders(headers, t, contentType);
這句代碼便是進(jìn)行了 contentType 的設(shè)置拥诡,它是其父類 AbstractHttpMessageConverter
中的方法,如下
/**
* Add default headers to the output message.
* <p>This implementation delegates to {@link #getDefaultContentType(Object)} if a
* content type was not provided, set if necessary the default character set, calls
* {@link #getContentLength}, and sets the corresponding headers.
* @since 4.2
*/
protected void addDefaultHeaders(HttpHeaders headers, T t, MediaType contentType) throws IOException{
if (headers.getContentType() == null) {
MediaType contentTypeToUse = contentType;
if (contentType == null || contentType.isWildcardType() || contentType.isWildcardSubtype()) {
contentTypeToUse = getDefaultContentType(t);
}
else if (MediaType.APPLICATION_OCTET_STREAM.equals(contentType)) {
MediaType mediaType = getDefaultContentType(t);
contentTypeToUse = (mediaType != null ? mediaType : contentTypeToUse);
}
if (contentTypeToUse != null) {
if (contentTypeToUse.getCharset() == null) {
Charset defaultCharset = getDefaultCharset();
if (defaultCharset != null) {
contentTypeToUse = new MediaType(contentTypeToUse, defaultCharset);
}
}
headers.setContentType(contentTypeToUse);
}
}
if (headers.getContentLength() < 0 && !headers.containsKey(HttpHeaders.TRANSFER_ENCODING)) {
Long contentLength = getContentLength(t, headers.getContentType());
if (contentLength != null) {
headers.setContentLength(contentLength);
}
}
}
同 FastJsonHttpMessageConverter
中的大體意思差不多氮发,都是在進(jìn)行 contentType 和 contentLenth 的設(shè)置渴肉,但在 contentTypeToUse = new MediaType(contentTypeToUse, defaultCharset);
這句代碼中,它給 contentType 指定了其編碼類型爽冕,因此即使它的類型是 "text/html" 仇祭,但也能正常顯示。
尾巴
雖然解決方案百度一下很快就能出來颈畸,但很多人都只是給了方案前塔,而沒有給原理,寫這篇文章的目的不單單是為了解決問題承冰,也順便是為了探尋一下 springmvc 的執(zhí)行流程华弓,了解其內(nèi)部對(duì)各個(gè)部件的調(diào)用流程,雖然花了點(diǎn)時(shí)間困乒,不過所幸學(xué)到了不少的東西寂屏。
關(guān)于這個(gè)不知道算不算 bug 的 bug,我也在 github 上提了一個(gè) issue ,希望能夠有所改善吧迁霎。
? ---完---