轉載自 http://blog.csdn.net/yiyuhanmeng/article/details/7441380
今番又遇到亂碼問題,有時候真覺得英語母語的那些地區確實挺省事的,至少不用為了這個經典麻煩去折騰。
網絡上討論亂碼問題的文章很多,因為各作者使用的計算機環境的不同,往往不是很全面。
這里非常推薦的一篇文章:
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一把我的頁面: