美文网首页
FastJsonHttpMessageConverter 乱码解

FastJsonHttpMessageConverter 乱码解

作者: 张光光 | 来源:发表于2017-08-08 17:26 被阅读1041次

前言

最近在将 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 ,希望能够有所改善吧。

​ ---完---

相关文章

网友评论

      本文标题:FastJsonHttpMessageConverter 乱码解

      本文链接:https://www.haomeiwen.com/subject/bfxxrxtx.html