为什么 CharacterEncodingFilter 没有生效

    <filter>
        <filter-name>CharacterEncodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>utf-8</param-value>
        </init-param>
        <init-param>
            <param-name>forceEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>

    <filter-mapping>
        <filter-name>CharacterEncodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

这个配置, 只有在 Controller 里,调用 HttpServletResponse 直接写数据时才会生效的 .如果是直接利用HttpServletResponse,就不能再用@ResponseBody了,例如下面这样子写就会报错:

    @RequestMapping("/index")
    @ResponseBody
    public String index(HttpServletResponse response) throws IOException {
        response.getWriter().write("你好,中文");
        return "中文";
    }

错误为:

Caused by:
java.lang.IllegalStateException: WRITER
	at org.eclipse.jetty.server.Response.getOutputStream(Response.java:706)
	at javax.servlet.ServletResponseWrapper.getOutputStream(ServletResponseWrapper.java:142)
	at org.springframework.session.web.http.OnCommittedResponseWrapper.getOutputStream(OnCommittedResponseWrapper.java:124)

直接用 HttpServletResponse 写数据

    @RequestMapping("/index")
    public void index(HttpServletResponse response) throws IOException {
        response.getWriter().write("你好,中文");
    }

➜  ~  curl http://localhost:8080/index
你好,中文
➜  ~

使用@ResponseBody

    @RequestMapping("/index")
    @ResponseBody
    public String index(HttpServletResponse response) throws IOException {
        return "你好,中文";
    }

➜  ~  curl http://localhost:8080/index
?????%                                                                                                                                                                                                    ➜  ~

可以看到,返回的是乱码了.

原因

因为默认情况下,如果Controller返回的是String的@ResponseBody话,Spring会调用默认的StringHttpMessageConverter来返回String,而这个StringHttpMessageConverter默认情况下,只是使用public static final Charset DEFAULT_CHARSET = Charset.forName("ISO-8859-1");这个字符集,所以就会报错了.

解决

方法一:添加指定的MessageConverter

	<mvc:annotation-driven>
			<!-- register custom converter that returns UTF-8 encoded response-body by defualt -->
			<mvc:message-converters register-defaults="true">
				<bean class="org.springframework.http.converter.StringHttpMessageConverter">
					<constructor-arg index="0" name="defaultCharset" value="UTF-8"/>
				</bean>
			</mvc:message-converters>
	</mvc:annotation-driven>

注意register-defaults这表示是否也注册默认的HttpMessageConverter(默认也是true

这样子,就注册多了一个StringHttpMessageConverter了,默认情况下的HttpMessageConverter有以下9个:

默认注册的 HttpMessageConverter

0 = {ByteArrayHttpMessageConverter@6398}
1 = {StringHttpMessageConverter@6399}
2 = {ResourceHttpMessageConverter@6400}
3 = {SourceHttpMessageConverter@6401}
4 = {AllEncompassingFormHttpMessageConverter@6402}
5 = {AtomFeedHttpMessageConverter@6403}
6 = {RssChannelHttpMessageConverter@6404}
7 = {Jaxb2RootElementHttpMessageConverter@6405}
8 = {MappingJacksonHttpMessageConverter@6406}

注意Jackson的,要添加相关的Jackson的依赖包.

现在添加多了一个的话,就有10个了:

0 = {StringHttpMessageConverter@6398}
 defaultCharset = {UTF_8@6408} "UTF-8"
 availableCharsets = {ArrayList@6409}  size = 170
 writeAcceptCharset = true
 logger = {SLF4JLocationAwareLog@6410}
 supportedMediaTypes = {ArrayList@6411}  size = 2
1 = {ByteArrayHttpMessageConverter@6399}
2 = {StringHttpMessageConverter@6400}
 defaultCharset = {ISO_8859_1@6413} "ISO-8859-1"
 availableCharsets = {ArrayList@6414}  size = 170
 writeAcceptCharset = false
 logger = {SLF4JLocationAwareLog@6410}
 supportedMediaTypes = {ArrayList@6415}  size = 2
3 = {ResourceHttpMessageConverter@6401}
4 = {SourceHttpMessageConverter@6402}
5 = {AllEncompassingFormHttpMessageConverter@6403}
6 = {AtomFeedHttpMessageConverter@6404}
7 = {RssChannelHttpMessageConverter@6405}
8 = {Jaxb2RootElementHttpMessageConverter@6406}
9 = {MappingJacksonHttpMessageConverter@6407}

可以看到,我们自己定义的StringHttpMessageConverter放在第一位了,字符集为UTF-8,而Spring默认的SpringHttpMessageConverterISO-8859

这时可以看到可以返回正常的中文了:

    @RequestMapping("/index")
    @ResponseBody
    public String index(HttpServletResponse response) throws IOException {
        return "你好,中文";
    }

➜  ~  curl http://localhost:8080/index
你好,中文%                                                                                                                                                                                                ➜  ~

方法二:指定 produces

    @RequestMapping(value = "/index", produces = "text/plain; charset=utf-8")
    @ResponseBody
    public String index(HttpServletResponse response) throws IOException {
        return "你好,中文";
    }

➜  ~  curl http://localhost:8080/index
你好,中文%                                                                                                                                                                                                ➜  ~

这样子,就算不配置指定UTF-8字符集的StringHttpMessageConverter,也可以正常返回中文了。因为源码里有处理,如果有指定charset的话,就会使用指定的字符集来写数据.这里以StringHttpMessageConverter为例:

	@Override
	protected void writeInternal(String s, HttpOutputMessage outputMessage) throws IOException {
		if (this.writeAcceptCharset) {
			outputMessage.getHeaders().setAcceptCharset(getAcceptedCharsets());
		}
		Charset charset = getContentTypeCharset(outputMessage.getHeaders().getContentType());
		StreamUtils.copy(s, charset, outputMessage.getBody());
	}

其他对象的转换器同理.

参考资料

  1. Spring HttpMessageConverter

  2. cnblogs fangjian0423

  3. Cihatkeser

  4. pigg