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
对象设置响应头 ContentType
1
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
。