星期二, 十二月 23, 2003

About JaVA Platform Internationalization Data Encoding Issue.

关于Java平台国际化数据编码常见问题分析


王旭科
MSN:sunosewang@msn.com

一. 前言
经常在看到有关讨论Java相关技术的汉字的显示,数据存储汉字乱码的解决方法。本人从事Java平台相关开发也有近5年的时间,深感这种有关数据编码(Encoding)的问题,不是一个简单的描述就可以指出问题的原因所在。目前零星的解决方案没有提供一个全面的解释。不同的浏览器,Web Server,Application Server,Database,支持的JDK版本不同,及设计的架构不周全,其组合是一个庞大的基数,下面就从B/S 3层架构的角度,说明国际化的数据编码常见问题的分析及解决方法。希望能分享自己的经验。
二. 常见Java应用架构

图 1
以上是基本的Java常见架构,实际中可能Web server和应用服务器(Application Server)合二为一,不过为了清晰起见,还是分开显示.

2.1 请求/响应(Request/Response) 工作模式
基于B/S架构的应用的模型,就是终端用户通过浏览器向web Server发送请求,Web解释并响应请求。其事件流模型可以参考标准HTTP协议获得细节。常见的数据转换及传送发生以下三个环节.所有的转换都是双向的.负责每个环节数据编码(Encoding)的角色不同,需要对每个环节的角色进行正确的参数设定互相配合才能得到期望的结果。

浏览器 ?----------àWeb Server. Browser Render引擎/Web Server 引擎

WebServer ?-------à 应用服务器 .JSP/Server 引擎及可能存在的其他连接WebServer
和Application Server的Plugin.

应用服务器 ?-------------à 数据库 .每个数据库厂家的JDBC驱动器.

这里最重要和最易变的是Browser ?>WebServer ?-àJSP/Servet 之间的数据流和事件流发生的数据编码,JDBC如果只是利用纯粹的thin的模式来访问数据库,逻辑比较简单,如果通过不同数据库厂家为了提高性能提供的利用本地client的方式来访问数据库,其配置复杂性随不同厂家而不同,这里略过。以下对该事件流做详细描述,以阐述后面发生的”内幕”.
2.2 浏览器 HTML Render引擎如何显示和提交数据.核心的URL-Encoding.
浏览器渲染html页面里的内容如何编码,决策顺序如下。
首先浏览器根据Web Server发送的Content-Type Header,里的Charset信息来决定自己如何渲染html的显示。如果没有Content-Type,就根据Html页面里的中的Content-Type来决定渲染的字符编码.一般如下:

一般出现乱码的情况,有可能是content-type同实际数据不符,所以使用浏览器的”改变
编码”的功能换一个字符集合,就能看到正确的数据.如果以上有关字符编码的信息都无
法得到,浏览器采用默认的ISO-8859-1来渲染HTML页面

其次,浏览器向WebServer通过 Form提交数据的时候,其编码数据的行为决策顺序如

1.
属性的accept-charset,指定的字符编码
2.指定的Content-Type
3.url-encoding 默认的字符编码.
这是标准按照HTML Internationalization (参考RFC 2070)规范的顺序。
根据实际的经验,FireBird 浏览器(或Mozilla Familly) 完全顺从这个顺序。
但IE 6 是2 指定的Content-Type优先级别最高,所以,在HTML面在
标记之前,标记后第一个写入来声明本页的默认字符编码,是良好的习惯。
可以保证大多数浏览器可以正确渲染HTML数据。
如果没有指定任何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字符串的方
式被传递.
如果中的charset指明的不符合实际数据或着指定的字符集合不包容实际输入的
字符集合,就会造成编码错误,丢失数据信息。
总之,浏览器向Web Server提交数据的时候,根据URL-Encodeds 编码数据并且设置
Content-Type 为application/x-www-form-urlencoded,但没有传送任何有关charset的信息。
2.2.1 小结
在HTML页面或JSP/Servlet等动态生成的页面里,必须指定正确的
息,才能保证数据显示渲染和提交给Web Server数据是正确编码的。如果没有指定任
何charset的信息,浏览器是按照ISO-8859-1编码显示和提交数据,可能造成数据信
息的丢失.

2.3 Web Server 如何接受Post/get的数据
通过2.2节的分析,我们可以知道,默认的情况下,WebServer是按照原始数据(raw data)来接受数据的。写过CGI应用应该知道,这些数据是存放在服务器系统同应用相关的环境变量Cache中,我们常说的context(上下文)就包含了这些原始的提交数据.

2.4 JSP/Servlet 如何获取数据
当使用Servlet调用getParameter或getParameters时候,通过Servlet包容器(Container)上下文来从WebServer环境变量中获取原始数据并编码,但由于没有关于Charset的信息,所以此时设置正确的字符编码,才能把被URL-Encoding的数据,正确还原。这是通过设置request的setCharacterEncoding来设置正确的字符集,才能得到正确数据。
同时根据浏览器渲染HTML的规范,同样送回浏览器的数据也必须指定正确的字符集才能保证浏览器正确编码显示,这是通过对response的setContentType方法调用来做到的。
在实际应用中,了解了这些原理,不难写出正确的处理数据的应用。Servlet 2.3规范提供了Filter技术,可以完美解决Post数据和回应信息的编码问题,具体例子见附录1.
如果没有Filter技术,则需要使用常用的“重构字符串”技术来解决这个问题,代码见附录2
2.4.1 小结:
判断在Servlet中是否正确的重构了提交的(Post) 数据,一个常见的小技巧是通过System.out.println打印出数据到后端控制台,如果同系统当前字符集相同的数据能正确显示,表明重构正确,比如你的服务器是Sun Solaris或Linux ,默认语言是中文,那么中文数据就可以正确的被打印出来,而不是一堆”?????”.

2.5 JDBC 如何保存数据库
当把通过2.4 正确重构的数据要写入数据库时,同样要考虑字符编码的问题。
首先必须在执行JDBC或使用J2EE CMP通过setxxxx符值之前,调整数据的编码同数据库字符编码一致,否则可能出错。这种转换同具体使用JDBC Driver 的方式不同而有所不同。假设纯粹使用thin(Type 3或Type 4)的方式,相对比较简单,只要知道正确的数据库端的字符集,实现数据重构为符合数据库字符编码的数据就可以了,代码见附录 2

三.通用国际化架构
1. 所有HTML或JSP/Servlet动态页面指明字符集合为UTF-8
2. Servlet 指明设置request获取参数为UTF-8
3. Servlet 指明reponse Content-Type 的charset为UTF-8
4. 数据库编码指明为UTF-8
这是最简明的国际化框架了.表现层HTML/JSP页面可以用最终用户本地语言编写保存为Unicode模式,或通过字典方式根据用户的选择,来动态显示HTML/JSP页面上的本地语言提示性标签。在JSTL 1.0推出后,没有理由再使用本地语言编写多套HTML/JSP页面,带来维护和代码的复杂性。当然具体应用是复杂的,这里只是给出一个建议性的措施。

本文给出比较细节的解释了B/S架构下涉及到的Java编码的方面,有助于出现问题时,快速定位问题环节并解决之。
各种规范发展的很快,如果本文描述有错误或更好的实现,欢迎指正.

本文代码在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


action.JSPEncoding
action.JSPEncoding

encoding
UTF-8



action.JSPEncoding
/*