關(guān)于Java平臺(tái)國(guó)際化數(shù)據(jù)編碼常見(jiàn)問(wèn)題分析 王旭科
一、前言
(CCW)經(jīng)常在看到有關(guān)討論Java相關(guān)技術(shù)的漢字的顯示,數(shù)據(jù)存儲(chǔ)漢字亂碼的解決方法。本人從事Java平臺(tái)相關(guān)開(kāi)發(fā)也有近5年的時(shí)間,深感這種有關(guān)數(shù)據(jù)編碼(Encoding)的問(wèn)題,不是一個(gè) 簡(jiǎn)單的描述就可以指出問(wèn)題的原因所在。目前零星的解決方案沒(méi)有提供一個(gè)全面的解釋。不同的瀏覽器,Web Server,Application Server,Database,支持的JDK版本不同,及設(shè)計(jì)的架構(gòu)不周全,其組合是一個(gè)龐大的基數(shù),下面就從B/S 3層架構(gòu)的角度,說(shuō)明國(guó)際化的數(shù)據(jù)編碼常見(jiàn)問(wèn)題的分析及解決方法。希望能分享自己的經(jīng)驗(yàn)。
二、常見(jiàn)Java應(yīng)用架構(gòu)
以上是基本的Java常見(jiàn)架構(gòu),實(shí)際中可能Web server和應(yīng)用服務(wù)器(Application Server)合二為一,不過(guò)為了清晰起見(jiàn),還是分開(kāi)顯示.
2.1 請(qǐng)求/響應(yīng)(Request/Response) 工作模式
基于B/S架構(gòu)的應(yīng)用的模型,就是終端用戶(hù)通過(guò)瀏覽器向web Server發(fā)送請(qǐng)求,Web解釋并響應(yīng)請(qǐng)求。其事件流模型可以參考標(biāo)準(zhǔn)HTTP協(xié)議獲得細(xì)節(jié)。常見(jiàn)的數(shù)據(jù)轉(zhuǎn)換及傳送發(fā)生以下三個(gè)環(huán)節(jié).所有的轉(zhuǎn)換都是雙向的.負(fù)責(zé)每個(gè)環(huán)節(jié)數(shù)據(jù)編碼(Encoding)的角色不同,需要對(duì)每個(gè)環(huán)節(jié)的角色進(jìn)行正確的參數(shù)設(shè)定互相配合才能得到期望的結(jié)果。
瀏覽器 ?----------?Web Server. Browser Render引擎/Web Server 引擎
WebServer ?-------? 應(yīng)用服務(wù)器 .JSP/Server 引擎及可能存在的其他連接WebServer
和Application Server的Plugin.
應(yīng)用服務(wù)器 ?-------------? 數(shù)據(jù)庫(kù) .每個(gè)數(shù)據(jù)庫(kù)廠家的JDBC驅(qū)動(dòng)器.
這里最重要和最易變的是Browser ?>WebServer ?-?JSP/Servet 之間的數(shù)據(jù)流和事件流發(fā)生的數(shù)據(jù)編碼,JDBC如果只是利用純粹的thin的模式來(lái)訪(fǎng)問(wèn)數(shù)據(jù)庫(kù),邏輯比較簡(jiǎn)單,如果通過(guò)不同數(shù)據(jù)庫(kù)廠家為了提高性能提供的利用本地client的方式來(lái)訪(fǎng)問(wèn)數(shù)據(jù)庫(kù),其配置復(fù)雜性隨不同廠家而不同,這里略過(guò)。以下對(duì)該事件流做詳細(xì)描述,以闡述后面發(fā)生的”內(nèi)幕”.
2.2 瀏覽器 HTML Render引擎如何顯示和提交數(shù)據(jù).核心的URL-Encoding.
瀏覽器渲染html頁(yè)面里的內(nèi)容如何編碼,決策順序如下。
首先瀏覽器根據(jù)Web Server發(fā)送的Content-Type Header,里的Charset信息來(lái)決定自己如何渲染html的顯示。如果沒(méi)有Content-Type,就根據(jù)Html頁(yè)面里的<meta>中的Content-Type來(lái)決定渲染的字符編碼.<meta>一般如下:
<META HTTP-EQUIV="Content-Type"CONTENT="text/html; CHARSET= “UTF-8">
一般出現(xiàn)亂碼的情況,有可能是content-type同實(shí)際數(shù)據(jù)不符,所以使用瀏覽器的”改變
編碼”的功能換一個(gè)字符集合,就能看到正確的數(shù)據(jù).如果以上有關(guān)字符編碼的信息都無(wú)
法得到,瀏覽器采用默認(rèn)的ISO-8859-1來(lái)渲染HTML頁(yè)面
其次,瀏覽器向WebServer通過(guò) Form提交數(shù)據(jù)的時(shí)候,其編碼數(shù)據(jù)的行為決策順序如
下
1.<Form>屬性的accept-charset,指定的字符編碼
2.<meta>指定的Content-Type
3.url-encoding 默認(rèn)的字符編碼.
這是標(biāo)準(zhǔn)按照HTML Internationalization (參考RFC 2070)規(guī)范的順序。
根據(jù)實(shí)際的經(jīng)驗(yàn),F(xiàn)ireBird 瀏覽器(或Mozilla Familly) 完全順從這個(gè)順序。
但I(xiàn)E 6 是2 <meta>指定的Content-Type優(yōu)先級(jí)別最高,所以,在HTML面在<body>
標(biāo)記之前,<head>標(biāo)記后第一個(gè)寫(xiě)入<meta>來(lái)聲明本頁(yè)的默認(rèn)字符編碼,是良好的習(xí)慣。
可以保證大多數(shù)瀏覽器可以正確渲染HTML數(shù)據(jù)。
如果沒(méi)有指定任何Content-Type,瀏覽器將按照iso-8859-1這種字符編碼.
URL-Encoding的詳細(xì)描述在(RFC 1738),重要的就是URL Encoding Converted.
“Only alphanumerics [0-9a-zA-Z], the special characters "$-_.+!*‘()," [not including the quotes - ed], and reserved characters used for their reserved purposes may be used unencoded within a URL."”
這種方式下,就是常見(jiàn)的%HH字符串的結(jié)果了。所有字符被編碼成 %HH字符串的方
式被傳遞.
如果<meta>中的charset指明的不符合實(shí)際數(shù)據(jù)或著指定的字符集合不包容實(shí)際輸入的
字符集合,就會(huì)造成編碼錯(cuò)誤,丟失數(shù)據(jù)信息。
總之,瀏覽器向Web Server提交數(shù)據(jù)的時(shí)候,根據(jù)URL-Encodeds 編碼數(shù)據(jù)并且設(shè)置
Content-Type 為application/x-www-form-urlencoded,但沒(méi)有傳送任何有關(guān)charset的信息。
2.2.1 小結(jié)
在HTML頁(yè)面或JSP/Servlet等動(dòng)態(tài)生成的頁(yè)面里,必須指定正確的<meta>信
息,才能保證數(shù)據(jù)顯示渲染和提交給Web Server數(shù)據(jù)是正確編碼的。如果沒(méi)有指定任
何charset的信息,瀏覽器是按照ISO-8859-1編碼顯示和提交數(shù)據(jù),可能造成數(shù)據(jù)信
息的丟失.
2.3 Web Server 如何接受Post/get的數(shù)據(jù)
通過(guò)2.2節(jié)的分析,我們可以知道,默認(rèn)的情況下,WebServer是按照原始數(shù)據(jù)(raw data)來(lái)接受數(shù)據(jù)的。寫(xiě)過(guò)CGI應(yīng)用應(yīng)該知道,這些數(shù)據(jù)是存放在服務(wù)器系統(tǒng)同應(yīng)用相關(guān)的環(huán)境變量Cache中,我們常說(shuō)的context(上下文)就包含了這些原始的提交數(shù)據(jù).
2.4 JSP/Servlet 如何獲取數(shù)據(jù)
當(dāng)使用Servlet調(diào)用getParameter或getParameters時(shí)候,通過(guò)Servlet包容器(Container)上下文來(lái)從WebServer環(huán)境變量中獲取原始數(shù)據(jù)并編碼,但由于沒(méi)有關(guān)于Charset的信息,所以此時(shí)設(shè)置正確的字符編碼,才能把被URL-Encoding的數(shù)據(jù),正確還原。這是通過(guò)設(shè)置request的setCharacterEncoding來(lái)設(shè)置正確的字符集,才能得到正確數(shù)據(jù)。
同時(shí)根據(jù)瀏覽器渲染HTML的規(guī)范,同樣送回瀏覽器的數(shù)據(jù)也必須指定正確的字符集才能保證瀏覽器正確編碼顯示,這是通過(guò)對(duì)response的setContentType方法調(diào)用來(lái)做到的。
在實(shí)際應(yīng)用中,了解了這些原理,不難寫(xiě)出正確的處理數(shù)據(jù)的應(yīng)用。Servlet 2.3規(guī)范提供了Filter技術(shù),可以完美解決Post數(shù)據(jù)和回應(yīng)信息的編碼問(wèn)題,具體例子見(jiàn)附錄1.
如果沒(méi)有Filter技術(shù),則需要使用常用的“重構(gòu)字符串”技術(shù)來(lái)解決這個(gè)問(wèn)題,代碼見(jiàn)附錄2
2.4.1 小結(jié):
判斷在Servlet中是否正確的重構(gòu)了提交的(Post) 數(shù)據(jù),一個(gè)常見(jiàn)的小技巧是通過(guò)System.out.println打印出數(shù)據(jù)到后端控制臺(tái),如果同系統(tǒng)當(dāng)前字符集相同的數(shù)據(jù)能正確顯示,表明重構(gòu)正確,比如你的服務(wù)器是Sun Solaris或Linux ,默認(rèn)語(yǔ)言是中文,那么中文數(shù)據(jù)就可以正確的被打印出來(lái),而不是一堆”?????”.
2.5 JDBC 如何保存數(shù)據(jù)庫(kù)
當(dāng)把通過(guò)2.4 正確重構(gòu)的數(shù)據(jù)要寫(xiě)入數(shù)據(jù)庫(kù)時(shí),同樣要考慮字符編碼的問(wèn)題。
首先必須在執(zhí)行JDBC或使用J2EE CMP通過(guò)setxxxx符值之前,調(diào)整數(shù)據(jù)的編碼同數(shù)據(jù)庫(kù)字符編碼一致,否則可能出錯(cuò)。這種轉(zhuǎn)換同具體使用JDBC Driver 的方式不同而有所不同。假設(shè)純粹使用thin(Type 3或Type 4)的方式,相對(duì)比較簡(jiǎn)單,只要知道正確的數(shù)據(jù)庫(kù)端的字符集,實(shí)現(xiàn)數(shù)據(jù)重構(gòu)為符合數(shù)據(jù)庫(kù)字符編碼的數(shù)據(jù)就可以了,代碼見(jiàn)附錄 2
三、通用國(guó)際化架構(gòu)
1、所有HTML或JSP/Servlet動(dòng)態(tài)頁(yè)面<meta>指明字符集合為UTF-8
2、Servlet 指明設(shè)置request獲取參數(shù)為UTF-8
3、Servlet 指明reponse Content-Type 的charset為UTF-8
4、數(shù)據(jù)庫(kù)編碼指明為UTF-8
這是最簡(jiǎn)明的國(guó)際化框架了.表現(xiàn)層HTML/JSP頁(yè)面可以用最終用戶(hù)本地語(yǔ)言編寫(xiě)保存為Unicode模式,或通過(guò)字典方式根據(jù)用戶(hù)的選擇,來(lái)動(dòng)態(tài)顯示HTML/JSP頁(yè)面上的本地語(yǔ)言提示性標(biāo)簽。在JSTL 1.0推出后,沒(méi)有理由再使用本地語(yǔ)言編寫(xiě)多套HTML/JSP頁(yè)面,帶來(lái)維護(hù)和代碼的復(fù)雜性。當(dāng)然具體應(yīng)用是復(fù)雜的,這里只是給出一個(gè)建議性的措施。
本文給出比較細(xì)節(jié)的解釋了B/S架構(gòu)下涉及到的Java編碼的方面,有助于出現(xiàn)問(wèn)題時(shí),快速定位問(wèn)題環(huán)節(jié)并解決之。
各種規(guī)范發(fā)展的很快,如果本文描述有錯(cuò)誤或更好的實(shí)現(xiàn),歡迎指正.
本文代碼在Jboss 3.2.2 with Tomcat 下調(diào)試通過(guò)。
附錄1:
Filter 源代碼:
0 /*
1 * JSPEncoding.java
2 *
3 * Created on 2003年12月17日, 下午9:45
4 */
5
6 package action;
7
8 import java.io.*;
9 import java.net.*;
10 import java.util.*;
11 import java.text.*;
12 import javax.servlet.*;
13 import javax.servlet.http.*;
14
15 import javax.servlet.Filter;
16 import javax.servlet.FilterChain;
17 import javax.servlet.FilterConfig;
18 import javax.servlet.ServletContext;
19 import javax.servlet.ServletException;
20 import javax.servlet.ServletRequest;
21 import javax.servlet.ServletResponse;
22
23 /**
24 *
25 * @author Administrator
26 * @version
27 */
28
29 public class JSPEncoding implements Filter {
30
31 // The filter configuration object we are associated with. If
32 // this value is null, this filter instance is not currently
33 // configured.
34 private FilterConfig filterConfig = null;
35 // default to UTF-8
36 private String targetEncoding = "UTF-8";
37 public JSPEncoding() {
38 }
39
40 private void doBeforeProcessing(ServletRequest request, ServletResponse response)
41 throws IOException, ServletException {
42 if (debug) log("JSPEncoding:DoBeforeProcessing");
43 }
44
45 private void doAfterProcessing(ServletRequest request, ServletResponse response)
46 throws IOExce1ption, ServletException {
47 if (debug) log("JSPEncoding:DoAfterProcessing");
48 }
49
50 /**
51 *
52 * @param request The servlet request we are processing
53 * @param result The servlet response we are creating
54 * @param chain The filter chain we are processing
55 *
56 * @exception IOException if an input/output error occurs
57 * @exception ServletException if a servlet error occurs
58 */
59 public void doFilter(ServletRequest request, ServletResponse response,
60 FilterChain chain)
61 throws IOException, ServletException {
62
63 if (debug) log("JSPEncoding:doFilter()");
64
65 doBeforeProcessing(request, response);
66
67 HttpServletRequest srequest = (HttpServletRequest)request;
68 srequest.setCharacterEncoding(targetEncoding);
69 HttpServletResponse sresponse=(HttpServletResponse)response;
70 sresponse.addHeader("charset", targetEncoding);
71 try {
72 chain.doFilter(srequest, sresponse);
73 }
74 catch(Throwable t) {
75 t.printStackTrace();
76 }
77
78 doAfterProcessing(request, response);
79
80 }
81
82
83 /**
84 * Return the1 filter configuration object for this filter.
85 */
86 public FilterConfig getFilterConfig() {
87 return (this.filterConfig);
88 }
89
90
91 /**
92 * Set the filter configuration object for this filter.
93 *
94 * @param filterConfig The filter configuration object
95 */
96 public void setFilterConfig(FilterConfig filterConfig) {
97
98 this.filterConfig = filterConfig;
99 }
100
101 /**
102 * Destroy method for this filter
103 *
104 */
105 public void destroy() {
106 filterConfig = null;
107 targetEncoding = null;
108 }
109
110
111 /**
112 * Init method for this filter
113 *
114 */
115 public void init(FilterConfig config) {
116
117 this.filterConfig = config;
118 this.targetEncoding = config.getInitParameter("encoding");
119 if (config != null) {
120 if (debug) {
121 log("JSPEncoding:Initializing filter");
122 }
123 }
124 }
125
126 /**
127 * Return a String representation of this object.
128 */
129 public String toString() {
130
131 if (filterConfig == null) return ("JSPEncoding()");
132 StringBuffer sb = new StringBuffer("JSPEncoding(1");
133 sb.append(filterConfig);
134 sb.append(")");
135 return (sb.toString());
136
137 }
138
139
140
141
142
143 public void log(String msg) {
144 filterConfig.getServletContext().log(msg);
145 }
146
147 private static final boolean debug = true;
148 }
附錄2 通用字符串重構(gòu)
/**
*@param pValue is raw data
*@pEncoding is target data Encode
*/
public String convert(String pValue, String pEncoding)
throws IOException
{
byte bytes[] = getBytes(pValue);
return convert(bytes, pEncoding);
byte[] getBytes(String pValue)
{
byte bytes[] = new byte[pValue.length()];
for(int i = 0; i < bytes.length; i++)
bytes[i] = (byte)pValue.charAt(i);
return bytes;
}
public String convert(byte pValue[], String pEncoding)
throws IOException
{
ByteArrayInputStream bais = new ByteArrayInputStream(pValue);
InputStreamReader isr = new InputStreamReader(bais, pEncoding);
StringBuffer sb = new StringBuffer();
for(int c = isr.read(); c != -1; c = isr.read())
sb.append((char)c);
return sb.toString();
}
附錄3
Servlet 2.3 Filter 的在web.xml中的表示
WEB-INF/web.xml
<filter>
<filter-name>action.JSPEncoding</filter-name>
<filter-class>action.JSPEncoding</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>action.JSPEncoding</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>