關于Java平臺國際化數(shù)據(jù)編碼常見問題分析
王旭科
一、前言
(CCW)經(jīng)常在看到有關討論Java相關技術的漢字的顯示,數(shù)據(jù)存儲漢字亂碼的解決方法。本人從事Java平臺相關開發(fā)也有近5年的時間,深感這種有關數(shù)據(jù)編碼(Encoding)的問題,不是一個 簡單的描述就可以指出問題的原因所在。目前零星的解決方案沒有提供一個全面的解釋。不同的瀏覽器,Web Server,Application Server,Database,支持的JDK版本不同,及設計的架構不周全,其組合是一個龐大的基數(shù),下面就從B/S 3層架構的角度,說明國際化的數(shù)據(jù)編碼常見問題的分析及解決方法。希望能分享自己的經(jīng)驗。
二、常見Java應用架構
以上是基本的Java常見架構,實際中可能Web server和應用服務器(Application Server)合二為一,不過為了清晰起見,還是分開顯示.
2.1 請求/響應(Request/Response) 工作模式
基于B/S架構的應用的模型,就是終端用戶通過瀏覽器向web Server發(fā)送請求,Web解釋并響應請求。其事件流模型可以參考標準HTTP協(xié)議獲得細節(jié)。常見的數(shù)據(jù)轉換及傳送發(fā)生以下三個環(huán)節(jié).所有的轉換都是雙向的.負責每個環(huán)節(jié)數(shù)據(jù)編碼(Encoding)的角色不同,需要對每個環(huán)節(jié)的角色進行正確的參數(shù)設定互相配合才能得到期望的結果。
瀏覽器 ?----------?Web Server. Browser Render引擎/Web Server 引擎
WebServer ?-------? 應用服務器 .JSP/Server 引擎及可能存在的其他連接WebServer
和Application Server的Plugin.
應用服務器 ?-------------? 數(shù)據(jù)庫 .每個數(shù)據(jù)庫廠家的JDBC驅動器.
這里最重要和最易變的是Browser ?>WebServer ?-?JSP/Servet 之間的數(shù)據(jù)流和事件流發(fā)生的數(shù)據(jù)編碼,JDBC如果只是利用純粹的thin的模式來訪問數(shù)據(jù)庫,邏輯比較簡單,如果通過不同數(shù)據(jù)庫廠家為了提高性能提供的利用本地client的方式來訪問數(shù)據(jù)庫,其配置復雜性隨不同廠家而不同,這里略過。以下對該事件流做詳細描述,以闡述后面發(fā)生的”內幕”.
2.2 瀏覽器 HTML Render引擎如何顯示和提交數(shù)據(jù).核心的URL-Encoding.
瀏覽器渲染html頁面里的內容如何編碼,決策順序如下。
首先瀏覽器根據(jù)Web Server發(fā)送的Content-Type Header,里的Charset信息來決定自己如何渲染html的顯示。如果沒有Content-Type,就根據(jù)Html頁面里的<meta>中的Content-Type來決定渲染的字符編碼.<meta>一般如下:
<META HTTP-EQUIV="Content-Type"CONTENT="text/html; CHARSET= “UTF-8">
一般出現(xiàn)亂碼的情況,有可能是content-type同實際數(shù)據(jù)不符,所以使用瀏覽器的”改變
編碼”的功能換一個字符集合,就能看到正確的數(shù)據(jù).如果以上有關字符編碼的信息都無
法得到,瀏覽器采用默認的ISO-8859-1來渲染HTML頁面
其次,瀏覽器向WebServer通過 Form提交數(shù)據(jù)的時候,其編碼數(shù)據(jù)的行為決策順序如
下
1.<Form>屬性的accept-charset,指定的字符編碼
2.<meta>指定的Content-Type
3.url-encoding 默認的字符編碼.
這是標準按照HTML Internationalization (參考RFC 2070)規(guī)范的順序。
根據(jù)實際的經(jīng)驗,F(xiàn)ireBird 瀏覽器(或Mozilla Familly) 完全順從這個順序。
但IE 6 是2 <meta>指定的Content-Type優(yōu)先級別最高,所以,在HTML面在<body>
標記之前,<head>標記后第一個寫入<meta>來聲明本頁的默認字符編碼,是良好的習慣。
可以保證大多數(shù)瀏覽器可以正確渲染HTML數(shù)據(jù)。
如果沒有指定任何Content-Type,瀏覽器將按照iso-8859-1這種字符編碼.
URL-Encoding的詳細描述在(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."”
這種方式下,就是常見的%HH字符串的結果了。所有字符被編碼成 %HH字符串的方
式被傳遞.
如果<meta>中的charset指明的不符合實際數(shù)據(jù)或著指定的字符集合不包容實際輸入的
字符集合,就會造成編碼錯誤,丟失數(shù)據(jù)信息。
總之,瀏覽器向Web Server提交數(shù)據(jù)的時候,根據(jù)URL-Encodeds 編碼數(shù)據(jù)并且設置
Content-Type 為application/x-www-form-urlencoded,但沒有傳送任何有關charset的信息。
2.2.1 小結
在HTML頁面或JSP/Servlet等動態(tài)生成的頁面里,必須指定正確的<meta>信
息,才能保證數(shù)據(jù)顯示渲染和提交給Web Server數(shù)據(jù)是正確編碼的。如果沒有指定任
何charset的信息,瀏覽器是按照ISO-8859-1編碼顯示和提交數(shù)據(jù),可能造成數(shù)據(jù)信
息的丟失.
2.3 Web Server 如何接受Post/get的數(shù)據(jù)
通過2.2節(jié)的分析,我們可以知道,默認的情況下,WebServer是按照原始數(shù)據(jù)(raw data)來接受數(shù)據(jù)的。寫過CGI應用應該知道,這些數(shù)據(jù)是存放在服務器系統(tǒng)同應用相關的環(huán)境變量Cache中,我們常說的context(上下文)就包含了這些原始的提交數(shù)據(jù).
2.4 JSP/Servlet 如何獲取數(shù)據(jù)
當使用Servlet調用getParameter或getParameters時候,通過Servlet包容器(Container)上下文來從WebServer環(huán)境變量中獲取原始數(shù)據(jù)并編碼,但由于沒有關于Charset的信息,所以此時設置正確的字符編碼,才能把被URL-Encoding的數(shù)據(jù),正確還原。這是通過設置request的setCharacterEncoding來設置正確的字符集,才能得到正確數(shù)據(jù)。
同時根據(jù)瀏覽器渲染HTML的規(guī)范,同樣送回瀏覽器的數(shù)據(jù)也必須指定正確的字符集才能保證瀏覽器正確編碼顯示,這是通過對response的setContentType方法調用來做到的。
在實際應用中,了解了這些原理,不難寫出正確的處理數(shù)據(jù)的應用。Servlet 2.3規(guī)范提供了Filter技術,可以完美解決Post數(shù)據(jù)和回應信息的編碼問題,具體例子見附錄1.
如果沒有Filter技術,則需要使用常用的“重構字符串”技術來解決這個問題,代碼見附錄2
2.4.1 小結:
判斷在Servlet中是否正確的重構了提交的(Post) 數(shù)據(jù),一個常見的小技巧是通過System.out.println打印出數(shù)據(jù)到后端控制臺,如果同系統(tǒng)當前字符集相同的數(shù)據(jù)能正確顯示,表明重構正確,比如你的服務器是Sun Solaris或Linux ,默認語言是中文,那么中文數(shù)據(jù)就可以正確的被打印出來,而不是一堆”?????”.
2.5 JDBC 如何保存數(shù)據(jù)庫
當把通過2.4 正確重構的數(shù)據(jù)要寫入數(shù)據(jù)庫時,同樣要考慮字符編碼的問題。
首先必須在執(zhí)行JDBC或使用J2EE CMP通過setxxxx符值之前,調整數(shù)據(jù)的編碼同數(shù)據(jù)庫字符編碼一致,否則可能出錯。這種轉換同具體使用JDBC Driver 的方式不同而有所不同。假設純粹使用thin(Type 3或Type 4)的方式,相對比較簡單,只要知道正確的數(shù)據(jù)庫端的字符集,實現(xiàn)數(shù)據(jù)重構為符合數(shù)據(jù)庫字符編碼的數(shù)據(jù)就可以了,代碼見附錄 2
三、通用國際化架構
1、所有HTML或JSP/Servlet動態(tài)頁面<meta>指明字符集合為UTF-8
2、Servlet 指明設置request獲取參數(shù)為UTF-8
3、Servlet 指明reponse Content-Type 的charset為UTF-8
4、數(shù)據(jù)庫編碼指明為UTF-8
這是最簡明的國際化框架了.表現(xiàn)層HTML/JSP頁面可以用最終用戶本地語言編寫保存為Unicode模式,或通過字典方式根據(jù)用戶的選擇,來動態(tài)顯示HTML/JSP頁面上的本地語言提示性標簽。在JSTL 1.0推出后,沒有理由再使用本地語言編寫多套HTML/JSP頁面,帶來維護和代碼的復雜性。當然具體應用是復雜的,這里只是給出一個建議性的措施。
本文給出比較細節(jié)的解釋了B/S架構下涉及到的Java編碼的方面,有助于出現(xiàn)問題時,快速定位問題環(huán)節(jié)并解決之。
各種規(guī)范發(fā)展的很快,如果本文描述有錯誤或更好的實現(xiàn),歡迎指正.
本文代碼在Jboss 3.2.2 with Tomcat 下調試通過。
附錄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 通用字符串重構
/**
*@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>