proxool 种AdminServlet类 不能显示中文

Tomcat6+proxool中文不能正常显示问题

 汉字的显示处理是一个用java进行web应用开发很基础而又经常出来烦我们的问题,它永远有新的花样来摧残我们脆弱的神经。

  近日给一个项目升级环境,由Tomcat4.1.31升级到Tomcat5.5或6.0,JDK也由1.4升级到1.6,经过一些简单修改一切都还算顺利(就是改了改代码中使用JDK6新增的保留字的问题,tomcat的兼容性当时看还是不错的,很顺利)。但在后来测试中发现我们用的proxool连接池的管理页面不能正常显示了,出现类似以下错误:

java.io.CharConversionException: Not an ISO 8859-1 character: 十
javax.servlet.ServletOutputStream.print(ServletOutputStream.java:89)
org.logicalcobwebs.proxool.admin.servlet.AdminServlet.printDefinitionEntry(AdminServlet.java:515)
org.logicalcobwebs.proxool.admin.servlet.AdminServlet.doSnapshot(AdminServlet.java:273)
org.logicalcobwebs.proxool.admin.servlet.AdminServlet.doStats(AdminServlet.java:145)
org.logicalcobwebs.proxool.admin.servlet.AdminServlet.doGet(AdminServlet.java:129)
javax.servlet.http.HttpServlet.service(HttpServlet.java:690)
javax.servlet.http.HttpServlet.service(HttpServlet.java:803)

  我参考了一下正常的情况,这里的“十”应该是当前的月份“十一月”三个字的第一个字。也就是说是当前输出不认识汉字,只能输出ISO 8859-1编码的字符(或者可以按某种规则被正常解析成该编码的字符,这种规则我们在后面可以看到)。

  首先要明确,在原来的Tomcat4+JDK1.4的环境是没有这个问题的,那么问题就是新换的环境上了。
分析一下org.logicalcobwebs.proxool.admin.servlet.AdminServlet这个类不难看出这里看到的无法正常转换的汉字来自于以下这条语句:

private static final DateFormat DATE_FORMAT = new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss");

  这里使用了“MMM”来取本地化格式的月份,当然在中文系统中就得到了“xx月”字样了。虽然这样的格式会给代码的移植带来不必要的麻烦,而且实际输出的类似“20-十一月-2007 08:00:00”这种格式看起来也是不伦不类(所以本文提供的修改方案中建议把这种改掉),但这并不是本文讨论的问题的本源,不支持中文输出才是关键。

  那我们再来看下异常栈,异常是在javax.servlet.ServletOutputStream类(抽象类javax.servlet.ServletOutputStream继承了java.io.OutputStream)的print(String s)方法中抛出来的,通过反编译工具可以很方便地看到这个类的原码(对于tomcat4一般这个类在common/lib/servlet.jar包中,tomcat6在servlet-api.jar包中)。我们可以看到print(String s)方法的以下代码段。

public void print(String s) throws IOException {
if (s==null) s="null";
int len = s.length();
for (int i = 0; i < len; i++) {
     char c = s.charAt (i);

     //
     // XXX NOTE: This is clearly incorrect for many strings,
     // but is the only consistent approach within the current
     // servlet framework. It must suffice until servlet output
     // streams properly encode their output.
     //
     if ((c & 0xff00) != 0) { // high order byte must be zero
   String errMsg = lStrings.getString("err.not_iso8859_1");
   Object[] errArgs = new Object[1];
   errArgs[0] = new Character(c);
   errMsg = MessageFormat.format(errMsg, errArgs);
   throw new CharConversionException(errMsg);
     }
     write (c);
}
}

  这个if ((c & 0xff00) != 0)就是抛出异常的起因之一了,汉字一定是过不了这一关的。经比较tomcat4与tomcat6带的这个类的算法没区别,但为什么在tomcat4会没有抛异常出来呢?看来一定是tomcat4在这里做了什么手脚。即然javax.servlet.ServletOutputStream是个抽象类,我们实现运行的类一定不会是它了,而是它的子类,这个子类一定是最终被tomcat实现了。经过分析tomcat代码最终找到了真正的实现类。tomcat4使用了org.apache.coyote.tomcat4.CoyoteOutputStream继承ServletOutputStream类,而tomcat6使用了org.apache.catalina.connector.CoyoteOutputStream继承ServletOutputStream类。从名字我们就能看出,前者十之八九是由后者演变而来的(coyote包中的一些类从tomcat5开始就不在org.apache.coyote.tomcatx包中了,而被放到了org.apache.catalina.connector包中)。
分析org.apache.coyote.tomcat4.CoyoteOutputStream类,我们可以看到一个方法如下:

public void print(String s) throws IOException
{
    ob.write(s);
}

而在org.apache.catalina.connector.CoyoteOutputStream类中没有的这个方法。

  覆盖了这个方法,也就意味着那个恼人的“if ((c & 0xff00) != 0)”就不会被执行到了。也就是说tomcat6新的CoyoteOutputStream类去掉这个了方法,这才是在tomcat4中可以但换成tomcat6就出问题的根本原因。当然tomcat6不再覆盖print(String s)方法也是有道理的,就是让我们使用Writer而不再使用OutputStream来输出HTML或XML之类的内容。

  这样看来使用OutputStream的print(String s)方法输出是不应该再用了,那如何输出呢?

  我们先来分析一下,OutputStream是输出二进制流了,也就是处理byte流的,而汉字明显是要用char类型来存储处理的,那么自然要用对应的Writer来进行输出操作了。幸好javax.servlet.http.HttpServletResponse类早就支持Writer输出了,但新问题是在tomcat的不同版本中的实现(如果有的话)会不会像OutputStream那样有差异进而导致此种问题或其它问题呢?这个答案还是要分析了不同版本的tomcat的代码才能晓得了。

  首先,容易知道使用response.getWriter()得到的是一个java.io.PrintWriter类。经分析,在tomcat4中最终使用的是org.apache.catalina.connector.ResponseWriter类,而在tomcat6中最终使用的是org.apache.catalina.connector.CoyoteWriter类。两个类的getWriter()方法分别如下:

//tomcat4,org.apache.catalina.connector.ResponseWriter
    public PrintWriter getWriter() throws IOException
    {
        if(writer != null)
            return writer;
        if(stream != null)
        {
            throw new IllegalStateException(sm.getString("responseBase.getWriter.ise"));
        } else
        {
            ResponseStream newStream = (ResponseStream)createOutputStream();
            newStream.setCommit(false);
            OutputStreamWriter osr = new OutputStreamWriter(newStream, getCharacterEncoding());
            writer = new ResponseWriter(osr, newStream);
            stream = newStream;
            return writer;
        }
    }

//tomcat6,org.apache.catalina.connector.CoyoteWriter
    public PrintWriter getWriter()
        throws IOException {

        if (usingOutputStream)
            throw new IllegalStateException
                (sm.getString("coyoteResponse.getWriter.ise"));

        if (Globals.STRICT_SERVLET_COMPLIANCE) {
            /*
             * If the response's character encoding has not been specified as
             * described in <code>getCharacterEncoding</code> (i.e., the method
             * just returns the default value <code>ISO-8859-1</code>),
             * <code>getWriter</code> updates it to <code>ISO-8859-1</code>
             * (with the effect that a subsequent call to getContentType() will
             * include a charset=ISO-8859-1 component which will also be
             * reflected in the Content-Type response header, thereby satisfying
             * the Servlet spec requirement that containers must communicate the
             * character encoding used for the servlet response's writer to the
             * client).
             */
            setCharacterEncoding(getCharacterEncoding());
        }

        usingWriter = true;
        outputBuffer.checkConverter();
        if (writer == null) {
            writer = new CoyoteWriter(outputBuffer);
        }
        return writer;

    }

  对比后我们看到,二者虽然代码差异很大,其中tomcat6的实现多了outputBuffer.checkConverter();这一行。其实这里只是进行从char到byte的转换,而对于汉字只要字符编码设置无误就不存在问题的(tomcat在这里有较复杂的处理,目的在于兼容JDK1.1,而不是使用nio)。字符编码的设置方法是类似“response.setContentType("text/html;charset=GBK");”这样的代码行。
问题都排除了,那么得出的结论就是二者的Writer输出是一致的,完全可以使用Writer来代替StreamOutput进行输出了。

  既然找到问题产生的根本原因和处理方法,那么下一步就是着手解决它了。我们基本的改进方案是只改进org.logicalcobwebs.proxool.admin.servlet.AdminServlet类,核心是让不再使用OutputStream输出HTML而是用Writer。

具体步骤大致如下:
1、改掉憋脚的日期格式,由
private static final DateFormat DATE_FORMAT = new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss");
替换为:
private static final DateFormat DATE_FORMAT = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss");

2、在doGet方法的“response.setHeader("Pragma", "no-cache");”一行后加上以下两行:
response.setLocale(new Locale((String)System.getProperties().get("user.language"), (String)System.getProperties().get("user.country")));
response.setContentType("text/html;charset=" + System.getProperties().get("file.encoding"));
要注意的是这里的字符集设置是依赖于当前操作系统环境的,也就是说,当前操作系统设的是GBK,这里才会被设置为GBK。如果认为没必要的话那么直接写死成"text/html;charset=GBK"即可,locale可直接使用Locale.SIMPLIFIED_CHINESE即可;

3、将所有private void doXxx(StreamOutput out, ...)方法的第一个参数的类型都换成java.io.PrintWriter。再把doGet方法中的一些response.getOutputStream()语句换成response.getWriter(),一切搞定;

4、还要将doGet方法中几处response.getStreamOutput()都替换为response.getWriter();

5、编译后将生成的新的org.logicalcobwebs.proxool.admin.servlet.AdminServlet类替换掉proxool.jar包中原有的,大功告成。


  总结一下,这个看似简单的汉字显示问题,其背景还是有些复杂的。尤其是对一个J2EE服务器软件(当然tomcat比较weblogic之类的真正的J2EE服务器还是要简单很多的)进行代码分析还是颇耗精力的。尽管付出了一些辛苦,但得到一个让我们满意又有小小激动的结果,那就是,在java中通常我们可以完美地从本质上处理一些问题,无论是对于服务器还是第三方插件。可见,开源带来的可用代码虽然可谓浩如烟海,但只要我们是个训练有素的水手,再架上一叶可顺风而破浪的轻舟,自然可畅游其间,快乐地探寻那些静静地等待着的彼岸。

你可能感兴趣的:(apache,tomcat,c,servlet,C#)