posts - 262,  comments - 221,  trackbacks - 0

在基于J2EE的B/S应用中,中文乱码是一个永恒的主题,永远都无法回避。诚然对于一般的程序员,我们没有必要对编码进行深刻的研究。但是至少我们需要了解:

 ①编码基础
 ②String的getBytes([encoding])方法内幕
 ③String的toCharArray()方法内幕
 ④输出时的编码与乱码原因
 ⑤UTF-8的编码规则和GBK如何转换到UTF-8
 ⑥字符在各种表现形式下的值
 ⑦native2ascii命令的用法

正因为Java中采用了Unicode编码作为中介,所以任何初始的输入和最终的输出都会有:
 ①从byte[]----》encode字符---》Unicode的输入转换
 ②从Unicode---》encode字符---》byte[]的输出转换

一个典型的J2EE B/S应用,从客户端输出到最终服务器端的输出,需要经历如下的流程

其中,客户端包括了:操作系统、Web浏览器、静态网页、JS等。服务器端包括了:Servlet、JSP、Filter、配置文件等。这中间的任何一步配置不当或转换不当都有可能导致乱码或者?的出现。下面就将针对各个流程环节中可能出现的乱码地方,做一次全面的总结并给出基本的对策:

【客户端】

①操作系统

对于Windows操作系统,中文语言的设置在控制面板中进行


如果把最后的“高级”中改成英文(美国),你会发现很多不支持多国语言的软件,菜单或工具栏的中文立马变成?了。

对于Linux系统,临时的设置可以通过下面的设置来指定系统使用中文环境和UTF-8编码格式

export LC_ALL=zh_CN.UTF-8


如果是需要永久性的起作用,必须在 /home/<user>/.bashrc文件中设置

②浏览器

现代的浏览器都可以动态的设置浏览页面的编码


【服务器端】

①服务器的配置文件

对于Tomcat,可以在server.xml文件中进行配置

<Connector port="80" maxThreads="150" minSpareThreads="25" maxSpareThreads="75" 
    enableLookups
="false" redirectPort="8443" acceptCount="100"
    debug
="0" connectionTimeout="20000" disableUploadTimeout="true" URIEncoding="UTF-8"/>

其中黄色高亮部分就是将请求的内容使用UTF-8进行编码。但要注意:这只是针对Get请求的!如果要连Post请求的内容都使用UTF-8编码,那么就要使用下面的过滤器了。

②过滤器

它是在请求未到达对应的servlet或JSP之前进行拦截,然后通过设置request的character encoding来一次性进行转码。

public class SetCharacterEncodingFilter implements Filter {

    
private String encode = "UTF-8";

    
/*
     * (non-Javadoc)
     * 
     * @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
     
*/

    
public void init(FilterConfig config) throws ServletException {
        String encodeName 
= config.getInitParameter("encoding");
        
if (encodeName != null && !"".equals(encodeName.trim())) {
            
this.encode = encodeName;
        }

    }


    
/*
     * (non-Javadoc)
     * 
     * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest,
     *      javax.servlet.ServletResponse, javax.servlet.FilterChain)
     
*/

    
public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) 
throws IOException, ServletException {

        HttpServletRequest hreq 
= (HttpServletRequest) request;
        HttpServletResponse hresp 
= (HttpServletResponse) response;
        
// HttpRequestWrapper req = new HttpRequestWrapper(hreq);
        hreq.setCharacterEncoding(this.encode);
        hresp.setContentType("text/html;charset=" + encode);
        chain.doFilter(hreq, hresp);
    }


    
/*
     * (non-Javadoc)
     * 
     * @see javax.servlet.Filter#destroy()
     
*/

    
public void destroy() {
    }


}

③HTTP Request封装器

在上面的filter代码中我们见到了一句注释:HttpRequestWrapper req = new HttpRequestWrapper(hreq);这是下面要介绍的一种方法。如果学习过设计模式我们会知道所谓的“Wrapper”类,其实都是一个Decorator/Proxy模式的实现。即通过在“Wrapper”类中包含一个对象,然后暴露和该对象同样方法的接口,在客户端调用该对象的方法之前做一些手脚。JDK中的动态代理DynamicProxy原理就和这个类似。


在Servlet的API中有一个类:HttpServletRequestWrapper。如果查看API我们会发现有趣的地方:


原来这个类也是实现了HttpServletRequest接口的。于是乎我们可以通过创建一个继续于该类的“Wrapper”,然后覆盖HttpServletRequest的getParameter(key)和getParameters()方法,在这两个方法返回字符串之前,进行重新编码。

package wrapper;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

public class GetHttpServletRequestWrapper extends HttpServletRequestWrapper {

    
private String charset = "UTF-8";

    
public GetHttpServletRequestWrapper(HttpServletRequest request) {
        
super(request);
    }


    
/**
     * 获得被装饰对象的引用和采用的字符编码
     * 
@param request
     * 
@param charset
     
*/

    
public GetHttpServletRequestWrapper(HttpServletRequest request,
            String charset) 
{
        
super(request);
        
this.charset = charset;
    }


    
/**
     * 实际上就是调用被包装的请求对象的getParameter方法获得参数,然后再进行编码转换
     
*/

    
public String getParameter(String name) {
        String value 
= super.getParameter(name);
        value 
= value == null ? null : convert(value);
        
return value;
    }


    
public String convert(String target) {
        System.out.println(
"编码转换之前:" + target);
        
try {
            
return new String(target.trim().getBytes("ISO-8859-1"), charset);
        }
 catch (UnsupportedEncodingException e) {
            
return target;
        }

    }


}

于是我们可以修改一些前面的filter,去掉那句注释和下面的request.setCharacterEncoding("UTF-8")。为什么呢?因为我们知道reuqest.getParameters(...)方法是对GET和POST请求同样的,现在我们已经在每次获取参数值之前做了转换,所以不论是GET还是POST请求,都可以正常的转码了。

要记住:request.setCharacterEncoding(...)方法只对将数据保存在HTTP信息体中的POST请求有效,对于将数据直接粘附与URL后面的GET请求是无效的。但是不论GET还是POST请求,他们都是通过request.getParameter(...)来获取参数的。这其实是一个一劳永逸的方法!

④Servlet

如果在前面我们正确地设置了Tomcat,过滤器,封装器,那么对于Servlet来说,要获取参数就是简单的request.getParameter(PARAM_NAME);了,但是如果没有设置过滤器或封装器,就只能手工地调用下面的语句:

String tmp = request.getParameter(PARAM_NAME);
String param = new String(tmp.getBytes("ISO-8859-1"), encoding);

通常来说这个encoding就是你应用的编码,亦是JSP页面的编码。如果JSP页面是GBK,而你的代码是UTF-8,那么用UTF-8做encoding就肯定乱码了。此时就得用上前面所说的GBK转UTF-8方法,得到原始字节后进行扩展,使之成为UTF-8格式的字符数组再进行解码了

PS:上面这种情况是经常出现的。特别是在你要抓取别人的网页内容时,对方可能使用的是GBK/GB2312编码,而你的Web应用是UTF-8编码。

在解决完请求的乱码问题之后,接下来要解决的就是响应的乱码问题了。响应的乱码问题通常发生于响应内容的编码和JSP页面,HTML网页的编码不同,所以在前面的过滤器中我们一并把这个问题给解决了:
hresp.setContentType("text/html;charset=" + encode);

或者调用
hresp.setCharaterEncoding("UTF-8");

来看看这个用法和HttpServletRequest.setCharacterEncoding有什么区别吧!

Sets the character encoding (MIME charset) of the response being sent to the client, for example, to UTF-8. If the character encoding has already been set by setContentType(java.lang.String) or setLocale(java.util.Locale), this method overrides it. Calling setContentType(java.lang.String) with the String of text/html and calling this method with the String of UTF-8 is equivalent with calling setContentType with the String of text/html; charset=UTF-8.

This method can be called repeatedly to change the character encoding. This method has no effect if it is called after getWriter has been called or after the response has been committed.

和前者不同,这个方法可以在getWriter()被调用后再调用,而且可以被多次调用。而request.setCharacterEncoding只能在request.getParameter之前调用。

其次就是这个方法的优先级是最高的。如果之前已经通过setContentType或setLocale指定编码,再次调用该方法会覆盖之前的配置。

⑤JSP

在JSP页面中,和编码相关的几个参数有
 A.contentType
 B.pageEncoding

这两个参数有什么不同呢?contentType是用来告诉浏览器,当接收到服务器传输回来的响应体内容后要用什么编码解析,这个是没有疑问的。但pageEncoding呢?

我们知道JSP页面在运行时会被容器编译成.class文件,对于文件必然有“文件编码”这一说。对了!pageEncoding就是告诉容器要以什么方式读入JSP文件。如果你的页面有中文字符或其他双字节字符,那么就必须用UTF-8,GBK,Big5等支持双字节的字符集了。

值得注意的是,虽然容器以pageEncoding指定的方式读入JSP文件,但在最终编译成.class文件后,还是以Unicode保存的。例如JSP页面的"中文"二字,在.class文件中会以"\u4e2d;\u6587;"保存。

⑥HTML

如果说我们可以通过response.setContentType或page指令来指定动态页面的编码格式,那么对于HTML这样静态的页面就无能为力了。于是就得使用另外一种方式了:
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

⑦属性配置文件

Java的国际化(i18N)是很多Web应用必备的功能之一,对于菜单文字,提示信息我们都可以用.properties的方式保存,但是在保存之前我们需要将内容转换成unicode的值,于是就要使用native2ascii命令了。具体可以参考前面的【Java基础专题】编码与乱码(07)---native2ascii命令的用法

【工具配置】

在团队合作中,由于每个人的开发环境不一致,通常会出现一个人本来可以正常显示中文的JSP文件或者Java文件,到了另外一个人的机器上就变成乱码了。通常这都是由于编辑器的编码不一致而造成的。以Eclipse为例:



在Dreamweaver中,我们可以这样设置/改变页面的编码


注意:在团队开发中,明确开发环境的配置时通常会忽略IDE编码的选择,导致大量不可逆的乱码情况出现。应该值得注意

良好的编程习惯,对编码和数据传输流程的清晰认识,规范的配置是确保JavaEE应用不会出现乱码的三大法宝


-------------------------------------------------------------
生活就像打牌,不是要抓一手好牌,而是要尽力打好一手烂牌。
posted on 2010-02-24 10:25 Paul Lin 阅读(4254) 评论(1)  编辑  收藏 所属分类: J2SE

只有注册用户登录后才能发表评论。


网站导航:
 
<2010年2月>
31123456
78910111213
14151617181920
21222324252627
28123456
78910111213

常用链接

留言簿(21)

随笔分类

随笔档案

BlogJava热点博客

好友博客

搜索

  •  

最新评论

阅读排行榜

评论排行榜