中文化和国际化攻略
一般攻略
既然在 Java 内部是直接使用 Unicode 表示一切字符的,表达中文自然不成问题。因此所谓的中文问题并不是由 Java 本身引起的。而是因为对 Java 和 Unicode 理解不透或应用不当引起的。下面列出的原则,是解决一切中文问题的总纲:
- 在 Java 内部,正确使用 Unicode 标准。对于中文来说,每个汉字使用一个 char 表示。
- 在所有的输入输出环节,指明正确的编码方式,进行正确的字符到字节,或字节到字符的转换。
- 如果输入源或输出目标直接支持,尽可能直接使用 Unicode 进行输入输出。例如, Oracle 数据库直接支持 UTF-8 的文本数据。使用 UTF-8 操作 Oracle ,可自动兼容所有的语言文字;反之,使用 ISO-8859-1 或者 ASCII 去操作 Oracle ,只能兼容欧美单字节的文字。
- 不要依赖平台默认的字符编码方式。例如,中文 Windows 下,默认编码为 GBK ,英文 Linux 下,默认编码为 ISO-8859-1 。依赖平台默认值意味着同样的程序在不同的平台上可能产生不同的结果。
遗留代码攻略
对于第三方的代码,或是以前遗留的代码,如果没有留有指定字符编码的接口,那么这些代码很可能使用默认的系统编码,或是使用固定的字符编码。这样很容易造成上述的各种中文乱码的问题。对于这些代码,我们可以做一个适配器,将它们返回的字符串转换成适当的 Unicode 内码。
例如,我们的数据库错误地使用了 ASCII 编码存储文本,也就是说从数据库返回的中文字,实际上被 “ 拆 ” 成了两个欧洲字符。但是数据库中已经保存了大量数据,想要把它改成正确的 UTF-8 存储格式并不容易。作为权宜之计,我们可以在数据访问层做一个适配器,将欧洲字符重新组合,变成真正的 Unicode 中文。
public class DBAdapter {
private DBObject obj;
// 重新组合字节,转变成真正的unicode 字符串
public String getString() {
String str = obj.getString();
try {
return new String(str.getBytes("8859_1"), "GBK");
} catch (UnsupportedEncodingException e) {
return str; // 不会执行到此处
}
}
// 将unicode 字符串中的中文拆成两个欧洲字符,以便数据库保存
public void setString(String str) {
try {
str = new String(str.getBytes("GBK"), "8859_1");
} catch (UnsupportedEncodingException e) {
}
obj.setString(str);
}
}
WEB 应用攻略
除了 Unicode 以外,无论何种本地字符集,都不能代表所有字符。这将导致一些问题:
- 难以在一屏幕显示多种语言的文字。
- 解码用户表单困难。
我们知道浏览器是根据当前页面的 content type 中指定的字符编码来发送用户的表单输入的。假设当前页面的 content type 为 text/html; charset=GBK ,则当用户按下 submit 按钮提交表单时,浏览器自动将用户输入的字符以 GBK 方式编码并发回到服务器端。假设页面的 content type 为 text/html; charset=BIG5 ,则用户的输入将以 BIG5 繁体中文的编码发送。但是,如果用户输入的字符超过了这个编码字符集的范围,会怎样呢?我们可以写一个简单的 JSP 做试验:
<%@page contentType="text/html; charset=BIG5"%>
<html>
<head>
<title>Form test</title>
<meta http-equiv="Content-Type" content="text/html; charset=BIG5"/>
</head>
<body>
<p>Character Encoding: <%=request.getCharacterEncoding()%></p>
<%
String mytext = request.getParameter("mytext");
if (mytext != null) {
out.println("<p>Value of parameter <code>mytext<code>:");
out.println("<table border='1'>");
out.println(" <tr><th>Display</th><th>Unicode</th><th>BIG5 code</tr>");
for (int i = 0; i < mytext.length(); i++) {
char ch = mytext.charAt(i);
byte[] big5bytes = Character.toString(ch).getBytes("BIG5");
int big5code = 0;
out.print(" <tr><td>" + ch + "</td><td>");
out.print(Integer.toHexString(0xFFFF & mytext.charAt(i)) + "</td><td>");
for (int j = big5bytes.length - 1; j >= 0; j--) {
big5code = (big5code << 8) + (0xFF & big5bytes[j]);
}
out.print(Integer.toHexString(big5code) + "</td></tr>");
}
out.println("</table></p>");
} else {
mytext = "";
}
%>
<form action="<%=request.getRequestURI()%>" method="GET">
<textarea name="mytext"><%=mytext%></textarea><br>
<input type="submit"/>
</form>
</body>
</html>
上述页面是用 BIG5 显示的。在文本框中打入简体中文字 “ 我爱 ” ,然后 submit 。在结果页面中,我们可以看到 “ 我 ” 被转换成了 BIG5 编码 DAA7 ,而简体中文 “ 爱 ” 在 BIG5 中没有对应的编码,因此被浏览器直接以 爱 的形式返回。其中 29233 是简体中文 “ 爱 ” 的十进制 Unicode 码。
可见浏览器会把超出当前字符集的字符,以实体编码的形式(如 爱 ),直接返回给服务器端。 Java servlet 并不会自动处理这样的输入值,这给进一步处理字符串造成了困难。
为什么不直接使用 UTF-8 作为 WEB 页面的编码呢?这样不仅可以让全世界的文字同时显示在同一屏幕上(只要安装了相应的字体),也大大简化了解码用户表单的工作(不需要处理 爱 这样的实体编码)。但使用 UTF-8 也会带来一些微小的不便:
- 一个中文需要用三个字节表示,稍微增加了网页的大小。但多数网页中的中文字的数量是非常用限的,因而字节数的增加也是非常有限的。例如 Alibaba 中文站的首页面,改成 UTF-8 以后,比 GBK 编码的页面仅仅增加了 1413 个字节。
- 使用不支持 UTF-8 的编辑器查看页面将看到 “ 乱码 ” 。但我们可以使用支持 UTF-8 的文本编辑器来查看页面的 HTML 源代码。此外,使用 UTF-8 编码 WEB 页面 并非 意味着用来生成 WEB 页面的模板也必须使用 UTF-8 。仍然可以使用 GBK 来书写 WEB 页面模板。
根据上面的讨论,我们得到如下最佳攻略:
1. 使用 UTF-8 作为 WEB 页面的编码。使用如下语句设置 content type :
2.
response.setContentType("text/html; charset=UTF-8");
并且在 WEB 页的 HTML 中设置标记:
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
对于 Turbine ,可在其配置文件中设置:
locale.default.charset=UTF-8
Turbine 将根据上述设置,自动为你设置 content type 。
3. 仍然可以使用 GBK 来书写页面模板。对于以 Velocity 为模板系统的 Turbine ,需要在 Turbine 的配置文件中设置:
4.
services.VelocityService.input.encoding=GBK
模板的内容将以 GBK 的方式转换成 Unicode ,最后以 UTF-8 的方式输出到用户浏览器。
5. 使用 UTF-8 解码用户输入的表单。有几种方式可以达到这个目的:
-
- 设置服务器特定的配置文件。对于 Resin Server ,需要在其配置文件 resin.conf 中设置: <web-app character-encoding="UTF-8"/> ;对于 Weblogic Server ,需要设置 WEB-INF/weblogic.xml 配置文件,具体方法参见 BEA文档 。
o 创建一个 javax.servlet.Filter ,在 servlet 被调用前,调用 request.setCharacterEncoding 方法:
o
import java.io.IOException;
o
o
import javax.servlet.Filter;
o
import javax.servlet.FilterChain;
o
import javax.servlet.FilterConfig;
o
import javax.servlet.ServletException;
o
import javax.servlet.ServletRequest;
o
import javax.servlet.ServletResponse;
o
o
public class SetCharacterEncodingFilter implements Filter {
o
o
public void init(FilterConfig config) throws ServletException {
o
}
o
o
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
o
throws IOException, ServletException {
o
request.setCharacterEncoding("UTF-8");
o
chain.doFilter(request, response);
o
}
o
o
public void destroy() {
o
}
o
}
o 在 servlet 代码中调用:
o
request.setCharacterEncoding("UTF-8");
Java Mail 攻略
和 WEB 应用完全类似,使用 Java Mail API 同样需要设置正确的 content type 和字符编码。
import javax.mail.internet.ContentType;
import javax.mail.internet.MimeUtility;
import javax.mail.Part;
...
Part part;
ContentType contentType;
...
contentType = new ContentType("text/plain"); // 或"text/html"
contentType.setParameter("charset", MimeUtility.mimeCharset("UTF-8"));
part.setContent("... text or HTML content ...", contentType.toString());
值得注意的是, RFC 822 标准规定, e-mail 的 header 不能包含非 ASCII 编码。也就是说, e-mail 的主题( subject )不能包含中文。那我们怎样在 e-mail 的 subject 中发送中文呢?还好,另一个标准 RFC 2047 定义了如何将非 ASCII 的 header 转换成 ASCII 的规则。我们不需要了解这个规则的细节,只要调用 javax.mail.internet.MimeUtility 就可以完成转换了:
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeUtility;
...
MimeMessage message;
...
message.setSubject(MimeUtility.encodeText("... e-mail 的主题 ...",
MimeUtility.mimeCharset("UTF-8"), null));
大结论
完成同一个目标,往往有许多种途径,总有一些途径是比较好的,也有一些是不太好的。那些不太好的途径,虽然也能完成任务,却会导致很多潜在的问题。这些问题可能要在一定的条件下才能表现出来。例如,我们公司的 Java 程序以及 Oracle 数据库,从一开始就没有正确使用 Unicode 来存取文本,结果导致了一系列的问题。而要修正这样的问题,代价是比较大的。
怎样的途径是好的呢?一般来说,符合业界标准的,或是市场标准的实现途径,是我们首选的途径。所以我们设计任何程序之前,一定要充分了解相应的标准,不能不求甚解,完成任务了事。
Unicode 标准及相关技术,不仅是解决中文问题的关键,而且是以统一的方法解决国际所有语言文字的问题的最好途径。使用好这些标准,必将为我们公司的产品的进一步向国际化发展,打好坚实的基础。