FastJsonHttpMessageConverter 亂碼解決

前言

最近在將 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ā)生了亂碼

請(qǐng)求亂碼.png

于是便查看了下瀏覽器的 response hearders 信息

hearder.png

從這可以看出后臺(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)跑起來

contenttype.png

沒有意外,傳入的就是 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)如圖所示引用鏈

write 方法引用鏈.png

第一個(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"

類型對(duì)比.png

到這里娇妓,差不多一切都明了了像鸡,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 ,希望能夠有所改善吧迁霎。

? ---完---

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末吱抚,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子考廉,更是在濱河造成了極大的恐慌秘豹,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,743評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件昌粤,死亡現(xiàn)場(chǎng)離奇詭異既绕,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)涮坐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門凄贩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人袱讹,你說我怎么就攤上這事疲扎。” “怎么了捷雕?”我有些...
    開封第一講書人閱讀 157,285評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵椒丧,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我救巷,道長(zhǎng)瓜挽,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,485評(píng)論 1 283
  • 正文 為了忘掉前任征绸,我火速辦了婚禮,結(jié)果婚禮上俄占,老公的妹妹穿的比我還像新娘管怠。我一直安慰自己,他們只是感情好缸榄,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,581評(píng)論 6 386
  • 文/花漫 我一把揭開白布渤弛。 她就那樣靜靜地躺著,像睡著了一般甚带。 火紅的嫁衣襯著肌膚如雪她肯。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,821評(píng)論 1 290
  • 那天鹰贵,我揣著相機(jī)與錄音晴氨,去河邊找鬼。 笑死碉输,一個(gè)胖子當(dāng)著我的面吹牛籽前,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 38,960評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼枝哄,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼肄梨!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起挠锥,我...
    開封第一講書人閱讀 37,719評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤众羡,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后蓖租,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體粱侣,經(jīng)...
    沈念sama閱讀 44,186評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,516評(píng)論 2 327
  • 正文 我和宋清朗相戀三年菜秦,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了甜害。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,650評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡球昨,死狀恐怖尔店,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情主慰,我是刑警寧澤嚣州,帶...
    沈念sama閱讀 34,329評(píng)論 4 330
  • 正文 年R本政府宣布,位于F島的核電站共螺,受9級(jí)特大地震影響该肴,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜藐不,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,936評(píng)論 3 313
  • 文/蒙蒙 一匀哄、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧雏蛮,春花似錦涎嚼、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,757評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至犀概,卻和暖如春立哑,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背姻灶。 一陣腳步聲響...
    開封第一講書人閱讀 31,991評(píng)論 1 266
  • 我被黑心中介騙來泰國(guó)打工铛绰, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人产喉。 一個(gè)月前我還...
    沈念sama閱讀 46,370評(píng)論 2 360
  • 正文 我出身青樓至耻,卻偏偏與公主長(zhǎng)得像若皱,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子尘颓,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,527評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容