SpringMVC使用@ResponseBody输出字符串时遇到的乱码问题及解决办法

  categories:java资料  author:

今番又遇到乱码问题,有时候真觉得英语母语的那些地区确实挺省事的,至少不用为了这个经典麻烦去折腾。

网络上讨论乱码问题的文章很多,因为各作者使用的计算机环境的不同,往往不是很全面。
这里非常推荐的一篇文章:
http://dohkoos.name/java-garbled-analysis.html简 而言之,乱码的“根本原因是由于编码和解码采用的不是同一种码”。例如作者所举的例子,使用GBK编码为UTF-8,使用ISO-8859从UTF-8解 码,可能会导致乱码问题。这就好比有一篇中文文章想给王五看,不过这篇文章先由张三翻译成为了英文,然后再由李四翻译成俄文(而不是翻译回中文),但是王 五只看得懂中文,于是就麻瓜了。我们需要保持编码或者解码两头,所使用的字符集转换方向需要正好相反:使用 GBK –> UTF-8 与 UTF-8 –> GBK。由于Java采用了UTF-8编码,所以编码解码均以UTF-8为中介。
对于翻译而言,就是先相当于: 先 中译英,对应的解码,反过来就是 英译中。

遇到乱码问题,通常的检查项包括:
1. 编辑器保存文件的字符集;
2. 数据库的字符集;
3. 应用服务器或者Web服务器处理字符串采用的字符集
4. JSP对于字符集声明
5. Servlet过滤器,以及MVC框架拦截器对于字符集的处理
6. 其它涉及字符集处理的环节

检查各个环节,统一按UTF-8设置。推断我这次碰到的问题属于上述第6中情况。

因为是通过SpringMVC提供的注解@ResponseBody来返回一个JSON字符串,然后在客户端上解析JSON(现如今以JSON作为数据交换格式貌似越来越时髦了,客户端我用的比较多的是jqGrid或者ExtJS)。

Controller代码如下:

@Controller

@RequestMapping(“/*”)

public class HelloController {

private transient final Log log = LogFactory.getLog(HelloController.class);

 

@Autowired

private UserManager mgr = null;

 

@RequestMapping(value=“hello_list.do”, method = RequestMethod.POST)

@ResponseBody

public String helloList() {

StringBuilder str = new StringBuilder(“{totalProperty:100,root:[“);

 

List<User> users = mgr.getUsers();

for (User user : users) {

str.append(“{id: “).append(user.getId());

str.append(“, name:'”).append(user.getLastName());

str.append(“‘, descn:'”).append(user.getFullName()).append(“‘},”);

}

str.append(“{id:4, name:’생활’, descn:’Китай’},”);

str.append(“{id:5, name:’tchen8′, descn:’中文’}]}”);

 

log.info(str.toString());

 

return str.toString();

}

 

}

在Spring配置文件里,默认如下:

<!– Enables the Spring MVC @Controller programming model –>

<mvc:annotation-driven />

调试程序,控制台输出日志看到是中文,但是在firebug中看到的服务器端送过来的字符串是???? (如果是 “口口口”这样的输出,需要先排除是否为系统的字体缺失),于是判断是服务器最后往端口写字符串流的时侯字符集不对。

通 过调试跟踪Spring的源码,声明@ResponseBody时,Spring会通过AnnotationMethodHandlerAdapter去 寻找对应的HttpMessageConverter, 我们这里声明返回的类型是String,于是对应StringHttpMessageConverter。通过实验,猜测这个 StringHttpMessageConverter也就是<mvc:annotation-driven />触发的默认的字符串转换工作类。

比较不幸的是,StringHttpMessageConverter所使用的默认字符集是ISO-8859-1

……

public class StringHttpMessageConverter extends AbstractHttpMessageConverter<String> {

public static final Charset DEFAULT_CHARSET = Charset.forName(“ISO-8859-1″);

……

这里不得不提的是与StringHttpMessageConverter 同级的类MappingJacksonHttpMessageConverter,天知道是什么原因:同一个作者,对于这两个类,默认字符集一个是ISO-8859-1,一个是UTF-8。

既然事已如此,那就想办法把这个地方用到的ISO-8859-1也改成UTF-8了。有两个思路:
1. 替换默认字符集;
2. 替换StringHttpMessageConverter

搜索了一下,先看到这个解决办法:
http://forum.springsource.org/showthread.php?t=81858
这 里提供的是使用一个所谓的ConfigurableStringHttpMessageConverter来替代 StringHttpMessageConverter,基本的思路技术是:由于StringHttpMessageConverter中的默认字符集变 量声明为final,无法直接通过继承去覆盖,那就把StringHttpMessageConverter照抄一遍,构造函数中新增一个代表字符集的输 入参数,然后在配置文件里面通过构造方法注入UTF-8。在配置文件中,将这个Bean声明在<mvc:annotation-driven />前面,从而能够先于StringHttpMessageConverter被Spring识别和注入。

但是这个方法多少有些蛮干的味道,基于它简化的一个版本可以如下,即通过继承StringHttpMessageConverter,然后在子类中注入我们想要的字符集配置:

public class MyStringHttpMessageConverter extends StringHttpMessageConverter {

 

public MyStringHttpMessageConverter(Charset defaultCharset) {

List<MediaType> mediaTypeList = new ArrayList<MediaType>();

mediaTypeList.add(new MediaType(“text”“plain”, defaultCharset));

mediaTypeList.add(MediaType.ALL);

super.setSupportedMediaTypes(mediaTypeList);

}

 

}

Bean的配置依然类似:

<beans:bean class=“org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter”>

<beans:property name=“messageConverters”>

<util:list>

<beans:bean id=“stringHttpMessageConverter” class=“org.tchen8.myapp.common.ConfigurableStringHttpMessageConverter”>

<beans:constructor-arg value=“UTF-8″ />

</beans:bean>

</util:list>

</beans:property>

</beans:bean>

 

<!– Enables the Spring MVC @Controller programming model –>

<mvc:annotation-driven />

上面的办法是以属性注入的方式,替换了默认的字符集,但为此也需要把converter替换。

另外一个比较简洁的办法,则不需要自己写converter类,而是直接通过属性注入,修改StringHttpMessageConverter的默认配置。

<beans:bean class=“org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter”>

<beans:property name=“messageConverters”>

<util:list>

<beans:bean class=“org.springframework.http.converter.StringHttpMessageConverter”>

<beans:property name=“supportedMediaTypes”>

<util:list>

<beans:value>text/html;charset=UTF-8</beans:value>

</util:list>

</beans:property>

</beans:bean>

</util:list>

</beans:property>

</beans:bean>

上面的这个办法,实际上通过setSupportedMediaTypes方法,其实也就是StringHttpMessageConverter在类注释中所提到的办法:

如果再多看一下StringHttpMessageConverter的源码,可以到它的父类中AbstractHttpMessageConverter有这么个方法:

/**

     * Returns the default content type for the given type. Called when {@link #write}

     * is invoked without a specified content type parameter.

     * <p>By default, this returns the first element of the

     * {@link #setSupportedMediaTypes(List) supportedMediaTypes} property, if any.

     * Can be overridden in subclasses.

     * @param t the type to return the content type for

     * @return the content type, or <code>null</code> if not known

     */

protected MediaType getDefaultContentType(T t) {

List<MediaType> mediaTypes = getSupportedMediaTypes();

return (!mediaTypes.isEmpty() ? mediaTypes.get(0) : null);

}

注释中写的明白:”Can be overridden in subclasses.” 那就不必客气了。于是我们大概也能有如下的做法:

public class MyStringHttpMessageConverter2 extends StringHttpMessageConverter {

private static final MediaType utf8 = new MediaType(“text”“plain”, Charset.forName(“UTF-8″));

@Override

protected MediaType getDefaultContentType(String dumy) {

return utf8;

}

}

对应的配置:

<beans:bean class=“org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter”>

<beans:property name=“messageConverters”>

<util:list>

<beans:bean id=“myStringHttpMessageConverter2″ class=“org.tchen8.myapp.common.MyStringHttpMessageConverter2″ />

</util:list>

</beans:property>

</beans:bean>

 

<!– Enables the Spring MVC @Controller programming model –>

<mvc:annotation-driven />

以 上的几个方法,都能解决@ResponseBody导致的乱码问题,虽然StringHttpMessageConverter将来确实有可能把默认字符 集修改成UTF-8,从而导致上述功夫最后变成白忙活。但也确实感谢有这么个小阻碍,迫使自己去分析问题寻找答案。收获不在于结果,而在过程吧

最后show一把我的页面:



快乐成长 每天进步一点点