« Java的中文处理学习笔记:Hello Unicode(2) | (回到Blog入口)|(回到首页) | XSLT简介:Google的XML接口的XSLT设计 »

Java的中文处理学习笔记:Hello Unicode(3)


试验3:WEB应用中的输入输出中的编码问题:Java是为做国际化应用设计的,Servlet应根据浏览器语言设置自动切换字符集配置

首先一个概念:即使是基于Java的WEB应用,在服务器和客户端之间传递的仍然是字节流,比如我从一个中文客户端的浏览器表单中提交“世界你好”这4个中文字到服务器时:首先浏览器按照GBK方式编码成字节流CA C0 BD E7 C4 E3 BA C3,然后8个字节按照URLEncoding的规范转成:%CA%C0%BD%E7%C4%E3%BA%C3,服务器端的Servlet接收到请求后应该按什么解码处理,输出时又应该按什么方式编码行字节流呢?

在目前的Servlet的规范中,如果不指定的话通过WEB提交时的输入ServletRequest和输出时的ServletResponse缺省都是ISO-8859-1方式编/码解码的(注意,这里的编码/解码方式是和操作系统环境中的语言环境是无关的)。因此,即使服务器操作系统的语言环境是中文,上面输入的请求仍然按英文解码成8个UNICODE字符,输出时仍按照英文再编码成8个字节,虽然这样在浏览器端如果设置是中文能够正确显示,但实际上读写的是“字节”,正确的方式是应该根据客户端浏览器设置ServletRequest和ServletResponse用相应语言的编码方式进行输入解码/输入编码,HelloUnicodeServlet.java就是这样一个监测客户端浏览器语言设置的例子:

当根据浏览器的头信息中的"Accept-Language"为zh-cn(中文)时,设置请求的解码方式和输出的字符集编码方式使用GBK:

        //auto detect broswer's languages
String clientLanguage = req.getHeader("Accept-Language");

//for Simplied Chinese
if ( clientLanguage.equals("zh-cn") ) {
req.setCharacterEncoding("GBK");
res.setContentType("text/html; charset=GBK");
}
输出为:
'世界你好' length=4
ServletRequest's Charset Encoding = GBK
ServletResponse's Charset Encoding = GBK
char[0]='世' byte=22 \u16 short=19990 \u4E16 CJK_UNIFIED_IDEOGRAPHS
char[1]='界' byte=76 \u4C short=30028 \u754C CJK_UNIFIED_IDEOGRAPHS
char[2]='你' byte=96 \u60 short=20320 \u4F60 CJK_UNIFIED_IDEOGRAPHS
char[3]='好' byte=125 \u7D short=22909 \u597D CJK_UNIFIED_IDEOGRAPHS

再做一个试验:把程序开头部分的浏览器自动检测功能注释掉,再次的输出结果就是和目前很多应用一样其实是按ISO-8859-1方式解码/编码的“字节应用”了:

'世界你好' length=8
ServletRequest's Charset Encoding = null
ServletResponse's Charset Encoding = ISO-8859-1
char[0]='? byte=-54 \uFFFFFFCA short=202 \uCA LATIN_1_SUPPLEMENT
char[1]='? byte=-64 \uFFFFFFC0 short=192 \uC0 LATIN_1_SUPPLEMENT
char[2]='? byte=-67 \uFFFFFFBD short=189 \uBD LATIN_1_SUPPLEMENT
char[3]='? byte=-25 \uFFFFFFE7 short=231 \uE7 LATIN_1_SUPPLEMENT
char[4]='? byte=-60 \uFFFFFFC4 short=196 \uC4 LATIN_1_SUPPLEMENT
char[5]='? byte=-29 \uFFFFFFE3 short=227 \uE3 LATIN_1_SUPPLEMENT
char[6]='? byte=-70 \uFFFFFFBA short=186 \uBA LATIN_1_SUPPLEMENT
char[7]='? byte=-61 \uFFFFFFC3 short=195 \uC3 LATIN_1_SUPPLEMENT
虽然这样的输出结果如果在浏览器中设置用中文字符集也能正确显示,但实际上处理的已经是“字节”而不是处理中文“字符”了。ServletRequest 和 ServletResponse 缺省使用ISO-8859-1字符集解码/编码的具体定义请参考:
http://java.sun.com/products/servlet/2.3/javadoc/javax/servlet/ServletRequest.html#setCharacterEncoding(java.lang.String)

http://java.sun.com/products/servlet/2.3/javadoc/javax/servlet/ServletResponse.html#setContentType()

以前能够配置让一个WEB应用能够在GBK方式编码的中文Windows2000服务器上和按ISO-8859-1方式编码的GNU/Linux上都能够正确的显示中文一直让我迷惑了很久。我仔细想了一下,后来终于想明白了,在一个国际化的应用中:ServletRequest和ServletResponse的编码/解码方式的确不应该根据服务器设置成固定的字符集,而应该是面向客户端语言环境进行输入/输出编码方式的自适应。一个按照国际化规范设计的WEB应用中:

  • 在Servlet的源代码中尽量不要有中文:因为在MVC模式中,Servlet主要是控制器(C)的角色,因此,应该通过ResourceBundle机制由Servlet控制转向到相应的显示器(JSP或者XSLT)中,所以应该将与本地界面语言相关的界面显示的部分从Servlet和后台的模块中完全剥离出来,放到相应的ResourceBundle文件中或者XSLT文件中。这样源程序里完全是英文,编译时完全不需要考虑字符集的问题。

    如果Servlet实在需要包含中文,则需要设置应用服务器的Javac编译选项,加上-encoding选项成系统缺省的字符集,如果把用中文编写的字符按照英文方式解码编译,然后再按照英文方式输出,虽然结果表面正确,实际上都成了面向“字节”编程。
  • 在Servlet层,应该像GOOGLE搜索引擎那样,设计成能够根据客户端浏览器的语言环境自适应输出,为了判断浏览器的语言Servlet中应该有类似以下的代码:
        public void doGet (HttpServletRequest req, HttpServletResponse res)
    throws ServletException, IOException {
    //从HTTP请求的头信息中获取客户端的语言设置
    String clientLanguage = req.getHeader("Accept-Language");

    //简体中文浏览器
    if ( clientLanguage.equals("zh-cn") ) {
    req.setCharacterEncoding("GBK");
    res.setContentType("text/html; charset=GBK");
    }
    //繁体中文浏览器
    else if ( clientLanguage.equals("zh-tw") ) {
    req.setCharacterEncoding("BIG5");
    res.setContentType("text/html; charset=BIG5");
    }
    //日文浏览器
    else if ( clientLanguage.equals("jp") ) {
    req.setCharacterEncoding("SJIS");
    res.setContentType("text/html; charset=SJIS");
    }
    //缺省认为是英文浏览器
    else {
    req.setCharacterEncoding("ISO-8859-1");
    res.setContentType("text/html; charset=ISO-8859-1");
    }
    ...
    //设置好request的解码方式和response的编码方式后,进行后续的操作。
    //比如再转向到HelloWorld.gbk.jsp HelloWorld.big5.jsp HelloWorld.jis.jsp等
    }

而SERVLET缺省将字符集设置为ISO-8859-1也许是标准制定者认为英文的浏览器占大多数吧(而且按照ISO-8859-1方式输出界面往往也是正确的)。

结论:

过以上几个Java试验程序得出的一些结论:

  • Java环境是基于操作系统上的一个虚拟机应用,因此,如果操作系统遵循国际化规范:JVM的缺省编码方式可以通过修改操作系统的LOCALE设置实现。对于一个Java应用来说,只要将LINUX的缺省编码方式设置成GBK,其文字编码处理应该和中文Windows平台上的表现是一致的。
        redhat 6.X使用linux内核的是基于glibc2.1.X,不支持中文LOCALE,因此无法通过改变LOCALE设置改变JVM的缺省编码方式,linux内核2.4开始基于glibc.2.2.x,对中文LOCALE有了比较好的支持。
  • 不同的JVM对字符集的支持程度不同:
        比如:IBM的JVM1.3.0开始支持GB18030,SUN的JVM从1.4开始支持GB18030
  • 正确的编码方式不一定表示能正确的显示,正确的显示还要需要相应的前端显示系统(字库)的支持
        但对于Linux上的服务应用来说,往往只要能确认字符正确的按照指定的方式编码就够了
  • 如果应用的是基于UNICODE的编码方式处理并使用UTF8字符集做集中存储,这样最便于根据客户端语言环境做本地化输出;

根据以上结论,设计一个适应多语言环境的应用,可以考虑一下2个应用处理模式:

  • (客户端应用或本地化应用)根据LOCALE,让Java应用根据系统LOCALE的缺省的字符集设置进行切换,按系统缺省的字符集进行编码解码,减少应用在编码处理上的复杂程度。
    输入字节流 ==>按系统语言字符集设置将字节流解码==> UNICODE处理 ==> 按系统语言字符集设置将UNICODE编码成字节流 ==> 输出字节流
      
  • (服务器端或跨语言平台应用):在应用的最外端:数据输入输出判断用户语言环境,核心按照UNICODE方式处理存储。可以把各种区域性的字符集(GB2312 BIG5)看成是UNICODE的一个子集。UNICODE存储的数据可以方便的转换成任意字符集。
    应用使用UTF8方式存储虽然要增加了存储空间,但也可以大大简化前端应用本地化(i10n)的复杂程度。    
    简体中文输入 繁体中文输入                 简体中文输出 繁体中文输出
    \ / \ /
    判断用户语言环境:解码 判断用户语言环境:编码
    \ /
    中间处理过程:UNICODE
    |
    UTF8编码存储

随着UNICODE被愈来愈多的系统和平台支持:Python Perl Glibc等,但我们应该珍惜一开始就按照国际化规范设计Java,并将其和新发展起来的XML规范相配合,相信符合国际化规范的应用设计从长远来看会展现出更多的优势。

TODO:
数据库应用中的字符集问题试验:MySQL Oracle JDBC

参考文档:
Java的国际化设计
http://java.sun.com/docs/books/tutorial/i18n/index.html

Linux 国际化本地化和中文化
http://www.linuxforum.net/doc/i18n-new.html

Linux 程序员必读:中文化与GB18030标准
http://www.ccidnet.com/tech/os/2001/07/31/58_2811.html

Unicode FAQ
http://www.cl.cam.ac.uk/~mgk25/unicode.html
http://www.linuxforum.net/books/UTF-8-Unicode.html (中文版)

Java 编程技术中汉字问题的分析及解决
http://www-900.ibm.com/developerWorks/cn/java/java_chinese/index.shtml

汉字的编码方式:
http://www.unihan.com.cn/cjk/ana17.htm

不同版本的JVM支持的编码方式
http://java.sun.com/j2se/1.3/docs/guide/intl/encoding.doc.html
http://java.sun.com/j2se/1.4/docs/guide/intl/encoding.doc.html

附录:

A. The Unicode 2.0 Character Set

 

Characters

Description

\u0000 - \u1FFF

Alphabets

\u0020 - \u007F

Basic Latin

\u0080 - \u00FF

Latin-1 supplement

\u0100 - \u017F

Latin extended-A

\u0180 - \u024F

Latin extended-B

\u0250 - \u02AF

IPA extensions

\u02B0 - \u02FF

Spacing modifier letters

\u0300 - \u036F

Combining diacritical marks

\u0370 - \u03FF

Greek

\u0400 - \u04FF

Cyrillic

\u0530 - \u058F

Armenian

\u0590 - \u05FF

Hebrew

\u0600 - \u06FF

Arabic

\u0900 - \u097F

Devanagari

\u0980 - \u09FF

Bengali

\u0A00 - \u0A7F

Gurmukhi

\u0A80 - \u0AFF

Gujarati

\u0B00 - \u0B7F

Oriya

\u0B80 - \u0BFF

Tamil

\u0C00 - \u0C7F

Telugu

\u0C80 - \u0CFF

Kannada

\u0D00 - \u0D7F

Malayalam

\u0E00 - \u0E7F

Thai

\u0E80 - \u0EFF

Lao

\u0F00 - \u0FBF

Tibetan

\u10A0 - \u10FF

Georgian

\u1100 - \u11FF

Hangul Jamo

\u1E00 - \u1EFF

Latin extended additional

\u1F00 - \u1FFF

Greek extended

\u2000 - \u2FFF

Symbols and punctuation

\u2000 - \u206F

General punctuation

\u2070 - \u209F

Superscripts and subscripts

\u20A0 - \u20CF

Currency symbols

\u20D0 - \u20FF

Combining diacritical marks for symbols

\u2100 - \u214F

Letterlike symbols

\u2150 - \u218F

Number forms

\u2190 - \u21FF

Arrows

\u2200 - \u22FF

Mathematical operators

\u2300 - \u23FF

Miscellaneous technical

\u2400 - \u243F

Control pictures

\u2440 - \u245F

Optical character recognition

\u2460 - \u24FF

Enclosed alphanumerics

\u2500 - \u257F

Box drawing

\u2580 - \u259F

Block elements

\u25A0 - \u25FF

Geometric shapes

\u2600 - \u26FF

Miscellaneous symbols

\u2700 - \u27BF

Dingbats

\u3000 - \u33FF

CJK auxiliary

\u3000 - \u303F

CJK symbols and punctuation

\u3040 - \u309F

Hiragana

\u30A0 - \u30FF

Katakana

\u3100 - \u312F

Bopomofo

\u3130 - \u318F

Hangul compatibility Jamo

\u3190 - \u319F

Kanbun

\u3200 - \u32FF

Enclosed CJK letters and months

\u3300 - \u33FF

CJK compatibility

\u4E00 - \u9FFF

CJK unified ideographs: Han characters used in China, Japan, Korea, Taiwan, and Vietnam

\uAC00 - \uD7A3

Hangul syllables

\uD800 - \uDFFF

Surrogates

\uD800 - \uDB7F

High surrogates

\uDB80 - \uDBFF

High private use surrogates

\uDC00 - \uDFFF

Low surrogates

\uE000 - \uF8FF

Private use

\uF900 - \uFFFF

Miscellaneous

\uF900 - \uFAFF

CJK compatibility ideographs

\uFB00 - \uFB4F

Alphabetic presentation forms

\uFB50 - \uFDFF

Arabic presentation forms-A

\uFE20 - \uFE2F

Combing half marks

\uFE30 - \uFE4F

CJK compatibility forms

\uFE50 - \uFE6F

Small form variants

\uFE70 - \uFEFE

Arabic presentation forms-B

\uFEFF

Specials

\uFF00 - \uFFEF

Halfwidth and fullwidth forms

\uFFF0 - \uFFFF

Specials

Comments

看完你的java中文处理三篇,点起了很多疑问,弱弱的质疑一下,你是不是把encoding, decoding, 编码解码搞反了?由字符/图形/符号到数字码化的过程叫编码encoding,反之叫解码吧?

发表一个评论

(如果你此前从未在此 Blog 上发表过评论,则你的评论必须在 Blog 主人验证后才能显示,请你耐心等候。)

相关文章

关于

此页面包含了发表于July 11, 2002 10:36 PM的 Blog 上的单篇日记。

此 Blog 的前一篇日记是 Java的中文处理学习笔记:Hello Unicode(2)

此 Blog 的后一篇日记是 XSLT简介:Google的XML接口的XSLT设计

更多信息可在 主索引 页和 归档 页看到。

Creative Commons License
此 Blog 中的日记遵循以下授权 Creative Commons(创作共用)授权.
Powered by
Movable Type 3.36