spring源码解析-web系列(八):使用@ResponseBody时中文乱码的解决

乱码的现象

先上代码:前端

代码1:web

@Controller
public class TestController {

    @RequestMapping(value = "/a")
    @ResponseBody
    public Object a() throws Exception {
        MyResponse myResponse = new MyResponse();
        myResponse.code = 200;
        myResponse.msg = "hello world, 你好世界。";
        return myResponse;
    }

    @RequestMapping(value = "/b")
    @ResponseBody
    public String b() throws Exception {
        return "hello world, 你好世界。";
    }

    static class MyResponse<T> {
        public int code;
        public String msg;
        public T data;
    }
}

代码2:spring

~ curl -X GET "http://localhost:8080/a"
{"code":200,"msg":"hello world, 你好世界。","data":null}
~ curl -X GET "http://localhost:8080/b"
hello world, ?????

咱们使用spring mvc开发restful接口的通常模式如代码1第4第11行代码所示,使用@ResponseBody注解一个方法,表示把该方法的返回值写进response的body里。同时声明一个对象,形如MyResponse,做为controller方法的返回值,统一restful接口的结构。代码2第1第2行调用该接口,正常返回中文字符,不乱码。json

若是咱们的返回值不须要最外层的结构,返回给用户的就是我咱们处理后的一个字符串,把这个字符串存放在response的body里返回,那么这个方法瓜熟蒂落的被写成代码1第13第17行的样子。代码2第3第4行是调用这个接口的过程,会出现中文乱码。restful

分析缘由

渲染的过程

面对乱码,最直接的解决办法就是查看方法的返回值是如何被写到response的body里的,这就是渲染的过程,咱们知道,spring mvc有两个地方会渲染,上面的状况会在 ServletInvocableHandlerMethod 类里进行渲染,能够参考spring源码解析-web系列(四):九大组件之HandlerAdapter文章里 ServletInvocableHandlerMethod 部分,spring源码解析-web系列(四):九大组件之HandlerAdapter文章代码8第20~第21行执行渲染过程,渲染的代码以下:mvc

代码3 (org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite):app

@Override
    public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
            ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {

        HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
        if (handler == null) {
            throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
        }
        handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
    }

    @Nullable
    private HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) {
        boolean isAsyncValue = isAsyncReturnValue(value, returnType);
        for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
            if (isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) {
                continue;
            }
            if (handler.supportsReturnType(returnType)) {
                return handler;
            }
        }
        return null;
    }

代码3第5行获取handler,代码3第9行调用handler的handleReturnValue方法渲染返回值。对于咱们的这个方法,获取到的handler为RequestResponseBodyMethodProcessor类型。curl

代码4 (org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.handleReturnValue):ide

public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
        mavContainer.setRequestHandled(true);
        ServletServerHttpRequest inputMessage = this.createInputMessage(webRequest);
        ServletServerHttpResponse outputMessage = this.createOutputMessage(webRequest);
        this.writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
    }

RequestResponseBodyMethodProcessor的handleReturnValue方法如代码4所示,它调用了父类的writeWithMessageConverters方法完成渲染。svg

代码5 (org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters):

protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType, ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
        Object outputValue;
        Class valueType;
        Object declaredType;
        if (value instanceof CharSequence) {
            outputValue = value.toString();
            valueType = String.class;
            declaredType = String.class;
        } else {
            outputValue = value;
            valueType = this.getReturnValueType(value, returnType);
            declaredType = this.getGenericType(returnType);
        }

        if (this.isResourceType(value, returnType)) {
            outputMessage.getHeaders().set("Accept-Ranges", "bytes");
            if (value != null && inputMessage.getHeaders().getFirst("Range") != null) {
                Resource resource = (Resource)value;

                try {
                    List<HttpRange> httpRanges = inputMessage.getHeaders().getRange();
                    outputMessage.getServletResponse().setStatus(HttpStatus.PARTIAL_CONTENT.value());
                    outputValue = HttpRange.toResourceRegions(httpRanges, resource);
                    valueType = outputValue.getClass();
                    declaredType = RESOURCE_REGION_LIST_TYPE;
                } catch (IllegalArgumentException var17) {
                    outputMessage.getHeaders().set("Content-Range", "bytes */" + resource.contentLength());
                    outputMessage.getServletResponse().setStatus(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE.value());
                }
            }
        }

        MediaType contentType = outputMessage.getHeaders().getContentType();
        Object mediaTypesToUse;
        if (contentType != null && contentType.isConcrete()) {
            mediaTypesToUse = Collections.singletonList(contentType);
        } else {
            HttpServletRequest request = inputMessage.getServletRequest();
            List<MediaType> requestedMediaTypes = this.getAcceptableMediaTypes(request);
            List<MediaType> producibleMediaTypes = this.getProducibleMediaTypes(request, valueType, (Type)declaredType);
            if (outputValue != null && producibleMediaTypes.isEmpty()) {
                throw new HttpMessageNotWritableException("No converter found for return value of type: " + valueType);
            }

            mediaTypesToUse = new ArrayList();
            Iterator var13 = requestedMediaTypes.iterator();

            while(var13.hasNext()) {
                MediaType requestedType = (MediaType)var13.next();
                Iterator var15 = producibleMediaTypes.iterator();

                while(var15.hasNext()) {
                    MediaType producibleType = (MediaType)var15.next();
                    if (requestedType.isCompatibleWith(producibleType)) {
                        ((List)mediaTypesToUse).add(this.getMostSpecificMediaType(requestedType, producibleType));
                    }
                }
            }

            if (((List)mediaTypesToUse).isEmpty()) {
                if (outputValue != null) {
                    throw new HttpMediaTypeNotAcceptableException(producibleMediaTypes);
                }

                return;
            }

            MediaType.sortBySpecificityAndQuality((List)mediaTypesToUse);
        }

        MediaType selectedMediaType = null;
        Iterator var21 = ((List)mediaTypesToUse).iterator();

        while(var21.hasNext()) {
            MediaType mediaType = (MediaType)var21.next();
            if (mediaType.isConcrete()) {
                selectedMediaType = mediaType;
                break;
            }

            if (mediaType.equals(MediaType.ALL) || mediaType.equals(MEDIA_TYPE_APPLICATION)) {
                selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
                break;
            }
        }

        HttpMessageConverter converter;
        GenericHttpMessageConverter genericConverter;
        label138: {
            if (selectedMediaType != null) {
                selectedMediaType = selectedMediaType.removeQualityValue();
                var21 = this.messageConverters.iterator();

                while(var21.hasNext()) {
                    converter = (HttpMessageConverter)var21.next();
                    genericConverter = converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter)converter : null;
                    if (genericConverter != null) {
                        if (((GenericHttpMessageConverter)converter).canWrite((Type)declaredType, valueType, selectedMediaType)) {
                            break label138;
                        }
                    } else if (converter.canWrite(valueType, selectedMediaType)) {
                        break label138;
                    }
                }
            }

            if (outputValue != null) {
                throw new HttpMediaTypeNotAcceptableException(this.allSupportedMediaTypes);
            }

            return;
        }

        outputValue = this.getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType, converter.getClass(), inputMessage, outputMessage);
        if (outputValue != null) {
            this.addContentDispositionHeader(inputMessage, outputMessage);
            if (genericConverter != null) {
                genericConverter.write(outputValue, (Type)declaredType, selectedMediaType, outputMessage);
            } else {
                converter.write(outputValue, selectedMediaType, outputMessage);
            }

            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Written [" + outputValue + "] as \"" + selectedMediaType + "\" using [" + converter + "]");
            }
        }

    }

代码5有点长,我先补充一点前置知识:HandlerMethodReturnValueHandler(咱们用的是RequestResponseBodyMethodProcessor实现类)使用来处理(渲染)咱们的controller方法的返回值的,可是它也不是直接渲染的,而是借助 HttpMessageConverter 的概念, HttpMessageConverter 的做用是:从request读取数据;把数据写入response。为了灵活通用可扩展, HttpMessageConverter 也是按照spring的套路实现的,每一个 HttpMessageConverter 能够判断本身是否支持某种类型的处理,在调用它的地方有若干个 HttpMessageConverter ,循环这些 HttpMessageConverter ,找到支持的Converter来处理。 HttpMessageConverter 接口定义以下:

代码6 (org.springframework.http.converter.HttpMessageConverter):

public interface HttpMessageConverter<T> {
    boolean canRead(Class<?> var1, @Nullable MediaType var2);

    boolean canWrite(Class<?> var1, @Nullable MediaType var2);

    List<MediaType> getSupportedMediaTypes();

    T read(Class<? extends T> var1, HttpInputMessage var2) throws IOException, HttpMessageNotReadableException;

    void write(T var1, @Nullable MediaType var2, HttpOutputMessage var3) throws IOException, HttpMessageNotWritableException;
}

经过代码6能够发现: HttpMessageConverter 在判断是否能够渲染时须要对象的类型和MediaType两个信息,对象的类型就是咱们的controller方法返回的对象类型,MediaType就是response里的content-type。HttpMessageConverter 的 getSupportedMediaTypes 方法返回这个 HttpMessageConverter 对应的能够写入的content-type的值。

代码5第2行~第85行获取 selectedMediaType ,selectedMediaType就是写进response的content-type。代码2第87第112行获取HttpMessageConverter。代码5第114第126行使用HttpMessageConverter进行渲染。

这里最关键的就是如何获取 selectedMediaType :代码5第39行获取request里header里的 Accept 设置的值,这里取到的值为 * / * 。代码5第40行获取这个controller处理之后写入到response里的content-type的值。获取过程以下:

代码7 (org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.getProducibleMediaTypes):

protected List<MediaType> getProducibleMediaTypes(HttpServletRequest request, Class<?> valueClass, @Nullable Type declaredType) {
        Set<MediaType> mediaTypes = (Set)request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
        if (!CollectionUtils.isEmpty(mediaTypes)) {
            return new ArrayList(mediaTypes);
        } else if (this.allSupportedMediaTypes.isEmpty()) {
            return Collections.singletonList(MediaType.ALL);
        } else {
            List<MediaType> result = new ArrayList();
            Iterator var6 = this.messageConverters.iterator();

            while(true) {
                while(var6.hasNext()) {
                    HttpMessageConverter<?> converter = (HttpMessageConverter)var6.next();
                    if (converter instanceof GenericHttpMessageConverter && declaredType != null) {
                        if (((GenericHttpMessageConverter)converter).canWrite(declaredType, valueClass, (MediaType)null)) {
                            result.addAll(converter.getSupportedMediaTypes());
                        }
                    } else if (converter.canWrite(valueClass, (MediaType)null)) {
                        result.addAll(converter.getSupportedMediaTypes());
                    }
                }

                return result;
            }
        }
    }

代码7第2行,先获取request里的HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE属性,这个属性就是咱们配置在@RequestMapping注解里的produces里的值,在HandlerMapping获取handler时被赋值到request里。若是有值,直接返回;不然代码7第8~第24行,循环全部的 HttpMessageConverter ,使用MediaType为null,判断HttpMessageConverter是否能够渲染这个对象,若是能够渲染,则把HttpMessageConverter支持的content-type返回。

代码5第68行对找到的MediaType进行排序,代码5第71~第85行,获取最合适的MediaType。

我见到的乱码是怎么出现的

渲染的过程分析完了,那么乱码究竟是怎么出现的呢?由于controller的返回值不一样,致使找到的 HttpMessageConverter 不一样,进而致使 MediaType 不一样,因此前端看到的是乱码。

对于代码1第6行的a方法,因为@RequestMapping注解里produces属性为空且返回值为MyResponse类型,在获取producibleMediaTypes时, 支持的 HttpMessageConverter 有MappingJackson2HttpMessageConverter,获取到的producibleMediaTypes为 application/json 和 application/*+json 两个,选择到最优的MediaType为 application/json ,最后会被MappingJackson2HttpMessageConverter渲染,在MappingJackson2HttpMessageConverter中添加defaultCharset,因为MappingJackson2HttpMessageConverter中的defaultCharset为 UTF-8 ,最终content-type为 application/json;charset=UTF-8 。

对于代码1第15行的b方法,因为@RequestMapping注解里produces属性为空且返回值为String类型,在获取producibleMediaTypes时, 支持的 HttpMessageConverter 有 StringHttpMessageConverter 和 MappingJackson2HttpMessageConverter 两个,获取到的producibleMediaTypes为 text/plain 、 * / * 、 application/json 、 application/*+json 四个,选择到最优的MediaType为 text/plain ,最后会被StringHttpMessageConverter渲染,在StringHttpMessageConverter中添加defaultCharset,因为StringHttpMessageConverter中的defaultCharset为 ISO-8859-1 ,最终content-type为 text/plain;charset=ISO-8859-1 。

下面代码能够证实上面的分析:

代码8:

~ curl -i -X GET "http://localhost:8080/a"
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Mon, 12 Aug 2019 09:10:10 GMT

{"code":200,"msg":"hello world, 你好世界。","data":null}

~
~
~
~ curl -i -X GET "http://localhost:8080/b"
HTTP/1.1 200
Content-Type: text/plain;charset=ISO-8859-1
Content-Length: 18
Date: Mon, 12 Aug 2019 09:10:12 GMT

hello world, ?????

总结

这个bug的缘由就是方法返回值不一样引发的MediaType不一样,进而致使content-type不一样。 建议咱们在使用的时候指定@RequestMapping注解的produces属性。