FastJsonHttpMessageConverter 乱码解决

前言

最近在将 fastjson 升级到最新版本(1.2.35)时发现官方推荐使用 FastJsonHttpMessageConverter 来集成 spring,于是便将 FastJsonHttpMessageConverter4 换成了 FastJsonHttpMessageConverter 其它设置没有改变,配置如下所示:

    @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);
    }

启动后却发生了乱码

请求乱码.png

于是便查看了下浏览器的 response hearders 信息

hearder.png

从这可以看出后台返回的就是最普通的 text/html 格式,连编码都没有指定,结果显而易见会乱码。可以确定问题是出在 content-type 这里了。

探寻

为了查出问题所在,我们就需要查看 FastJsonHttpMessageConverter 的源码了,如果只想看解决方案的朋友可以点这里

首先,直接点到顶层父类接口.

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;
}

可以看到其中有个 write(...) 方法是指定 mediaType(即 content-type) 的,进入到 FastJsonHttpMessageConverter 中查看,发现有复写 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();
    }

可以很明显的看出在这里进行了 content-type 的编码操作,而且这里传入了一个 contentType ,值是多少呢?打个断点跑起来

contenttype.png

没有意外,传入的就是 text/html ,而且我们也可以看到 headers 的 size 为 0,也就是说会进入下面这个语句中

if(headers.getContentType() == null) {
            if(contentType == null || contentType.isWildcardType() || contentType.isWildcardSubtype()) {
                contentType = this.getDefaultContentType(t);
            }

            if(contentType != null) {
                headers.setContentType(contentType);
            }
 }

这里先判断 contentType 是否为 null,如果不为 null 的话就直接进行 headers.setContentType(contentType)的操作,也就造成了乱码。

知道了问题所在,那么解决起来就很快了,我们要做的便是改变这个 contentType,第一件事便是要知道它从何而来,这就还是要进入 FastJsonHttpMessageConverter 的顶层父接口 HttpMessageConverter 中,在这里查看 write(...) 方法在何地被引用,由于需要进入源码查询,因此需要导入源码包,具体导入过程可以百度查找,我用的 idea ,直接点击反编译类文件的右上角的 Download Sources 便可以下载和关联源文件,下载完后双击选中 write(...) 方法,按 CTRL + ALT + H 便可以出现如图所示引用链

write 方法引用链.png

第一个便是我们要找的目标,进入到里面,直接定位关键代码:

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 ,然后调用 write () 方法,可以看到这里给 mediaType 赋的值是一个叫做 selectedMediaType 的变量,这个变量又是什么呢?继续搜索,发现下面这段代码:

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 中获取的第一个可以与请求类型 requestedMediaTypes 中某个类型所相兼容的类型,而所谓的 producibleMediaTypes 就是在 FastJsonHttpMessageConverter 中空参构造方法中所设置的 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 空参构造
    public FastJsonHttpMessageConverter() {
        super(MediaType.ALL);
    }

​千回万转,最终又回到了原点,这里设置的参数是 ALL ,点进 MediaType 中可以发现 ALL 的类型是 "*/*" ,也就是说匹配所有类型,因此 selectedMediaType 默认就为 requestedMediaTypes 中的第一个类型,即为 "text/html"

类型对比.png

到这里,差不多一切都明了了,FastJsonHttpMessageConverter 既没有在指定 contentType 时设置 defaultCharset ,也没有在 supportContentTypes 中设置 contentType 的具体类型和编码,会乱码也就不足为奇了。

解决

通过对源码的一番探寻,我们可以很容易的找出解决方案出来,这里提供两种方法,可以根据个人爱好采用。

  • 方案一,自定义 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);
        }
    
  • 方案二(针对 springboot ),在 application.properties 中添加 spring.http.encoding.force=true 这一行配置,表示强制使用 defaultCharset(因此也还是需要设置 defaultCharset)。

思考

两种解决方案,相比之下,第一种更明了也更灵活一点,毕竟 springboot 的思想便是零配置。springmvc 中默认的 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 就不需要呢?通过继承关系我们就可以明白,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); 这句代码便是进行了 contentType 的设置,它是其父类 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 中的大体意思差不多,都是在进行 contentType 和 contentLenth 的设置,但在 contentTypeToUse = new MediaType(contentTypeToUse, defaultCharset); 这句代码中,它给 contentType 指定了其编码类型,因此即使它的类型是 "text/html" ,但也能正常显示。

尾巴

虽然解决方案百度一下很快就能出来,但很多人都只是给了方案,而没有给原理,写这篇文章的目的不单单是为了解决问题,也顺便是为了探寻一下 springmvc 的执行流程,了解其内部对各个部件的调用流程,虽然花了点时间,不过所幸学到了不少的东西。

关于这个不知道算不算 bug 的 bug,我也在 github 上提了一个 issue ,希望能够有所改善吧。

​ ---完---

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

推荐阅读更多精彩内容