[ HTML 版请阅读 http://beansoft./ajax/JspRuntime/readme/index.htm] (原创)在 JSP/Servlet 中使用 Bean 自动属性填充机制 作者: BeanSoft 2004.04 Abstract: 大家都知道, 在 JSP 开发中经常用到的一个操作就是从 request 中读取参数, 然后把它赋给 JavaBean 中对应的实现. JSP 提供了一个操作 <jsp:setProperty name="beanName" property="*"/> 来简化这一过程. 但是在 Servlet 中却无法直接使用这个方法, 而且中文用户经常出现汉字值无法正确取出以及用户输入非法数值时页面出错的情况. 本文将就如何从修改 Tomcat 源码解决这些问题展开讨论, 并展示已经取得的部分成果...... 从 JSP 规范刚刚推出的时候起, 为了方便对 Bean 的操作, 里面就包含了一个内置指令 <jsp:setProperty> 来简化从 HttpServletRequest 中的参数到 JavaBean 的对应参数的读取和设置工作. 的确, 在纯英文和 JSP 环境下, 这个操作减轻了相当大的重复使用 request.getParameter("xxx") 和 someBean.setXxx(xxx) 的工作, 特别是在当有大量属性需要读取和设置的时候. 不过根据我个人在 Tomcat 下的使用经验, 这个操作很少能对中文的参数值进行正确的读取和设置工作, 归根结底是由 Tomcat 的默认请求的参数的字符集都是 ISO8859-1 造成的. 这里不再详细讨论相关的解决方法, 只关注于本文的核心内容. 而且更令人头痛的是, 一旦用户不小心在 Bean 的对应属性中没有输入正确的值, 例如本来应该是一个数字却输入了字母, 这个操作就会报错, 并跳出整个流程处理. 在实际中, 可能并不需要如此的稍有错误就页面停止执行的情况出现, 因此这里也将考虑解决这个问题. 首先简要介绍一下测试的时候所用到的一个很简单的 JavaBean: sample.SampleBean 只有两个不同类型的属性: id 和 name, 这两种类型也方便我们对表单值出错时的测试. 接下来我们需要通过两个简单的页面来搞清楚 Tomcat 的底层到底是如何实现这个操作的. 将 SampleBean 类复制到 [TOMCAT安装目录]/webapps/ROOT/WEB-INF/classes/sample 目录下, 然后在您的 Tomcat 的 webapps/ROOT 目录下创建一个名为 test.jsp 的文件, 内容简单如下: <jsp:useBean id="sampleBean" scope="page" class="sample.SampleBean" /> <jsp:setProperty name="sampleBean" property="*"/> , 然后在您的浏览器里输入地址(假定您是按照默认的配置运行的 Tomcat) http://localhost:8080/test.jsp, 这将使 Tomcat 翻译 JSP 文件, 然后编译到 servlet, 最后运行这个页面. 当然, 这个时候在浏览器里您什么也看不到, 我们的目的只是为了弄清楚到底 Tomcat 是如何实现自动填充属性的工作的. 打开目录[TOMCAT安装目录]/work, 在里面对应的目录下仔细查找的话会找到一个对应的包含 test_jsp 字样的 .java 文件(不同的 Tomcat 版本这个文件名字会有所变化), 打开这个文件查看其内容, 您总可以看到类似如下的语句: ...... // begin [file="E:\\index.jsp"from=(1,0);to=(1,49)] JspRuntimeLibrary.introspect(pageContext.findAttribute("sampleBean"), request); // end ...... , 这就是 <jsp:setProperty name="sampleBean" property="*"/> 的真正实现语句, 它调用了类 org.apache.jasper.runtime.JspRuntimeLibrary 中的一个方法. 好了, 一切都将从这里开始, 解决相关问题的途径就在这里. 打开 Tomcat 源码(我这里使用的是 Tomcat 4.12 的源码, 这个源码可以从 Tomcat 的站点下载, 如果您安装有 JBuilder 开发环境, 那么这个源码就位于目录 [JBuilder安装目录]\extras\jakarta-tomcat-4.1.12-src 下. 搜索 JspRuntimeLibrary.java, 打开它, 再查找方法 introspect, 可以看到如下代码: // __begin introspectMethod public static void introspect(Object bean, ServletRequest request) throws JasperException { Enumeration e = request.getParameterNames(); while ( e.hasMoreElements() ) { String name = (String) e.nextElement(); String value = request.getParameter(name); introspecthelper(bean, name, value, request, name, true); } } // __end introspectMethod 它通过循环读取 request 对象里面的所有参数, 然后再尝试设置这些值到和参数名称相同的 Bean 中的属性上去. 看到带下划线的那个代码了吗? 这是我们再熟悉不过的一个语句了, 而且 Tomcat 下这个默认的取参数的操作对汉字的读取总是字符集编码错误的, 所以最后填充给 Bean 的属性后的取值也是错误的, 这也是论坛上经常出现的关于 JSP 中文问题中经常要讨论的一个话题. 然后再看看 introspecthelper 方法, 发现它最后调用了 internalIntrospecthelper 方法. Ok, 这就是属性填充的最终实现代码. 看看这个方法的实现, 可以发现如下所示的读取参数列表的代码: ... Class t = type.getComponentType(); String[] values = request.getParameterValues(param); //XXX Please check. if(values == null) return; ... if(value == null || (param != null && value.equals(""))) return; Object oval = convert(prop, value, type, propertyEditorClass); if ( oval != null ) method.invoke(bean, new Object[] { oval }); ... 这个地方也会出现汉字无法正确读出的问题, 因此需要修改. 第二段代码则是给出了如何将输入的字符串值转换为最终的相应的数据类型, 那就是调用 convert 方法, 而 convert 方法会出现异常, 这样, 整个循环处理就会因而中断并报错. 可以看到, 如果转换结果值为 null 的话, 就不会跳出出错处理流程. 因此考虑在 convert 失败时返回一个 null 值来取代原来的抛出异常的做法. 具体的修改过程并不十分有趣, 而是非常乏味. convert 方法被修改, 当转换类型发生错误时捕获它并返回一个 null 的转换结果, 字符集问题则只是简单的加入了转换内码的方法, 从而允许用户指定 request 所使用的字符集和最后转换到的字符集, 便于将程序移植到不同的字符集特性的服务器上工作. 主要的修改结果如下所示: // __begin convertMethod /** * 将输入的字符串转换为目标类型(会出错). * Note: 修改出错处理, 出现异常时只返回空值, * 即可改变 Bean 赋值出错时发生中断的问题. */ public static Object convert( String propertyName, String s, Class t, Class propertyEditorClass) throws JasperException { ... } catch (Exception ex) { // Modified by BeanSoft Studio -- 只返回空值, 不报错 System.out.println(ex); return null; // Old code below: //throw new JasperException(ex); } } // __end convertMethod // __begin introspectMethod /** * 用 ServletRequest 的默认字符集实现 ServletRequest 到 Bean 值的自动填充工作. * 实现和 Tomcat 默认的操作相同的功能. * * @param bean JavaBean 对象 * @param request ServletRequest 对象 */ public static void introspect(Object bean, ServletRequest request) { //throws JasperException { introspect(bean, request, null); } // __end introspectMethod // __begin introspectMethod /** * 从默认字符集转换到目标字符集并进行 ServletRequest 到 Bean 值的自动填充工作. * * @param bean JavaBean 对象 * @param request ServletRequest 对象 * @param encoding 目标字符集(如果要转换到中文字符集, 此参数应该为 "GB2312" * 或者 "GBK"), 如果参数为空, 则不做任何转码 */ public static void introspect(Object bean, ServletRequest request, String encoding) { //throws JasperException { introspect(bean, request, null, encoding); } // __end introspectMethod // __begin introspectMethod /** * 用给定字符编码转换实现 ServletRequest 到 Bean 值的自动填充工作. * * @param bean JavaBean 对象 * @param request ServletRequest 对象 * @param sourceEncoding ServeltRequest 的源字符集(如果为空, 则默认为 ISO8859-1) * @param targetEncoding 目标字符集, 例如 "GB2312" 或者 "GBK" */ public static void introspect(Object bean, ServletRequest request, String sourceEncoding, String targetEncoding) { //throws JasperException { // Check default encoding if(sourceEncoding == null || sourceEncoding.length() == 0) { sourceEncoding = "ISO8859-1" } try { Enumeration e = request.getParameterNames(); while (e.hasMoreElements()) { String name = (String) e.nextElement(); String value = request.getParameter(name); // 字符集转换, 仅当目标字符集非空时进行此操作 if(value != null && targetEncoding != null && targetEncoding.length() > 0) { try { value = new String(value.getBytes(sourceEncoding), targetEncoding); } catch (UnsupportedEncodingException exception) { exception.printStackTrace(); } } introspecthelper(bean, name, value, request, name, true, sourceEncoding, targetEncoding); } } catch (Exception e) { e.printStackTrace(); } } // __end introspectMethod /** * Bean 属性设置的实现(可转换字符集编码版本). * * @param bean * @param prop 属性名称 * @param value * @param request * @param param 表单参数名称 * @param ignoreMethodNF * @param sourceEncoding ServeltRequest 的源字符编码名称(如果为空, 则默认为 ISO8859-1) * @param targetEncoding 目标字符集(可实现中文化处理, 则此参数应该为 "GB2312" 或者 "GBK") * * @throws JasperException */ private static void internalIntrospecthelper( Object bean, String prop, String value, ServletRequest request, String param, boolean ignoreMethodNF, String sourceEncoding, String targetEncoding ) throws JasperException { ... // 从 servlet 读取参数(此处应该加入字符集处理机制) String[] values = request.getParameterValues(param); //XXX Please check. if (values == null) { return; } else { // TODO: 字符集转换 - Added by BeanSoft Studio on 2004-04-06 if(sourceEncoding != null && targetEncoding != null && targetEncoding.length() > 0) { for(int i = 0; i < values.length; i++) { values[i] = new String(values[i].getBytes( sourceEncoding), targetEncoding); } } } ... } 通过和前面的代码段比较, 将很容易的看出来到底哪些部分被修改过已实现我的目的: 自动转换字符集和避免出错时中止操作. Ok, 现在有三个方法来完成从 ServletRequest 到 JavaBean 的值自动填充工作, 为了不和 Tomcat 自带的类库冲突, 这些类已经被独立出来, 并重新放在了一个名为 studio.beansoft 的包中, 目前这些类已经在 Tomcat 3, 4 下通过了测试, 测试过的操作系统有 Window 2000, Red Hat Linux 8 以及 SCO OpenServer(UNIX). 这三个方法的使用方式如下: 在 JSP 和 Servlet 中可以调用下列代码: studio.beansoft.jasper.runtime.JspRuntimeLibrary.introspect(myBean, request); 来完成和 <%jsp:setProperty name="myBean" property="*"/> 同样的操作(除了不再出错时中止). 也可以自动处理字符串的编码问题, 例如从 Tomcat 下的默认 ServletRequest 对象 字符集编码(ISO8859-1)转换到 GBK 编码可以调用下列代码: studio.beansoft.jasper.runtime.JspRuntimeLibrary.introspect(myBean, request,"GBK"); 还可以显式的指定源字符集到目标字符集的编码, 例如从 UTF8 编码转换到 GBK 编码: studio.beansoft.jasper.runtime.JspRuntimeLibrary.introspect(myBean, request,"UTF-8", "GBK"); 附件中带了一个小 WEB 应用程序来演示这个工具类的使用方法, 并拿它和标准 Tomcat 实现进行比较. 把那个名为 web-app.war 的 web 应用安装到 Tomcat 下(复制该文件到 [TOMCAT安装目录]/webapps 下, 然后重新启动 Tomcat), 在浏览器中键入: http://localhost:8080/web-app/test.jsp 来启动测试程序. 相关资源: 1. 在线浏览部分程序源码: studio.beansoft.jasper.runtime.JspRuntimeLibrary org.apache.jasper.runtime.JspRuntimeLibrary 2. 下载本文完整源代码和示例 Web 应用 jspruntime.zip 如果您对本文有什么好的建议或者意见, 欢迎联系我: beansoft@126.com 或者 beansoftstudio@yahoo.com.cn . 注: 本文使用的 HTML 格式的 Java 程序代码使用 Java2HTML Version 1.3.1 生成.
|