HttpMessageConverter - 响应中文内容乱码问题
HttpMessageConverter - 响应中文内容乱码问题
在讲解 @ResponseBody 注解时,我们测试了四种响应数据到浏览器的情况:
- 超链接发起请求,服务器通过 Servlet API 的 response 对象响应字符串到浏览器
- 超链接发起请求,服务器使用 @ResponseBody 响应字符串到浏览器
- 超链接发起请求,服务器使用 @ResponseBody 响应一个 Java 对象到浏览器
- Axios 发起 Ajax 请求,服务器使用 @ResponseBody 响应字符串到浏览器
当响应数据中包含中文内容时,前两种会出现中文内容乱码现象!
为什么 CharacterEncodingFilter 没有作用?
还记得,在 web.xml 中曾经配置了一个 CharacterEncodingFilter,如下:
1 | <filter> |
我们明明将 forceResponseEncoding 属性设为了 true,为什么会出现响应中文乱码呢?
这是因为,forceResponseEncoding=true 时,CharacterEncodingFilter 只是帮我们设置了 response.setCharacterEncoding("UTF-8"),让服务器按 UTF-8 格式来编码响应数据。但是浏览器接收到服务器的响应数据后,它并不知道该响应数据的编码格式是什么?
所以,一般来说,我们会同时在响应头中设置 ContentType 来告诉浏览器,所返回的响应数据的编码是什么,比如 response.setContentType(text/html;charset=utf-8) 就是告诉浏览器,返回的响应体是个 html 页面,且编码是 utf-8。
小结一下,响应中文内容乱码问题,原因就是没有设置好 ContentType 响应头!上面提到的前两种情况出现中文乱码,都是这个原因。
标注 @ResponseBody 的控制器方法的返回值,是如何被转换为响应体的?
控制器方法一旦被 @ResponseBody 标注后,RequestResponseBodyMethodProcessor 中的 handleReturnValue 方法会被调用,来完成返回值数据到响应体的转换。
1 |
|
该方法接收的 returnValue 即控制器方法的返回值,returnType 即控制器方法的返回类型。该方法中最关键的是 writeWithMessageConverters。
调用 writeWithMessageConverters 方法需要传入 4 个参数:returnValue 返回值,returnType 返回类型,inputMessage 其实就是对请求 request 的一个封装,outputMessage 其实就是对响应 response 的一个封装。
该函数中做了很多重要的工作,我们主要关心其中的:
- 综合请求头中的
Accept信息和控制器方法的returnType返回类型信息,确定最终的生产的媒体类型,即确定最终响应头的ContentType信息。
1 | // 从请求头 `Accept` 中获取浏览器希望响应的媒体类型 |
默认情况下,所有已注册的 HttpMessageConverter 包括 8 个,查看 messageConverters 属性:
1 | 0 = {ByteArrayHttpMessageConverter@17781} |
需要强调的是,这些转换器在列表中的位置代表了其优先级,ByteArrayHttpMessageConverter 最高,MappingJackson2HttpMessageConverter 最低。
另外,如果我又注册了一个 StringHttpMessageConverter,那么最终该属性中将包含 9 个值,其中包含两个 StringHttpMessageConverter 类型的转换器,我自己注册那个优先级最高放在最前面。
1 | // 进行兼容性匹配,得到既能满足 Accept 又能处理 `returnType` 返回类型的 converter 所支持的媒体类型 |
以上步骤完成后,selectedMediaType 即为最终选择的要放到响应头中的 ContentType
- 根据之前最终选择的
ContentType,以及返回值类型returnType,选取最合适的HttpMessageConverter,然后执行returnValue到响应体的转换。
1 | // 遍历所有转换器,排前面的先被遍历,所以说优先级高 |
converter.write 方法底层会将 selectedMediaType 设置到响应头 ContentType 中。
第一种情况:超链接请求 + Servlet response 响应字符串
1 | <a th:href="@{/testResponse}">通过原生 Servlet API 的 response 对象响应数据到浏览器</a><br> |
1 |
|
乱码原因:响应头信息中没有设置 ContentType,浏览器不知道响应体的具体编码是什么,从而产生乱码。
解决方案:直接通过 response 对象设置响应头 ContentType1
2
3
4
5
public void testResponse(HttpServletResponse response) throws IOException {
response.setContentType("text/html;charset=utf-8");
response.getWriter().print("hello, response 张三");
}
第二种情况:超链接请求 + @ResponseBody 响应字符串
1 | <a th:href="@{/testResponseBody}">通过 @ResponseBody 响应数据到浏览器</a><br> |
1 |
|
乱码原因:响应头信息中虽然设置了 ContentType,但是编码不是 UTF-8,而是 ISO-8859-1,从而导致乱码。
综合超链接请求的 Accept 内容,以及 testResponseBody 返回值类型是 String,最终确定的媒体类型是 text/html,且最终将会选择 StringHttpMessageConverter 来进行响应报文信息的转换。
StringHttpMessageConverter 在处理 text/html 和 application/json 数据时有不同的处理方式:
text/html类型:将text/html和默认编码一并设置到响应头的ContentType中application/json类型:直接将application/json设置到响应头的ContentType中,直接忽略默认编码,因为服务器会自动为该类型添加UTF-8编码。
StringHttpMessageConverter 的默认编码是 ISO-8859-1,因此处理 text/html 类型时,最终响应头中的 ContentType=text/html/charset=iso-8859-1,产生乱码。
1 | public static final Charset DEFAULT_CHARSET = StandardCharsets.ISO_8859_1; |
解决方案一:在 RequestMapping 中增加 produces 属性,明确指定 ContentType
1 |
|
注意,对于第一种情况,也可以采用这种解决方案。
解决方案二:配置 <mvc:message-converters>,注册一个新的 StringHttpMessageConverter ,并使其默认编码为 UTF-8
打开 SpringMVC 的配置文件,在开启 mvc 注解驱动的标签内部添加配置:
1 | <mvc:annotation-driven> |
新注册的 StringHttpMessageConverter 优先级最高,因此会选择该默认编码为 UTF-8 的新转换器进行转换。
PS:SpringBoot 中自动配置了两个 StringHttpMessageConverter,优先级较高的那个其默认编码为 UTF-8,优先级较低的那个默认编码为 ISO-8859-1。
UTF-8 编码的 StringHttpMessageConverter 相关的自动配置,详情看如下代码:
1 | // HttpMessageConvertersAutoConfiguration.java |
第三种情况:超链接请求 + @ResponseBody 响应 Java 对象
1 | <a th:href="@{/testResponseUser}">通过 @ResponseBody 响应 User 对象到浏览器</a><br> |
1 |
|
无乱码原因:
综合超链接请求的 Accept 内容,以及 testResponseUser 返回值类型是 User,最终确定的媒体类型是 application/json,且最终将会选择 MappingJackson2HttpMessageConverter 来进行响应报文信息的转换。
MappingJackson2HttpMessageConverter 无默认编码,因此只将 application/json 设置到响应头中。
对于 application/json 类型的响应体,服务器默认通知浏览器采用 UTF-8 格式进行编码,因此最终响应头依然会是 ContentType=application/json;charset=UTF-8。
第四种情况:Ajax 请求 + @ResponseBody 响应字符串
1 | <div id="app"> |
1 |
|
无乱码原因:
综合 Ajax 请求的 Accept 内容,以及 testAxios 返回值类型是 String,最终确定的媒体类型是 application/json,且最终将会选择 StringHttpMessageConverter 来进行响应报文信息的转换。
对于 application/json 类型,StringHttpMessageConverter 直接将 application/json 设置到响应头的 ContentType 中,直接忽略默认编码。
对于 application/json 类型的响应体,服务器默认通知浏览器采用 UTF-8 格式进行编码,因此最终响应头依然会是 ContentType=application/json;charset=UTF-8。