海鸥航迹

学习之笔记,好文之收集。

导航

Struts原理与实践

(第1部分)

一、 什么是Struts

框架(Framework)是可重用的,半完成的应用程序,可以用来产生专门的定制程序。

您只要细心地研究真实的应用程序,就会发现程序大致上由两类性质不同的组件组成,一类与程序要处理的具体事务密切相关,我们不妨把它们叫做业务组件;另一类是应用服务。比如说:一个税务征管系统和一个图书管理系统会在处理它们的业务方面存在很大的差异,这些直接处理业务的组件由于业务性质的不同不大可能在不同的系统中重用,而另一些组件如决定程序流向的控制、输入的校验、错误处理及标签库等这些只与程序相关的组件在不同的系统中可以很好地得到重用。人们自然会想要是把这些在不同应用程序中有共性的一些东西抽取出来,做成一个半成品程序,这样的半成品就是所谓的程序框架,再做一个新的东西时就不必白手起家,而是可以在这个*础上开始搭建。实际上,有些大型软件企业选择自己搭建这样的框架。但大多数中小型软件企业或者其他组织,没有条件自己建立框架。

Struts作为一个开放原代码的应用框架,在最近几年得到了飞速的发展,在JSP Web应用开发中应用得非常广泛,有的文献上说它已经成为JSP Web应用框架的事实上的标准。那么,究竟什么是Struts呢?

要回答这个问题还得从JSP Web应用的两种*本的结构模式:Model 1和Model 2说起,为了给读者一些实实在在的帮助,并力图让学习曲线变得平坦一些,我想采用实例驱动的方法来逐步深入地回答有关问题,因为,学一门技术的最好方法莫过于在实践中学习、在实践中体会,逐步加深对其精神实质的理解和把握,而不是一上来就引入一大堆新概念让大家觉得无所适从,或者死记硬背一大堆概念而面对一个真正的实际需求束手无策。正如,一个人即使在书本上学成了游泳博士,只要他不下水,我想他也是不大可能真正会游泳的。

Model 1结构如图1所示:



图1

mode1 1是一个以JSP文件为中心的模式,在这种模式中JSP页面不仅负责表现逻辑,也负责控制逻辑。专业书籍上称之为逻辑耦合在页面中,这种处理方式,对一些规模很小的项目如:一个简单的留言簿,也没什么太大的坏处,实际上,人们开始接触一些对自己来说是新的东西的时候,比如,用JSP访问数据库时,往往喜欢别人能提供一个包含这一切的单个JSP页面,因为这样在一个页面上他就可以把握全局,便于理解。但是,用Model 1模式开发大型时,程序流向由一些互相能够感知的页面决定,当页面很多时要清楚地把握其流向将是很复杂的事情,当您修改一页时可能会影响相关的很多页面,大有牵一发而动全身的感觉,使得程序的修改与维护变得异常困难;还有一个问题就是程序逻辑开发与页面设计纠缠在一起,既不便于分工合作也不利于代码的重用,这样的程序其健壮性和可伸缩性都不好。

Grady Booch等人在UML用户指南一书中,强调建模的重要性时,打了一个制作狗窝、私人住宅、和大厦的形象比喻来说明人们处理不同规模的事物时应该采用的合理方法一样,人们对不同规模的应用程序也应该采用不同的模式。

为了克服Model 1的缺陷,人们引入了Model 2,如图2所示:



图2

它引入了"控制器"这个概念,控制器一般由servlet来担任,客户端的请求不再直接送给一个处理业务逻辑的JSP页面,而是送给这个控制器,再由控制器根据具体的请求调用不同的事务逻辑,并将处理结果返回到合适的页面。因此,这个servlet控制器为应用程序提供了一个进行前-后端处理的中枢。一方面为输入数据的验证、身份认证、日志及实现国际化编程提供了一个合适的切入点;另一方面也提供了将业务逻辑从JSP文件剥离的可能。业务逻辑从JSP页面分离后,JSP文件蜕变成一个单纯完成显示任务的东西,这就是常说的View。而独立出来的事务逻辑变成人们常说的Model,再加上控制器Control本身,就构成了MVC模式。实践证明,MVC模式为大型程序的开发及维护提供了巨大的便利。

其实,MVC开始并不是为Web应用程序提出的模式,传统的MVC要求M将其状态变化通报给V,但由于Web浏览器工作在典型的拉模式而非推模式,很难做到这一点。因此有些人又将用于Web应用的MVC称之为MVC2。正如上面所提到的MVC是一种模式,当然可以有各种不同的具体实现,包括您自己就可以实现一个体现MVC思想的程序框架,Struts就是一种具体实现MVC2的程序框架。它的大致结构如图三所示:



图三

图三*本勾勒出了一个*于Struts的应用程序的结构,从左到右,分别是其表示层(view)、控制层(controller)、和模型层(Model)。其表示层使用Struts标签库构建。来自客户的所有需要通过框架的请求统一由叫ActionServlet的servlet接收(ActionServlet Struts已经为我们写好了,只要您应用没有什么特别的要求,它*本上都能满足您的要求),根据接收的请求参数和Struts配置(struts-config.xml)中ActionMapping,将请求送给合适的Action去处理,解决由谁做的问题,它们共同构成Struts的控制器。Action则是Struts应用中真正干活的组件,开发人员一般都要在这里耗费大量的时间,它解决的是做什么的问题,它通过调用需要的业务组件(模型)来完成应用的业务,业务组件解决的是如何做的问题,并将执行的结果返回一个代表所需的描绘响应的JSP(或Action)的ActionForward对象给ActionServlet以将响应呈现给客户。

过程如图四所示:



图四

这里要特别说明一下的是:就是Action这个类,上面已经说到了它是Struts中真正干活的地方,也是值得我们高度关注的地方。可是,关于它到底是属于控制层还是属于模型层,存在两种不同的意见,一种认为它属于模型层,如:《JSP Web编程指南》;另一些则认为它属于控制层如:《Programming Jakarta Struts》、《Mastering Jakarta Struts》和《Struts Kick Start》等认为它是控制器的一部分,还有其他一些书如《Struts in Action》也建议要避免将业务逻辑放在Action类中,也就是说,图3中Action后的括号中的内容应该从中移出,但实际中确有一些系统将比较简单的且不打算重用的业务逻辑放在Action中,所以在图中还是这样表示。显然,将业务对象从Action分离出来后有利于它的重用,同时也增强了应用程序的健壮性和设计的灵活性。因此,它实际上可以看作是Controller与Model的适配器,如果硬要把它归于那一部分,笔者更倾向于后一种看法,即它是Controller的一部分,换句话说,它不应该包含过多的业务逻辑,而应该只是简单地收集业务方法所需要的数据并传递给业务对象。实际上,它的主要职责是:

  • 校验前提条件或者声明
  • 调用需要的业务逻辑方法
  • 检测或处理其他错误
  • 路由控制到相关视图

    上面这样简单的描述,初学者可能会感到有些难以接受,下面举个比较具体的例子来进一步帮助我们理解。如:假设,我们做的是个电子商务程序,现在程序要完成的操作任务是提交定单并返回定单号给客户,这就是关于做什么的问题,应该由Action类完成,但具体怎么获得数据库连接,插入定单数据到数据库表中,又怎么从数据库表中取得这个定单号(一般是自增数据列的数据),这一系列复杂的问题,这都是解决怎么做的问题,则应该由一个(假设名为orderBo)业务对象即Model来完成。orderBo可能用一个返回整型值的名为submitOrder的方法来做这件事,Action则是先校验定单数据是否正确,以免常说的垃圾进垃圾出;如果正确则简单地调用orderBo的submitOrder方法来得到定单号;它还要处理在调用过程中可能出现任何错误;最后根据不同的情况返回不同的结果给客户。

    二、为什么要使用Struts框架

    既然本文的开始就说了,自己可以建这种框架,为什么要使用Struts呢?我想下面列举的这些理由是显而易见的:首先,它是建立在MVC这种公认的好的模式上的,Struts在M、V和C上都有涉及,但它主要是提供一个好的控制器和一套定制的标签库上,也就是说它的着力点在C和V上,因此,它天生就有MVC所带来的一系列优点,如:结构层次分明,高可重用性,增加了程序的健壮性和可伸缩性,便于开发与设计分工,提供集中统一的权限控制、校验、国际化、日志等等;其次,它是个开源项目得到了包括它的发明者Craig R.McClanahan在内的一些程序大师和高手持续而细心的呵护,并且经受了实战的检验,使其功能越来越强大,体系也日臻完善;最后,是它对其他技术和框架显示出很好的融合性。如,现在,它已经与tiles融为一体,可以展望,它很快就会与JSF等融会在一起。当然,和其他任何技术一样,它也不是十全十美的,如:它对类和一些属性、参数的命名显得有些随意,给使用带来一些不便;还有如Action类execute方法的只能接收一个ActionForm参数等。但瑕不掩瑜,这些没有影响它被广泛使用。

    三、Struts的安装与*本配置

    我们主要针对Struts1.1版本进行讲解,这里假定读者已经配置好java运行环境和相应的Web容器,本文例子所使用的是j2sdk和Tomcat4.1.27。下面,将采用类似于step by step的方式介绍其*础部分。

    安装Struts
    到http://jakarta.apache.org/ 下载Struts的安装文件,本文例子使用的是1.1版。

    接下来您要进行如下几个步骤来完成安装:
    1、解压下载的安装文件到您的本地硬盘
    2、生成一个新的Web应用,假设我们生成的应用程序的根目录在/Webapps/mystruts目录。在server.xml文件中为该应用新建一个别名如/mystruts
    3、从第1步解压的文件中拷贝下列jar文件到/Webapps/mystruts/WEB-INF/lib目录,主要文件有如下一些。

    struts.jar
    commons-beanutils.jar
    commons-collections.jar
    commons-dbcp.jar
    commons-digester.jar
    commons-logging.jar
    commons-pool.jar
    commons-services.jar
    commons-validator.jar


    4、创建一个web.xml文件,这是一个*于servlet的Web应用程序都需要的部署描述文件,一个Struts Web应用,在本质上也是一个*于servlet的Web应用,它也不能例外。

    Struts有两个组件要在该文件中进行配置,它们是:ActionServlet和标签库。下面是一个配置清单:

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3
    //EN" "http://java.sun.com/dtd/web-app_2_3.dtd">
    <web-app>
      <servlet>
        <servlet-name>action</servlet-name>
        <servlet-class>org.apache.struts.action.ActionServlet</servlet-class>
        <init-param>
          <param-name>config</param-name>
          <param-value>/WEB-INF/struts-config.xml</param-value>
        </init-param>
        <init-param>
          <param-name>debug</param-name>
          <param-value>2</param-value>
        </init-param>
        <load-on-startup>2</load-on-startup>
      </servlet>
      <servlet-mapping>
        <servlet-name>action</servlet-name>
        <url-pattern>*.do</url-pattern>
      </servlet-mapping>
      <taglib>
        <taglib-uri>/WEB-INF/struts-bean.tld</taglib-uri>
        <taglib-location>/WEB-INF/struts-bean.tld</taglib-location>
      </taglib>
      <taglib>
        <taglib-uri>/WEB-INF/struts-html.tld</taglib-uri>
        <taglib-location>/WEB-INF/struts-html.tld</taglib-location>
      </taglib>
      <taglib>
        <taglib-uri>/WEB-INF/struts-logic.tld</taglib-uri>
        <taglib-location>/WEB-INF/struts-logic.tld</taglib-location>
      </taglib>
    </web-app>


    上面我们在web.xml中完成了对servlet和标签库的*本配置,而更多的框架组件要在struts-config.xml中进行配置:

    5、创建一个*本的struts-config.xml文件,并把它放在/Webapps/mystruts/WEB-INF/目录中,该文件是*于Struts应用程序的配置描述文件,它将MVC结构中的各组件结合在一起,开发的过程中会不断对它进行充实和更改。在Struts1.0时,一个应用只能有一个这样的文件,给分工开发带来了一些不便,在Struts1.1时,可以有多个这样的文件,将上述缺点克服了。需在该文件中配置的组件有:data-sources

    global-execptions
    form-beans
    global-forwards
    action-mappings
    controller
    message-resources
    plug-in


    配置清单如下:

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE struts-config PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 1.1
    //EN" "http://jakarta.apache.org/struts/dtds/struts-config_1_1.dtd">
    <struts-config>
      <message-resources parameter="ApplicationResources" />
    </struts-config>


    到此为止,我们已经具备了完成一个最简单Struts应用的所需的各种组件。前面已经提到,在开发过程中我们会不断充实和修改上面两个配置描述文件。下面我们将实际做一个非常简单的应用程序来体验一下Struts应用开发的真实过程,以期对其有一个真实的认识。在完成*础部分的介绍后,笔者会给出一些在实际开发中经常用到而又让初学者感到有些难度的实例。最后,会介绍Struts与其他框架的关系及结合它们生成应用程序的例子.
  • 下面,我们就从一个最简单的登录例子入手,以对Struts的主要部分有一些直观而清晰的认识。这个例子功能非常简单,假设有一个名为lhb的用户,其密码是awave,程序要完成的任务是,呈现一个登录界面给用户,如果用户输入的名称和密码都正确返回一个欢迎页面给用户,否则,就返回登录页面要求用户重新登录并显示相应的出错信息。这个例子在我们讲述Struts的*础部分时会反复用到。之所以选用这个简单的程序作为例子是因为不想让过于复杂的业务逻辑来冲淡我们的主题。

    因为Struts是建立在MVC设计模式上的框架,你可以遵从标准的开发步骤来开发你的Struts Web应用程序,这些步骤大致可以描述如下:
    1定义并生成所有代表应用程序的用户接口的Views,同时生成这些Views所用到的所有ActionForms并将它们添加到struts-config.xml文件中。
    2在ApplicationResource.properties文件中添加必要的MessageResources项目
    3生成应用程序的控制器。
    4在struts-config.xml文件中定义Views与 Controller的关系。
    5生成应用程序所需要的model组件
    6编译、运行你的应用程序.

    (第2部分)


    下面,我们就一步步按照上面所说的步骤来完成我们的应用程序:

    第一步,我们的应用程序的Views部分包含两个.jsp页面:一个是登录页面logon.jsp,另一个是用户登录成功后的用户功能页main.jsp,暂时这个页面只是个简单的欢迎页面。

    其中,logon.jsp的代码清单如下:

    <%@ page contentType="text/html; charset=UTF-8" %>
    <%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %>
    <%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %>
    <HTML>
    <HEAD>
    <TITLE><bean:message key="logon.jsp.title"/></TITLE>
    <html:base/>
    </HEAD>
    <BODY>
    <h3><bean:message key="logon.jsp.page.heading"/></h3>
    <html:errors/>
    <html:form action="/logonAction.do" focus="username">
    <TABLE border="0" width="100%">
    <TR>
    <TH align="right"><bean:message key="logon.jsp.prompt.username"/></TH>
    <TD align="left"><html:text property="username"/></TD>
    </TR>
    <TR>
    <TH align="right"><bean:message key="logon.jsp.prompt.password"/></TH>
    <TD align="left"><html:password property="password"/></TD>
    </TR>
    <TR>
    <TD align="right">
      <html:submit><bean:message key="logon.jsp.prompt.submit"/></html:submit>
    </TD>
    <TD align="left">
      <html:reset><bean:message key="logon.jsp.prompt.reset"/></html:reset>
    </TD>
    </TR>
    </TABLE>
    </html:form>
    </BODY>
    </HTML>



    main.jsp的代码清单如下:

    <%@ page contentType="text/html; charset=UTF-8" %>
    <%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %>
    <%@ taglib uri="/WEB-INF/struts-logic.tld" prefix="logic" %>
    
    <HTML>
    <HEAD>
    <TITLE><bean:message key="main.jsp.title"/></TITLE>
    <html:base/>
    </HEAD>
    <BODY>
    <logic:present name="userInfoForm">
    <H3>
      <bean:message key="main.jsp.welcome"/> 
      <bean:write name="userInfoForm" property="username"/>!
    </H3>
    </logic:present>
    </BODY>
    </HTML>



    首先,我们看一下logon.jsp文件,会发现它有这么两个鲜明的特点:一是文件头部有诸如:
    <%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %>
    <%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %>

    这样的指令代码,他们的作用就是指示页面要用到struts的自定义标签,标签库uri是一个逻辑引用,标签库的描述符(tld)的位置在web.xml文件中给出,见上篇文章的配置部分。struts的标签库主要由四组标签组成,它们分别是:

  • bean标签,作用是在jsp中操纵bean
  • logic标签,作用是在jsp中进行流程控制
  • html标签,作用是显示表单等组件
  • template标签,作用是生成动态模板

    关于每类标签的具体作用及语法,因受篇幅限制,不在这里详细讨论,大家可参考struts手册之类的资料。只是心里要明白所谓标签其后面的东西就是一些类,这点与bean有些相似,它们在后端运行,生成标准的html标签返回给浏览器。

    要使用它们显然要把它们的标签库描述文件引入到我们的系统中,这是些以.tld为扩展名的文件,我们要把它们放在/webapps/mystruts/WEB-INF/目录下。引入struts标签后原来普通的html标签如文本框的标签变成了这样的形式

    Jsp文件的第二个特点,就是页面上根本没有直接写用于显示的文字如:username,password等东西,而是用这种形式出现。这个特点为国际化编程打下了坚实的*础,关于国际化编程后面的文章还会专门讨论。

    这个简单的应用所用到的ActionForm为UserInfoForm,代码清单如下:

    package entity;
    import org.apache.struts.action.ActionForm;
    import org.apache.struts.action.ActionMapping;
    import javax.servlet.http.HttpServletRequest;
    
    public class UserInfoForm extends ActionForm{
    
      private String username;
      private String password;
    
    
      public String getUsername() {
        return (this.username);
      }
      public void setUsername(String username) {
        this.username = username;
      }
    
      public String getPassword() {
        return (this.password);
      }
      public void setPassword(String password) {
        this.password = password;
      }
    }


    在你的应用程序的WEB-INF目录下再建一个classes目录,在新建的这个classes目录下再建如下几个目录entity(用于存放ActionForm类)、action目录(用于存放Action类)、bussness目录(用于存放作为Model的业务对象类)。Classes目录下的子目录就是所谓的包,以后,还会根据需要增加相应的包。

    现在,将UserInfoForm.java保存到entity目录中。

    把如下代码添加到/webapps/mystruts/WEB-INF/struts-config.xml文件中

    <form-beans>
        <form-bean name="userInfoForm" type="entity.UserInfoForm" />
      </form-beans>


    特别要提醒一下的是:关于ActionForm的大小写,一定要按照上面的写,以免造成不必要的麻烦。

    到此,我们完成了第一步工作。

    第二步,我们建一个名为ApplicationResource.properties的文件,并把它放在/webapps/mystruts/WEB-INF/classes目录下。它在struts-config.xml的配置信息我们已在第一篇文章的末尾说了,就是:


    目前我们在ApplicationResource.properties文件中加入的内容是:

    #Application Resource for the logon.jsp
    logon.jsp.title=The logon page
    logon.jsp.page.heading=Welcome World!
    logon.jsp.prompt.username=Username:
    logon.jsp.prompt.password=Password:
    logon.jsp.prompt.submit=Submit
    logon.jsp.prompt.reset=Reset
    
    #Application Resource for the main.jsp
    main.jsp.title=The main page
    main.jsp.welcome=Welcome:


    到此,我们已完成了第二个步骤。

    第三步,我们开始生成和配置Controller组件。

    在前面我们已经提到,Struts应用程序的控制器由org.apache.struts.action.ActionServlet和org.apache.struts.action.Action类组成,其中,前者已由Struts准备好了,后者Struts只是为我们提供了个骨架,我们要做的是为实现应用程序的特定功能而扩展Action类,下面是实现我们登录程序的Action类的代码清单:

    package action;
    import java.io.IOException;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpSession;
    import javax.servlet.http.HttpServletResponse;
    import org.apache.struts.action.Action;
    import org.apache.struts.action.ActionError;
    import org.apache.struts.action.ActionErrors;
    import org.apache.struts.action.ActionForm;
    import org.apache.struts.action.ActionForward;
    import org.apache.struts.action.ActionMapping;
    import org.apache.struts.action.ActionServlet;
    import bussness.UserInfoBo;
    import entity.UserInfoForm;
    public final class LogonAction extends Action {
      
      public ActionForward execute(ActionMapping mapping,
             ActionForm form,
             HttpServletRequest request,
             HttpServletResponse response)
             throws IOException, ServletException {
        UserInfoForm userInfoForm = (UserInfoForm) form;           
        //从web层获得用户名和口令
        String username = userInfoForm.getUsername().trim();
        String password = userInfoForm.getPassword().trim();
        //声明错误集对象
        ActionErrors errors = new ActionErrors();
        //校验输入
        if(username.equals("")){
          ActionError error=new ActionError("error.missing.username");
          errors.add(ActionErrors.GLOBAL_ERROR,error);
        }
        if(password.equals("")){
          ActionError error=new ActionError("error.missing.password");
          errors.add(ActionErrors.GLOBAL_ERROR,error);
        }
        
        //调用业务逻辑
        if(errors.size()==0){
          String validated = "";
          try{
            UserInfoBo userInfoBo=new UserInfoBo();
            validated =userInfoBo.validatePwd(username,password);
            if(validated.equals("match")){
              //一切正常就保存用户信息并转向成功的页面      
              HttpSession session = request.getSession();
              session.setAttribute("userInfoForm", form);          
                  return mapping.findForward("success");
            } 
          }
          
          catch(Throwable e){
            //处理可能出现的错误
            e.printStackTrace();
            ActionError error=new ActionError(e.getMessage());
            errors.add(ActionErrors.GLOBAL_ERROR,error);
          }
        }  
        //如出错就转向输入页面,并显示相应的错误信息
        saveErrors(request, errors);    
        return new ActionForward(mapping.getInput());    
      } 
    }


    这个action类中有两个错误消息键要加到ApplicationResource.properties文件中,清单如下:

    #Application Resource for the LogonAction.java
    error.missing.username=<li><font color="red">missing username</font></li>
    error.missing.password=<li><font color="red">missing password</font></li>>


    第四步:在struts-config.xml文件中定义Views与 Controller的关系,也就是配置所谓的ActionMapping。它们在struts-config.xml中的位置是排在… 标签后,我们的登录程序的配置清单如下:

    <action-mappings>
        <action input="/logon.jsp" name="userInfoForm" path="/logonAction" scope="session" 
          type="action.LogonAction" validate="false">
          <forward name="success" path="/main.jsp" />      
        </action>
      </action-mappings>


    第五步:生成应用程序所需要的model组件,该组件是完成应用程序业务逻辑的地方,现在我的登录程序的业务逻辑很简单,就是判断用户是不是lhb并且其口令是不是awave如果是就返回一个表示匹配的字符串"match",否则,就抛出出错信息。其代码清单如下:

    package bussness;
    
    import entity.UserInfoForm;
    
    public class UserInfoBo {
    
      public UserInfoBo(){
        
      }        
    
      public String validatePwd(String username,String password){
              
        String validateResult=""; 
           
        if(username.equals("lhb")&&password.equals("awave")){
          validateResult="match";
        }
        else{
          
          throw new RuntimeException("error.noMatch");
        }          
        
        return validateResult;   
        
      }
    }


    将其放在bussness包中。

    我们同样要将其表示错误信息的键值设置在ApplicationResource.properties文件中,清单如下:

    #Application Resource for the UserInfoBo.java
    error.noMatch=<li><font color="red">no matched user</font></li>


    到此为止,我们已经完成了这个简单登录程序的所有组件。下面就可以享受我们的劳动成果了。

    第六步、编译运行应用程序。

    常规的做法是用Ant来装配和部署Struts应用程序,如果按这个套路,这篇文章就会显得十分冗长乏味,同时也没有太大的必要,因为,用一个IDE一般可以很方便地生成一个应用。因此,我们采用简便的方法,直接编译我们的.java文件。不过这里要注意一点的是:实践证明,要使得编译过程不出错,还必须将struts.jar文件放一份拷贝到/common/lib目录中,并在环境变量中设置CLASSPATH 其值是/common/lib/struts.jar;配置好后就可以分别编译entity、bussness及action目录下的.java文件了。编译完成后:打开/conf目录下的server.xml文件,在前加上如下语句为我们的应用程序建一个虚拟目录:

    <Context path="/mystruts" docBase="mystruts" debug="0"
                     reloadable="true">                 
                </Context>


    启动,tomcat。在浏览器中输入:http://localhost:8080/mystruts/logon.jsp
    如果前面的步骤没有纰漏的话,一个如图所示的登录画面就会出现在你的眼前。



    如果,不输入任何内容直接点击Submit按钮,就会返回到logon.jsp并显示missing username和missing password错误信息;如果输入其他内容,则会返回no matched user的错误;如果输入的用户名是lhb且口令是awave则会显示表示登录成功的欢迎页面。

    上面虽然是一个功能很简单的应用程序,但麻雀虽小,五脏俱全,*本涉及到了struts的主要组成部分。下面我们就来分析一下程序的特点和*本的工作原理。

    首先,我们在浏览器中输入.jsp文件时,后台将struts的自定义标签"翻译"成普通的html标签返回给浏览器,而一些提示信息如作为输入框label的username、password还有按钮上提示信息还有错误信息等都来自MessageResources即ApplicationResource.properties文件中对应的键值。当我们点击Submit按钮时,从web.xml的配置可以看出,请求将被ActionServlet截获。它通过表单中提供的action参数在struts-config.xml文件中查找对应的项目,如果有对应的ActionForm,它就用表单中数据填充ActionForm的对应属性,本例中的ActionForm为userInfoForm,相应的属性是username和password,这就是所谓的实例化ActionForm。然后,将控制交给对应的Action,本例中是LogonAction,它做的主要工作是对ActionForm中取出的username和password做了一下校验,这里只是简单检验它们是否为空(这些简单的格式化方面的校验应该放在客户端进行,而且struts也为我们提供了一个很好的模式,后面如果有可能会详细介绍)。如果不为空则调用判断用户及口令是否正确的业务逻辑模块UserInfoBo,同时,它会捕获可能出现的错误,然后根据业务逻辑返回的结果将程序导向不同的页面,本例中如果业务逻辑返回的结果是"match"则依据中的返回main.jsp页面给浏览器同时在session对象中保存了用户的登录信息;否则,返回输入页面并显示相应的出错信息,完成了上篇文章所说的它的四个主要职责。

    大家一定注意到了,在本例的业务逻辑模块UserInfoBo中,将用户与密码是写死在程序中的,在一个真实的应用程序中是不会这样做的,那些需要永久保存的信息如,username及口令等都会保存在数据库文件之类的永久介质中,下一篇文章我们将介绍在struts中如何访问数据库
  • (第三部分)

    一、JDBC的工作原理

    Struts在本质上是java程序,要在Struts应用程序中访问数据库,首先,必须搞清楚Java Database Connectivity API(JDBC)的工作原理。正如其名字揭示的,JDBC库提供了一个底层API,用来支持独立于任何特定SQL实现的*本SQL功能。提供数据库访问的*本功能。它是将各种数据库访问的公共概念抽取出来组成的类和接口。JDBC API包括两个包:java.sql(称之为JDBC内核API)和javax.sql(称之为JDBC标准扩展)。它们合在一起,包含了用Java开发数据库应用程序所需的类。这些类或接口主要有:
    Java.sql.DriverManager
    Java.sql.Driver
    Java.sql.Connection
    Java.sql.Statement
    Java.sql.PreparedStatement
    Java.sql.ResultSet等

    这使得从Java程序发送SQL语句到数据库变得比较容易,并且适合所有SQL方言。也就是说为一种数据库如Oracle写好了java应用程序后,没有必要再为MS SQL Server再重新写一遍。而是可以针对各种数据库系统都使用同一个java应用程序。这样表述大家可能有些难以接受,我们这里可以打一个比方:联合国开会时,联合国的成员国的与会者(相当我们这里的具体的数据库管理系统)往往都有自己的语言(方言)。大会发言人(相当于我们这里的java应用程序)不可能用各种语言来发言。你只需要使用一种语言(相当于我们这里的JDBC)来发言就行了。那么怎么保证各成员国的与会者都听懂发言呢,这就要依靠同声翻译(相当于我们这里的JDBC驱动程序)。实际上是驱动程序将java程序中的SQL语句翻译成具体的数据库能执行的语句,再交由相应的数据库管理系统去执行。因此,使用JDBC API访问数据库时,我们要针对不同的数据库采用不同的驱动程序,驱动程序实际上是适合特定的数据库JDBC接口的具体实现,它们一般具有如下三种功能:

  • 建立一个与数据源的连接
  • 发送SQL语句到数据源
  • 取回结果集

    那么,JDBC具体是如何工作的呢?

    Java.sql.DriverManager装载驱动程序,当Java.sql.DriverManager的getConnection()方法被调用时,DriverManager试图在已经注册的驱动程序中为数据库(也可以是表格化的数据源)的URL寻找一个合适的驱动程序,并将数据库的URL传到驱动程序的acceptsURL()方法中,驱动程序确认自己有连接到该URL的能力。生成的连接Connection表示与特定的数据库的会话。Statement(包括PreparedStatement和CallableStatement)对象作为在给定Connection上执行SQL语句的容器。执行完语句后生成ResultSet结果集对象,通过结果集的一系列getter就可以访问表中各列的数据。

    这里,是讲的JDBC的*本工作过程,实际应用中,往往会使用JDBC扩展对象如DataSource等,限于篇幅,就不在此详细讨论了。

    二、访问数据库所要做的*本配置

    我们以访问MS SQL Server2000数据库为例,介绍其*本的配置情况。首先,要到微软网站去下载JDBC的驱动程序,运行setup.exe将得到的三个文件:msbase.jar、mssqlserver.jar及msutil.jar放在/webapps/mystruts/WEB-INF/lib目录下。

    在struts-config.xml文件中配置数据源

    这里,有一点要引起大家的注意的,就是,struts-config.xml中配置的各个项目是有一定的顺序要求的,几个主要项目的顺序大致是这样的:

    data-sources
    form-beans
    action-mappings
    message-resources
    plug-in


    在配置时要遵守上述顺序

    <data-sources>
        <data-source key="A" type="org.apache.commons.dbcp.BasicDataSource">
          <set-property property="driverClassName"
              value="com.microsoft.jdbc.sqlserver.SQLServerDriver" />
          <set-property property="url"
              value="jdbc:microsoft:sqlserver://127.0.0.1:1433;DatabaseName=mystruts;SelectMethod=cursor" />
          <set-property property="username" value="sa" />
          <set-property property="password" value="yourpwd" />
          <set-property property="maxActive" value="10" />
          <set-property property="maxWait" value="5000" />
          <set-property property="defaultAutoCommit" value="false" />
          <set-property property="defaultReadOnly" value="false" />
        </data-source>
      </data-sources>


    我们来对这段配置代码做一个简单的说明:

    这句中,如果您的struts应用程序中只配置一个数据源则key="A"可以不要,而配置多个数据源时就要用这个键值区别,也就是说,可以为一个应用程序配置多个数据源让它访问多个数据库。

    <set-property property="url" 
            value="jdbc:microsoft:sqlserver://127.0.0.1:1433;DatabaseName=mystruts;
            SelectMethod=cursor" />


    这句中的sqlserver://127.0.0.1:1433;DatabaseName=mystruts;的数据库服务器名(本例是用代表本机的ip地址)和数据库名称要与您的具体情况相同。同时,还要注意访问数据库的用户名和口令也要合乎您的实际情况。

    表示最大的活动连接数,这也说明这些连接是池化(pooling)的。

    表示对数据库的增、删、改操作必须显式地提交。即必须使用connect.commit();这样的命令才能真正让数据库表中的记录作相应的改变。设置成这样方便用户组织自己的数据库事务。

    三、现在我们就来扩展前面我们讲的那个登录的例子,让它访问存储在数据库表中的用户名和口令信息,同时也让它给出的出错信息更明确一些。

    为此,我们先要做一些准备工作,如果您还没有安装MS SQL Server2000请先安装,并下载最新的补丁包。再建一个名为mystruts的数据库,并在该数据库中建一个名为userInfo的表,该表有两个字段既:username和password,它们的字段类型都为varchar(10),其中username为主键。在该表中输入一条记录,username和password的字段值分别为lhb和awave。到此准备工作就*本做好了。

    为了访问数据库,首先,要修改Action类,修改后的代码清单如下:

    package action;
    import java.io.IOException;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpSession;
    import javax.servlet.http.HttpServletResponse;
    import org.apache.struts.action.Action;
    import org.apache.struts.action.ActionError;
    import org.apache.struts.action.ActionErrors;
    import org.apache.struts.action.ActionForm;
    import org.apache.struts.action.ActionForward;
    import org.apache.struts.action.ActionMapping;
    import org.apache.struts.action.ActionServlet;
    import bussness.UserInfoBo;
    import entity.UserInfoForm;
    import javax.sql.DataSource;
    import java.sql.Connection;
    import java.sql.SQLException;
    
    public final class LogonAction extends Action {
    
      public ActionForward execute(ActionMapping mapping,
             ActionForm form,
             HttpServletRequest request,
             HttpServletResponse response)
             throws IOException, ServletException {
        UserInfoForm userInfoForm = (UserInfoForm) form;
        //从web层获得用户名和口令
        String username = userInfoForm.getUsername().trim();
        String password = userInfoForm.getPassword().trim();
        //声明错误集对象
        ActionErrors errors = new ActionErrors();
        //声明数据源和连接对象
        DataSource dataSource;
        Connection cnn=null;
    
        //校验输入
        if(username.equals("")){
          ActionError error=new ActionError("error.missing.username");
          errors.add(ActionErrors.GLOBAL_ERROR,error);
        }
        if(password.equals("")){
          ActionError error=new ActionError("error.missing.password");
          errors.add(ActionErrors.GLOBAL_ERROR,error);
        }
    
        //调用业务逻辑
        if(errors.size()==0){
          String validated = "";
          try{
            //取得数据库连接
            dataSource = getDataSource(request,"A");
            cnn = dataSource.getConnection();
    
            UserInfoBo userInfoBo=new UserInfoBo(cnn);
            validated =userInfoBo.validatePwd(username,password);
            if(validated.equals("match")){
              //一切正常就保存用户信息并转向成功的页面
              HttpSession session = request.getSession();
              session.setAttribute("userInfoForm", form);
                    return mapping.findForward("success");
            }
          }
    
          catch(Throwable e){
            //处理可能出现的错误
            e.printStackTrace();
            ActionError error=new ActionError(e.getMessage());
            errors.add(ActionErrors.GLOBAL_ERROR,error);
          }
        }
        //如出错就转向输入页面,并显示相应的错误信息
        saveErrors(request, errors);
        return new ActionForward(mapping.getInput());
      }
    }


    注意:dataSource = getDataSource(request,"A");这句中,如果配置中只有一个数据源,且没有key="A",则这句应写为dataSource = getDataSource(request);

    从清单上可以看出,主要就是增加了访问数据库的代码。同时,我们的业务对象的形式也发生了一个变化,原来没有参数,现在有一个代表数据库连接的参数cnn,因此我们也要对业务对象进行适当地修改。

    更改后的业务对象代码清单如下:

    package bussness;
    
    import entity.UserInfoForm;
    import java.sql.Connection;
    import java.sql.SQLException;
    import java.lang.Exception;
    import db.UserInfoDao;
    
    public class UserInfoBo {
      private Connection cnn=null;
    
      public UserInfoBo(Connection cnn){
        this.cnn=cnn;
      }
    
      public String validatePwd(String username,String password){
    
        String validateResult="";
       
        try{
          UserInfoDao userInfoDao = new UserInfoDao(cnn);
          validateResult=userInfoDao.validatePwd(username,password);
          if(validateResult.equals("error.logon.invalid")){
            //如果用户名与口令不匹配则报此错
            throw new RuntimeException("error.logon.invalid"); 
          }
          else if(validateResult.equals("error.removed.user")){
            //如果找不到用户则报此错,这样用户看到的出错信息会更详细
            throw new RuntimeException("error.removed.user"); 
          }
        }
        catch(Exception e){
          throw new RuntimeException(e.getMessage());
        }
        finally{
          try{
            if(cnn!=null){
              cnn.close();
            }
          }
          catch(SQLException sqle){
            sqle.printStackTrace();
            throw new RuntimeException("error.unexpected");
          }
        }
        return validateResult;
      }
    }


    这个业务对象的代码还是比较简单的,重点要讲的就是它在validatePwd方法中调用了一个名叫UserInfoDao的对象,它就是真正进行数据库操作的数据访问对象。其代码清单如下:

    package db;
    import entity.UserInfoForm;
    import java.sql.*;
    
    public class UserInfoDao {
      private Connection con;
    
      public UserInfoDao(Connection con) {
        this.con=con;
      }
      
      public String validatePwd(String username,String password){
        PreparedStatement ps=null;
        ResultSet rs=null;
        String validated="error.logon.invalid";
        UserInfoForm userInfoForm=null;
        String sql="select * from userInfo where username=?";
        try{
          if(con.isClosed()){
            throw new IllegalStateException("error.unexpected");
    
          }
          ps=con.prepareStatement(sql);
          ps.setString(1,username);
          rs=ps.executeQuery();
          if(rs.next()){
            if(!rs.getString("password").trim().equals(password)){
              return validated;//口令不正确返回口令不匹配信息
              
            }
            else{
    
              validated = "match";//口令正确返回口令匹配信息
              return validated;
            }
          }else{
            
            validated="error.removed.user";//没有找到该用户
            return validated;
            
          }
    
        }catch(SQLException e){
            e.printStackTrace();
            throw new RuntimeException("error.unexpected");
        }finally{
          try{
            if(ps!=null)
              ps.close();
            if(rs!=null)
              rs.close();
          }catch(SQLException e){
            e.printStackTrace();
            throw new RuntimeException("error.unexpected");
          }
        }
      }
    }


    下面,简单地分析一下数据访问对象的工作过程:

    要访问数据库,一般要经历的如下几个步骤:
  • 获得到数据库的连接
  • 创建SQL语句
  • 执行SQL语句
  • 管理结果集

    其中,得到数据库的连接本例中是在Action类中完成的,代码如下:
    dataSource = getDataSource(request,"A");
    cnn = dataSource.getConnection();

    Action在调用业务对象时将连接作为一个参数传给业务对象,再由业务对象传给数据库访问对象。

    要说明一点的是,要将struts-legacy.jar文件放在/webapps/mystruts/WEB-INF/lib目录下。

    我们要在/webapps/mystruts/WEB-INF/classes目录下再建一个名叫db的子目录,将数据访问类以UserInfoDao.java文件名保存在该子目录中。按照上篇文章介绍的方法,编译各个包中的.java文件。就可以启动Tomcat重新运行您的程序了。

    细心一点的读者可能都注意到了,到目前为止,我们程序中的各种消息都不是用中文表示的,在下一篇文章中,我们将讨论Struts的国际化编程即所谓的i18n编程,对我们在编程中经常遇到的乱码问题也一同作些分析。

    参考文献:
    《JSP Web 编程指南》---电子工业出版社 Jayson Falkner等著 司光亚 牛红等译
    《Java数据库编程宝典》John O'Donahue等著 甑广启 于耀等译
    《Struts in Action》Ted Husted Cedric Dumoulin George Franciscus David Winterfeldt著
    《Programming Jakarta Struts》Chuck Cavaness著
    《Mastering Jakarta Struts》James Goodwill著
    《Struts Kick Start》James Turner Kevin Bedell著
  • (第4部分)

    本篇我们来讨论一下struts的国际化编程问题,即所谓的i18n编程问题,这一篇我们讨论其*础部分。与这个问题紧密相关的是在各java论坛中被频繁提及的中文乱码问题,因为,英、美编程人员较少涉及到中文乱码问题,因此,这方面的英文资料也是非常奇缺的,同时也很少找到这方面比较完整的中文资料,本文也尝试对中文乱码问题做一些探讨。要解决上述问题,需要有一定的字符集方面的知识,下面,我们就先介绍字符集的有关情况:

    一、从ASCII到Unicode(UTF-8)

    电子计算机技术是从美国开始发展起来的,因为美国使用的文字为英文,美国规定的计算机信息交换用的字符编码集是人们熟知的扩展ASCII码,它以8bit字节为单位存储,ASCII的0-31及127为控制符,32-126为可见字符,包括所有的英文字母,阿拉伯数字和其他一些常见符号,128-255的ASCII码则没有定义。

    ASCII对英语国家是够用了,但对其他西欧国家却不够用,因此,人们将ASCII扩展到0-255的范围,形成了ISO-8859-1字符集。值得一提的是,因为考虑到程序中处理的信息大多是西文信息,因此有些WEB容器(如:Tomcat4.x)在处理所接收到的request字符串时,如果您没指定request的编码方式则系统就缺省地采用ISO-8859-1,明白这一点对理解后面的问题会有帮助。

    相比西方的拼音文字,东方的文字(如中文)的字符数要大得多,根本不可能在一个字节内将它们表示出来,因此,它们以两个字节为单位存储,以中文国标字符集GB2312为例,它的第一个字节为128-255。系统可以据此判断,若第一个字节大于127,则把与该字节后紧接着的一个字节结合起来共两个字节组成一个中文字符。这种由多个字节存储一个字符的字符集叫多字节字符集(MultiByte Charsets),对应的象ASCII这种用一个字节存储一个字符的字符集叫单字节字符集(SingleByte Charsets)。在GB2312字符集中,ASCII字符仍然用一个字节存储,换句话说该ASCII是该字符集的子集。

    GB2312只包含数千个常用汉字,往往不能满足实际需要,因此,人们对它进行扩展,这就有了我们现在广泛使用的GBK字符集,GBK是现阶段Windows及其他一些中文操作系统的缺省字符集。它包含2万多个字符,除了保持和GB2312兼容外,还包含繁体中文字,日文字符和朝鲜字符。值得注意的是GBK只是一个规范而不是国家标准,新的国家标准是GB18030-2000,它是比GBK包含字符更多的字符集。

    我国的台湾地区使用的文字是繁体字,其字符集是BIG5,而日本采用的字符集则是SJIS。它们的编码方法与GB2312类似,它们的ASCII字符部分是兼容的,但扩展部分的编码则是不兼容的,比如这几种字符集中都有"中文"这两个字符,但他们在各自的字符集中的编码并不相同,这就是用GB2312写成的网页用BIG5浏览时,看到的是乱糟糟的信息的原因。

    可见,在字符集的世界里,呈现给我们的是一个群雄割据的局面,各字符集拥有一块自己的地盘。这给各国和各地区交换信息带来了很大的困难,同时,也给国际化(本地化)编程造成了很大的麻烦。

    常言道:"分久必合",随着国际标准ISO10646定义的通用字符集(Universal Character Set即UCS)的出现,使这种局面发生了彻底的改观。UCS 是所有其他字符集标准的一个超集. 它保证与其他字符集是双向兼容的. 就是说, 如果你将任何文本字符串翻译到 UCS格式, 然后再翻译回原编码, 你不会丢失任何信息。UCS 包含了用于表达所有已知语言的字符。不仅包括拉丁语、希腊语、 斯拉夫语、希伯来语、阿拉伯语、亚美尼亚语和乔治亚语的描述、还包括中文、 日文和韩文这样的象形文字、 以及平假名、片假名、 孟加拉语、 旁遮普语果鲁穆奇字符(Gurmukhi)、 泰米尔语、印.埃纳德语(Kannada)、Malayalam、泰国语、 老挝语、 汉语拼音(Bopomofo)、Hangul、 Devangari、Gujarati、Oriya、Telugu 以及其他数也数不清的语。对于还没有加入的语言, 由于正在研究怎样在计算机中最好地编码它们, 因而最终它们都将被加入。

    ISO 10646 定义了一个 31 位的字符集。 然而, 在这巨大的编码空间中, 迄今为止只分配了前 65534 个码位 (0x0000 到 0xFFFD)。 这个 UCS 的 16位子集称为 *本多语言面 (Basic Multilingual Plane, BMP)。 将被编码在 16 位 BMP 以外的字符都属于非常特殊的字符(比如象形文字), 且只有专家在历史和科学领域里才会用到它们。

    UCS 不仅给每个字符分配一个代码, 而且赋予了一个正式的名字。 表示一个 UCS 值的十六进制数, 通常在前面加上 "U+", 就象 U+0041 代表字符"拉丁大写字母A"。 UCS 字符 U+0000 到 U+007F 与 US-ASCII(ISO 646) 是一致的, U+0000 到 U+00FF 与 ISO 8859-1(Latin-1) 也是一致的。这里要注意的是它是以16bit为单位存储,即便对字母"A"也是用16bit,这是与前面介绍的所有字符集不同的地方。

    历史上,在国际标准化组织研究ISO10646标准的同时,另一个由多语言软件制造商组成的协会也在从事创立单一字符集的工作,这就是现在人们熟知的Unicode。幸运的是,1991年前后ISO10646和Unicode的参与者都认识到,世界上不需要两个不同的单一字符集。他们合并双方的工作成果,并为创立单一编码表而协同工作。两个项目仍都存在并独立地公布各自的标准,都同意保持ISO10646和Unicode的码表兼容,并紧密地共同调整任何未来的扩展。这与当年在PC机上的操作系统MS-dos与PC-dos的情形有些相象。后面,我们将视ISO10646和Unicode为同一个东西。

    有了Unicode,字符集问题接近了完美的解决,但不要高兴得过早。由于历史的原因:一些操作系统如:Unix、Linux等都是*于ASCII设计的。此外,还有一些数据库管理系统软件如:Oracle等也是围绕ASCII来设计的(从其8i的白皮书上介绍的设置系统字符集和字段的字符集中可以间接地看到这一点)。在这些系统中直接用Unicode会导致严重的问题。用这些编码的字符串会包含一些特殊的字符, 比如 '\0' 或 '/', 它们在 文件名和其他 C 库函数参数里都有特别的含义。 另外, 大多数使用 ASCII 文件的 UNIX 下的工具, 如果不进行重大修改是无法读取 16 位的字符的。 *于这些原因, 在文件名, 文本文件, 环境变量等地方,直接使用Unicode是不合适的。

    在 ISO 10646-1 Annex R 和 RFC 2279 里定义的 UTF-8 (Unicode Transformation Form 8-bit form)编码没有这些问题。

    UTF-8 有以下一些特性:

    UCS 字符 U+0000 到 U+007F (ASCII) 被编码为字节 0x00 到 0x7F (ASCII 兼容)。 这意味着只包含 7 位 ASCII 字符的文件在 ASCII 和 UTF-8 两种编码方式下是一样的。

    所有 >U+007F 的 UCS 字符被编码为一个多个字节的串, 每个字节都有标记位集。 因此,ASCII 字节 (0x00-0x7F) 不可能作为任何其他字符的一部分。

    表示非 ASCII 字符的多字节串的第一个字节总是在 0xC0 到 0xFD 的范围里, 并指出这个字符包含多少个字节。 多字节串的其余字节都在 0x80 到 0xBF 范围里。 这使得重新同步非常容易, 并使编码无国界,且很少受丢失字节的影响。

    UTF-8 编码字符理论上可以最多到 6 个字节长, 然而 16 位 BMP 字符最多只用到 3 字节长。

    字节 0xFE 和 0xFF 在 UTF-8 编码中从未用到。

    通过,UTF-8这种形式,Unicode终于可以广泛的在各种情况下使用了。在讨论struts的国际化编程之前,我们先来看看我们以前在jsp编程中是怎样处理中文问题以及我们经常遇到的:

    二、中文字符乱码的原因及解决办法

    java的内核是Unicode的,也就是说,在程序处理字符时是用Unicode来表示字符的,但是文件和流的保存方式是使用字节流的。在java的*本数据类型中,char是Unicode的,而byte是字节,因此,在不同的环节java要对字节流和char进行转换。这种转换发生时如果字符集的编码选择不当,就会出现乱码问题。

    我们常见的乱码大致有如下几种情形:
    1、汉字变成了问号"?"
    2、有的汉字显示正确,有的则显示错误
    3、显示乱码(有些是汉字但并不是你预期的)
    4、读写数据库出现乱码

    下面我们逐一对它们出现的原因做一些解释:

    首先,我们讨论汉字变成问号的问题。

    Java中byte与char相互转换的方法在sun.io包中。其中,byte到char的常用转换方法是:
    public static ByteToCharConverter getConverter(String encoding);

    为了便于大家理解,我们先来做一个小实验:比如,汉字"你"的GBK编码为0xc4e3,其Unicode编码是\u4f60。我们的实验是这样的,先有一个页面比如名为a_gbk.jsp输入汉字"你",提交给页面b_gbk.jsp。在b_gbk.jsp文件中以某种编码方式得到"你"的字节数组,再将该数组以某种编码方式转换成char,如果得到的char值是0x4f60则转换是正确的。

    a_gbk.jsp的代码如下:

    <%@ page contentType="text/html; charset=GBK" language="java" import="java.sql.*" errorPage="" %>
    <table width="611" border="0" align="center" cellpadding="0" cellspacing="0">
      <tr>
        <td>&nbsp;</td>
        <td class="bigword">&nbsp;</td>
        <td>&nbsp;</td>
      </tr>
      <tr>
        <td width="100">&nbsp;</td>
        <td class="bigword">Input</td>
        <td width="100">&nbsp;</td>
      </tr>
      <tr>
        <td>&nbsp;</td>
        <td class="bigword">&nbsp;</td>
        <td>&nbsp;</td>
      </tr>
    </table>
    <table width="611" border="0" align="center" cellpadding="0" cellspacing="0">
      <tr>
        <td><form method="post" action="b_gbk.jsp">
            <table width="611" border="0" cellpadding="0" cellspacing="0">
              <tr>
                <td width="100" align="right"></td>
                <td><input name="ClsID" type="text" class="word" id="ClsID" maxlength="2" >
                  *</td>
              </tr>
              <tr>
                <td width="100" align="right">&nbsp;</td>
                <td><input name="btn" type="submit" value="OK">
                 </td>
              </tr>
            </table>
          </form></td>
      </tr>
    </table>



    b_gbk.jsp的代码如下:

    <%@ page contentType="text/html; charset=GBK" import="sun.io.*,java.util.*" %>
    <%
    String a=(String)request.getParameter("ClsID");
    byte b[]=a.getBytes("ISO8859-1");
    for(int j=0;j<b.length;j++){
      out.println(Integer.toHexString(b[j])+"<br>");
    }
    ByteToCharConverter convertor=ByteToCharConverter.getConverter("GBK");
    char[] c=convertor.convertAll(b);
    out.println("b length:"+b.length+"<br>");
    out.println("c length:"+c.length+"<br>");
    for(int i=0;i<c.length;i++){
           out.println(Integer.toHexString(c[i])+"<br>");
    }
    String a1=new String(a.getBytes("ISO8859-1"),"GBK");
    %>
    <%="a是:"+a%><br>
    <%="a1是:"+a1%>



    在浏览器中打开a_gbk.jsp并输入一个"你"字,点击OK按钮提交表单,则会出现如图1所示的结果:



    图1

    从图1可以看出,在b_gbk.jsp中这样将byte转换为char是正确的,即得到的char是\u4f60。这里要注意的是:byte b[]=a.getBytes("ISO8859-1");中的编码是ISO8859-1,这就是我们前面提到的有些web容器在您没有指定request的字符集时它就采用缺省的ISO8859-1。

    从图1中我们还看到表达式<%="a是:"+a%>中的a并没有正确地显示"你"而是变成"??"这是什么原因呢?这里的a是作为一个String被显示的,我们来看看我们常用的String构造函数:

    String(byte[] bytes,String encoding);

    在国标平台上,该函数会认为bytes是按GBK编码的,如果后一个参数省略,它也会认为是encoding是GBK。

    对前一个参数就相当于将b_gbk.jsp文件的这句byte b[]=a.getBytes("ISO8859-1");中的ISO8859-1改为GBK,这样显然在GBK字符集中找不到相应的目的编码,它给出的结果是0x3f、0x3f。因此,就会显示为"??",这也就是造成乱码的第一种现象的原因。我们的例子是演示的从byte到char的转换过程,相反的过程也会造成同样的问题,限于篇幅,就不在此讨论了,大家自己可以做类似的实验来验证。

    解决该问题的方法就是象例子中a1那样,在获取byte数组时,指定编码为ISO8859-1。

    接下来,我们讨论有些汉字能正常显示,有些不能正常显示的问题。

    如果我们将String a1=new String(a.getBytes("ISO8859-1"),"GBK");中的GBK改为GB2312则象***的"*"字就不能正常显示,这是因为该字是GBK中的字符而在GB2312中不存在。

    解决上述两种问题的方法就是象a1那样构造String,也就是人们常说的同时也是常用的转码的方法。采用这种方法会在程序中到处出现这种语句,特别是在Struts中,Struts有一个回写表单的功能,在回写时也要做这种转换,这样的语句差不多要多一倍。因此,这是个比较笨拙的方法,有没有简捷一些的方法呢?其实是有的,只要在取得request的字符串前加上request.setCharacterEncoding("GBK");这句,指定request的字符集。则<%="a是:"+a%>中的a就能正常显示,a1反而不能正常显示。此时要将byte b[]=a.getBytes("ISO8859-1");中的ISO8859-1变成GBK,从byte到char的转换才是正确的,这就是此时a能正常显示而a1反而不能正常显示的原因。如果此时要a1正常显示则必须将String a1=new String(a.getBytes("ISO8859-1"),"GBK");中的ISO8859-1改为GBK。

    很显然,使用request.setCharacterEncoding("GBK");只能解决GBK字符问题,要解决i18n问题则要使用UTF-8来取代GBK。我们接着做上述实验,将a_gbk.jsp和b_gbk.jsp分别另存为a.jsp和b.jsp将文件中的GBK改为UTF-8,更改后的代码分别如下:

    a.jsp代码:

    <%@ page contentType="text/html; charset=UTF-8" language="java" import="java.sql.*" errorPage="" %>
    
    <table width="611" border="0" align="center" cellpadding="0" cellspacing="0">
      <tr>
        <td>&nbsp;</td>
        <td class="bigword">&nbsp;</td>
        <td>&nbsp;</td>
      </tr>
      <tr>
        <td width="100">&nbsp;</td>
        <td class="bigword">Input</td>
        <td width="100">&nbsp;</td>
      </tr>
      <tr>
        <td>&nbsp;</td>
        <td class="bigword">&nbsp;</td>
        <td>&nbsp;</td>
      </tr>
    </table>
    <table width="611" border="0" align="center" cellpadding="0" cellspacing="0">
      <tr>
        <td><form method="post" action="b.jsp">
            <table width="611" border="0" cellpadding="0" cellspacing="0">
              <tr>
                <td width="100" align="right"></td>
                <td><input name="ClsID" type="text" class="word" id="ClsID" maxlength="2" >
                  *</td>
              </tr>
              <tr>
                <td width="100" align="right">&nbsp;</td>
                <td><input name="btn" type="submit" value="OK">
                 </td>
              </tr>
            </table>
          </form></td>
      </tr>
    </table>
    b.jsp代码:
    <ccid_nobr>
    <table width="400" border="1" cellspacing="0" cellpadding="2" 
     bordercolorlight = "black" bordercolordark = "#FFFFFF" align="center">
    <tr>
        <td bgcolor="e6e6e6" class="code" style="font-size:9pt">
        <pre><ccid_code>  <%@ page contentType="text/html; charset=UTF-8" import="sun.io.*,java.util.*" %>
    
    <%
    request.setCharacterEncoding("UTF-8");
    String a=(String)request.getParameter("ClsID");
    byte b[]=a.getBytes("UTF-8");
    for(int j=0;j<b.length;j++){
      out.println(Integer.toHexString(b[j])+"<br>");
    }
    ByteToCharConverter convertor=ByteToCharConverter.getConverter("UTF-8");
    char[] c=convertor.convertAll(b);
    out.println("b length:"+b.length+"<br>");
    out.println("c length:"+c.length+"<br>");
    for(int i=0;i<c.length;i++){
      out.println(Integer.toHexString(c[i])+"<br>");
    }
    String a1=new String(a.getBytes("UTF-8"),"UTF-8");
    %>
    <%="a是:"+a%><br>
    <%="a1是:"+a1%>



    再在a.jsp中输入"你"字,你会发现显示结果中,一个汉字是用三个byte表示的,它们的值分别是0xe4、0xbd、0xa0,也就是说用UTF-8来表示汉字,每个汉字要比GBK多占用一个byte,这也是使用UTF-8要多付出的一点代价吧。

    现在,我们讨论一下第三个问题,即显示乱码,有些莫名其妙的汉字并不是你预期的结果。

    在上例中将String a1=new String(a.getBytes("UTF-8"),"UTF-8");改为String a1=new String(a.getBytes("UTF-8"),"GBK");再输入"你"字,则a1会显示成"浣?",您只要看一看"浣"的UTF-8码和GBK码就会知道其中的奥秘了。

    下面,我们讨论一下最后一个问题,就是读写数据库时出现乱码。

    现在一些常用的数据库都支持数据库encoding,也就是说在创建数据库时可以指定它自己的字符集设置,数据库数据以指定的编码形式存储。当应用程序访问数据库时,在入口和出口处都会有encoding转换。如果,在应用程序中字符本来已变成了乱码,当然也就无法正确地转换为数据库的字符集了。数据库的encoding可根据需要来设置,比如要支持简、繁体中文、日、韩、英语选GBK,如果还要支持其他语言最好选UTF-8。

    本篇文章对字符集及中文乱码问题做了一下探讨,为实现国际化编程的实践打下一个*础。下一篇文章,我们将介绍struts中实现国际化编程的具体步骤,并将我们前面介绍的登录例子进行国际化。

    参考文献:

    UTF-8 and Unicode FAQ

    《JSP动态网站技术入门与提高》太阳工作室 孙晓龙 赵莉编著

    第5部分

    一个支持i18n的应用程序应该有如下一些特征:
    1增加支持的语言时要求不更改程序代码
    2字符元素、消息、和图象保存在原代码之外
    3依赖于不同文化的数据如:日期时间、小数、及现金符号等数据对用户的语言和地理位置应该有正确的格式
    4应用程序能迅速地适应新语言和/或新地区

    Struts主要采用两个i18n组件来实现国际化编程:

    第一个组件是一个被应用程序控制器管理的消息类,它引用包含地区相关信息串的资源包。第二个组件是一个JSP定制标签,,它用于在View层呈现被控制器管理的实际的字符串。在我们前面的登录例子中这两方面的内容都出现过。

    用Struts实现国际化编程的标准做法是:生成一个java属性文件集。每个文件包含您的应用程序要显示的所有消息的键/值对。

    这些文件的命名要遵守如下规则,代表英文消息的文件可作为缺省的文件,它的名称是ApplicationResources.properties;其他语种的文件在文件名中都要带上相应的地区和语言编码串,如代表中文的文件名应为ApplicationResources_zh_CN.properties。并且其他语种的文件与ApplicationResources.properties文件要放在同一目录中。

    ApplicationResources.properties文件的键/值都是英文的,而其他语种文件的键是英文的,值则是对应的语言。如在我们前面的登录例子中的键/值对:logon.jsp.prompt.username=Username:在中文文件中就是:logon.jsp.prompt.username=用户名:当然,在实际应用时要把中文转换为AscII码。

    有了上一篇文章和以上介绍的一些*础知识后。我们就可以将我们的登录程序进行国际化编程了。

    首先,我们所有jsp页面文件的字符集都设置为UTF-8。即在页面文件的开始写如下指令行:

    <%@ page contentType="text/html; charset=UTF-8" %>,在我们的登录例子中已经这样做了,这里不需要再改动。

    其次,将所有的request的字符集也设置为UTF-8。虽然,我们可以在每个文件中加入这样的句子:request.setCharacterEncoding("UTF-8");来解决,但这样显得很麻烦。一种更简单的解决方法是使用filter。具体步骤如下:

    在mystruts\WEB-INF\classes目录下再新建一个名为filters的目录,新建一个名为:SetCharacterEncodingFilter的类,并保存在该目录下。其实,这个类并不要您亲自来写,可以借用tomcat中的例子。现将该例子的程序节选如下:

    package filters;
    import java.io.IOException;
    import javax.servlet.Filter;
    import javax.servlet.FilterChain;
    import javax.servlet.FilterConfig;
    import javax.servlet.ServletException;
    import javax.servlet.ServletRequest;
    import javax.servlet.ServletResponse;
    import javax.servlet.UnavailableException;
    
    /**
     * <p>Example filter that sets the character encoding to be used in parsing the
     * incoming request, either unconditionally or only if the client did not
     * specify a character encoding.  Configuration of this filter is based on
     * the following initialization parameters:</p>
     * <ul>
     * <li><strong>encoding</strong> - The character encoding to be configured
     *     for this request, either conditionally or unconditionally based on
     *     the <code>ignore</code> initialization parameter.  This parameter
     *     is required, so there is no default.</li>
     * <li><strong>ignore</strong> - If set to "true", any character encoding
     *     specified by the client is ignored, and the value returned by the
     *     <code>selectEncoding()</code> method is set.  If set to "false,
     *     <code>selectEncoding()</code> is called <strong>only</strong> if the
     *     client has not already specified an encoding.  By default, this
     *     parameter is set to "true".</li>
     * </ul>
     *
     * <p>Although this filter can be used unchanged, it is also easy to
     * subclass it and make the <code>selectEncoding()</code> method more
     * intelligent about what encoding to choose, based on characteristics of
     * the incoming request (such as the values of the <code>Accept-Language</code>
     * and <code>User-Agent</code> headers, or a value stashed in the current
     * user's session.</p>
     *
     * @author Craig McClanahan
     * @version $Revision: 1.2 $ $Date: 2001/10/17 22:53:19 $
     */
    
    public class SetCharacterEncodingFilter implements Filter {
    
    
        // ----------------------------------------------------- Instance Variables
    
    
        /**
         * The default character encoding to set for requests that pass through
         * this filter.
         */
        protected String encoding = null;
    
    
        /**
         * The filter configuration object we are associated with.  If this value
         * is null, this filter instance is not currently configured.
         */
        protected FilterConfig filterConfig = null;
    
    
        /**
         * Should a character encoding specified by the client be ignored?
         */
        protected boolean ignore = true;
    
    
        // --------------------------------------------------------- Public Methods
    
    
        /**
         * Take this filter out of service.
         */
        public void destroy() {
    
            this.encoding = null;
            this.filterConfig = null;
    
        }
    
    
        /**
         * Select and set (if specified) the character encoding to be used to
         * interpret request parameters for this request.
         *
         * @param request The servlet request we are processing
         * @param result The servlet response we are creating
         * @param chain The filter chain we are processing
         *
         * @exception IOException if an input/output error occurs
         * @exception ServletException if a servlet error occurs
         */
        public void doFilter(ServletRequest request, ServletResponse response,
                             FilterChain chain)
            throws IOException, ServletException {
    
            // Conditionally select and set the character encoding to be used
            if (ignore || (request.getCharacterEncoding() == null)) {
                String encoding = selectEncoding(request);
                if (encoding != null)
                    request.setCharacterEncoding(encoding);
            }
    
            // Pass control on to the next filter
            chain.doFilter(request, response);
    
        }
    
    
        /**
         * Place this filter into service.
         *
         * @param filterConfig The filter configuration object
         */
        public void init(FilterConfig filterConfig) throws ServletException {
    
            this.filterConfig = filterConfig;
            this.encoding = filterConfig.getInitParameter("encoding");
            String value = filterConfig.getInitParameter("ignore");
            if (value == null)
                this.ignore = true;
            else if (value.equalsIgnoreCase("true"))
                this.ignore = true;
            else if (value.equalsIgnoreCase("yes"))
                this.ignore = true;
            else
                this.ignore = false;
    
        }
    
    
        // ------------------------------------------------------ Protected Methods
    
    
        /**
         * Select an appropriate character encoding to be used, based on the
         * characteristics of the current request and/or filter initialization
         * parameters.  If no character encoding should be set, return
         * <code>null</code>.
         * <p>
         * The default implementation unconditionally returns the value configured
         * by the <strong>encoding</strong> initialization parameter for this
         * filter.
         *
         * @param request The servlet request we are processing
         */
        protected String selectEncoding(ServletRequest request) {
    
            return (this.encoding);
    
        }
    
    }



    其中,request.setCharacterEncoding(encoding);是一个关键句子。

    为了让该类工作,我们还要在web.xml文件中对它进行配置,配置代码如下:

    <filter>
        <filter-name>Set Character Encoding</filter-name>
        <filter-class>filters.SetCharacterEncodingFilter</filter-class>
        <init-param>
          <param-name>encoding</param-name>
          <param-value>UTF-8</param-value>
        </init-param>
      </filter>
      <filter-mapping>
        <filter-name>Set Character Encoding</filter-name>
        <url-pattern>/*</url-pattern>
      </filter-mapping>



    最后,就是准备资源包文件,我们以创建一个中文文件为例:

    将ApplicationResources.properties文件打开,另存为ApplicationResources_zh.properties,这只是一个过渡性质的文件。将文件中键/值对的值都用中文表示。更改完后的代码如下:

    #Application Resource for the logon.jsp
    logon.jsp.title=登录页
    logon.jsp.page.heading=欢迎 世界!
    logon.jsp.prompt.username=用户名:
    logon.jsp.prompt.password=口令:
    logon.jsp.prompt.submit=提交
    logon.jsp.prompt.reset=复位
    
    #Application Resource for the main.jsp
    main.jsp.title=主页
    main.jsp.welcome=欢迎:
    
    #Application Resource for the LogonAction.java
    error.missing.username=<li><font color="red">没有输入用户名</font></li>
    error.missing.password=<li><font color="red">没有输入口令</font></li>
    
    #Application Resource for the UserInfoBo.java
    error.noMatch=<li><font color="red">没有匹配的用户</font></li>
    
    #Application Resource for the UserInfoBo.java
    error.logon.invalid=<li><font color="red">用户名/口令是无效的</font></li>
    error.removed.user=<li><font color="red">找不到该用户</font></li>
    error.unexpected=<li><font color="red">不可预期的错误</font></li>



    使用native2ascii工具将上面文件中的中文字符转换为ascii码,并生成一个最终使用的资源文件ApplicationResources_zh_CN.properties。

    具体做法是打开一个dos窗口,到mystruts\WEB-INF\classes目录下,运行如下语句:

    native2ascii -encoding GBK ApplicationResources_zh.properties ApplicationResources_zh_CN.properties

    生成的文件ApplicationResources_zh_CN.properties的内容如下:

    #Application Resource for the logon.jsp
    logon.jsp.title=\u767b\u5f55\u9875
    logon.jsp.page.heading=\u6b22\u8fce \u4e16\u754c!
    logon.jsp.prompt.username=\u7528\u6237\u540d:
    logon.jsp.prompt.password=\u53e3\u4ee4:
    logon.jsp.prompt.submit=\u63d0\u4ea4
    logon.jsp.prompt.reset=\u590d\u4f4d
    
    #Application Resource for the main.jsp
    main.jsp.title=\u4e3b\u9875
    main.jsp.welcome=\u6b22\u8fce:
    
    #Application Resource for the LogonAction.java
    error.missing.username=<li><font color="red">\u6ca1\u6709\u8f93\u5165\u7528\u6237\u540d</font></li>
    error.missing.password=<li><font color="red">\u6ca1\u6709\u8f93\u5165\u53e3\u4ee4</font></li>
    
    #Application Resource for the UserInfoBo.java
    error.noMatch=<li><font color="red">\u6ca1\u6709\u5339\u914d\u7684\u7528\u6237</font></li>
    
    #Application Resource for the UserInfoBo.java
    error.logon.invalid=<li><font color="red">\u7528\u6237\u540d/\u53e3\u4ee4\u662f\u65e0\u6548\u7684</font></li>
    error.removed.user=<li><font color="red">\u627e\u4e0d\u5230\u8be5\u7528\u6237</font></li>
    error.unexpected=<li><font color="red">\u4e0d\u53ef\u9884\u671f\u7684\u9519\u8bef</font></li>



    从这里可以看出,所有的中文字都转换成了对应的Unicode码。

    现在,再运行登录例子程序,您会发现它已经是显示的中文了。在浏览器的"工具"--"Internet选项"的"语言首选项"对话框中,去掉"中文(中国)"加上英文,再试登录程序,此时,又会显示英文。这就是说不同国家(地区)的客户都可以看到自己语言的内容,这就实现了国际化编程的*本要求。如果还要显示其他语言,可采用类似处理中文的方法进行,这里就不细讲了。

    本文中的例子程序所采用的数据库仍然是MS SQLServer2000,数据库字符集为gbk。实验表明,对简、繁体中文,英文及日文字符都能支持。

    参考文献:
    《Programming Jakarta Struts》Chuck Cavaness著
    《Mastering Jakarta Struts》James Goodwill著

    第6部分

    本文我们来讨论一下Struts中的输入校验问题。我们知道,信息系统有垃圾进垃圾出的特点,为了避免垃圾数据的输入,对输入进行校验是任何信息系统都要面对的问题。在传统的编程实践中,我们往往在需要进行校验的地方分别对它们进行校验,而实际上需要校验的东西大多都很类似,如必需的字段、日期、范围等等。因此,应用程序中往往到处充斥着这样一些显得冗余的代码。而与此形成鲜明对照的是Struts采用Validator框架(Validator框架现在是Jakarta Commons项目的一部分)来解决校验问题,它将校验规则代码集中到外部的且对具体的应用程序中立的.xml文件中,这样,就将那些到处出现的校验逻辑从应用程序中分离出来,任何一个Struts应用都可以使用这个文件,同时还为校验规则的扩展提供了便利。更难能可贵的是由于Validator框架将校验中要用到的一些消息等信息与资源绑定有机结合在一起,使得校验部分的国际化编程变得十分的便捷和自然。

        Validator框架大致有如下几个主要组件:

        Validators:是Validator框架调用的一个Java类,它处理那些*本的通用的校验,包括required、mask(匹配正则表达式)、最小长度、最大长度、范围、日期等

        .xml配置文件:主要包括两个配置文件,一个是validator-rules.xml,另一个是validation.xml。前者的内容主要包含一些校验规则,后者则包含需要校验的一些form及其组件的集合。

        资源绑定:提供(本地化)标签和消息,缺省地共享struts的资源绑定。即校验所用到的一些标签与消息都写在ApplicationResources.properity文件中。

        Jsp tag:为给定的form或者action path生成JavaScript validations。

        ValidatorForm:它是ActionForm的一个子类。

        为了对Validator框架有一个比较直观的认识,我们还是以前面的登陆例子的输入来示范一下Validator框架的使用过程:

        首先,找一个validator-rules.xml文件放在mystruts\WEB-INF目录下,下面是该文件中涉及到的required验证部分代码的清单:

    <validator name="required"
    <!--①-->
                classname="org.apache.struts.validator.FieldChecks"
                   method="validateRequired"
             methodParams="java.lang.Object,
                           org.apache.commons.validator.ValidatorAction,
                           org.apache.commons.validator.Field,
                           org.apache.struts.action.ActionErrors,
    javax.servlet.http.HttpServletRequest"
    <!--②-->
                      msg="errors.required">
    <!--③-->
             <javascript><![CDATA[
                function validateRequired(form) {
                    var isValid = true;
                    var focusField = null;
                    var i = 0;
                    var fields = new Array();
                    oRequired = new required();
                    for (x in oRequired) {
                          var field = form[oRequired[x][0]];
                          
                        if (field.type == 'text' ||
                            field.type == 'textarea' ||
                            field.type == 'file' ||
                            field.type == 'select-one' ||
                            field.type == 'radio' ||
                            field.type == 'password') {
                            
                            var value = '';
                            // get field's value
                            if (field.type == "select-one") {
                            var si = field.selectedIndex;
                            if (si >= 0) {
                            value = field.options[si].value;
                                              }
                                        } else {
                                              value = field.value;
                                        }
                            
                            if (trim(value).length == 0) {
                            
                                  if (i == 0) {
                                      focusField = field;
                                  }
                                  fields[i++] = oRequired[x][1];
                                  isValid = false;
                            }
                        }
                    }
                    if (fields.length > 0) {
                       focusField.focus();
                       alert(fields.join('\n'));
                    }
                    return isValid;
                }
                
                // Trim whitespace from left and right sides of s.
                function trim(s) {
                    return s.replace( /^\s*/, "" ).replace( /\s*$/, "" );
                }
                
                ]]>
             </javascript>
    
    </validator>



        ① 节的代码是引用一个服务器边的验证器,其对应的代码清单如下:

    public static boolean validateRequired(Object bean,
                                               ValidatorAction va, Field field,
                                               ActionErrors errors,
                                               HttpServletRequest request) {
    
            String value = null;
            if (isString(bean)) {
                value = (String) bean;
            } else {
                value = ValidatorUtil.getValueAsString(bean, field.getProperty());
            }
            
            if (GenericValidator.isBlankOrNull(value)) {
                errors.add(field.getKey(), Resources.getActionError(request, va, field));
                return false;
            } else {
                return true;
            }
    }



        ② 节是验证失败后的出错信息,要将对应这些键值的信息写入到ApplicationResources.properity文件中,常见的错误信息如下:

    # Standard error messages for validator framework checks
    errors.required={0} is required.
    errors.minlength={0} can not be less than {1} characters.
    errors.maxlength={0} can not be greater than {1} characters.
    errors.invalid={0} is invalid.
    errors.byte={0} must be a byte.
    errors.short={0} must be a short.
    errors.integer={0} must be an integer.
    errors.long={0} must be a long.
    errors.float={0} must be a float.
    errors.double={0} must be a double.
    errors.date={0} is not a date.
    errors.range={0} is not in the range {1} through {2}.
    errors.creditcard={0} is an invalid credit card number.
    errors.email={0} is an invalid e-mail address.



        ③ 节的代码用于客户边的JavaScript验证

    其次,在validation.xml文件中配置要验证的form极其相应的字段,下面是该文件中的代码:

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE form-validation PUBLIC "-//Apache Software Foundation
    //DTD Commons Validator Rules Configuration 1.0//EN" 
    "http://jakarta.apache.org/commons/dtds/validator_1_0.dtd">
    <form-validation>
    <formset>
    <form name="userInfoForm">
    <field property="username"
    depends="required,mask,minlength,maxlength">
    <arg0 key="logon.jsp.prompt.username" resource="true"/>
    <arg1 name="minlength" key="${var:minlength}" resource="false"/>
    <arg1 name="maxlength" key="${var:maxlength}" resource="false"/>
    <var>
    <var-name>mask</var-name>
    <var-value>^\w</var-value>
    </var>
    <var>
    <var-name>minlength</var-name>
    <var-value>2</var-value>
    </var>
    <var>
    <var-name>maxlength</var-name>
    <var-value>16</var-value>
    </var>
    </field>
    <field property="password"
    depends="required,minlength,maxlength">
    <arg0 key="logon.jsp.prompt.password" resource="true"/>
    <arg1 name="minlength" key="${var:minlength}" resource="false"/>
    <arg1 name="maxlength" key="${var:maxlength}" resource="false"/>
    <var>
    <var-name>minlength</var-name>
    <var-value>2</var-value>
    </var>
    <var>
    <var-name>maxlength</var-name>
    <var-value>16</var-value>
    </var>
    </field>
    </form>
    </formset>
    </form-validation>



        这里要注意的是:该文中的中的键值都是取自资源绑定中的。前面还讲到了出错信息也是写入ApplicationResources.properity文件中,因此,这就为国际化提供了一个很好的*础。

        再次,为了使服务器边的验证能够进行,将用到的formBean从ActionForm的子类改为ValidatorForm的子类,即:
        将public class UserInfoForm extends ActionForm改为:public class UserInfoForm extends ValidatorForm

        到此,进行服务器边的验证工作已经一切准备得差不多了,此时,只要完成最后步骤就可以实验服务器边的验证了。但大多数情况下,人们总希望把这些*本的简单验证放在客户边进行。

        为了能进行客户边的验证,我们还要对logon.jsp文件做适当的修改。

        将

    <html:form action="/logonAction.do" focus="username">

    改为
        

    <html:form action="/logonAction.do" focus="username" onsubmit="return validateUserInfoForm(this)">



        在标签后加上:
        

    <html:javascript dynamicJavascript="true" staticJavascript="true" formName="userInfoForm"/>



        最后,对struts的配置文件struts-config.xml作适当的修改:
        1、将

    <action input="/logon.jsp" name="userInfoForm"
     path="/logonAction" scope="session" type="action.LogonAction" validate="false" >

    改为

    <action input="/logon.jsp" name="userInfoForm" 
    path="/logonAction" scope="session" type="action.LogonAction" validate="true" >

    其作用是要求进行校验

        2、将下列代码放在struts-config.xml文件中的标签前。其作用是将用于校验的各个组件结合在一起。

    <plug-in className="org.apache.struts.validator.ValidatorPlugIn">
        <set-property property="pathnames"
          value="/WEB-INF/validator-rules.xml,/WEB-INF/validation.xml" />
    </plug-in>



        到此为止,我们的一切工作准备就绪,您可以享受自己的劳动成果了,试着输入各种组合的用户名和口令,看看它们的验证效果。仔细体会你会发现,服务器边的验证要更全面一些,比如对password的字符长度的验证。

        参考文献:
    《Struts in Action》Ted Husted Cedric Dumoulin George Franciscus David Winterfeldt著
    《Programming Jakarta Struts》Chuck Cavaness著

    第7部分

    上一篇文章中介绍校验时提到客户边的校验用到了JavaScript,实际上用Struts配合JavaScript还可以实现许多有用的功能,比如,级联下拉菜单的实现就是一个典型的例子:

    本例假设要实现的是一个文章发布系统,我们要发布的文章分为新闻类和技术类,其中新闻类又分为时事新闻和行业动态;技术类又分为操作系统、数据库、和编程语言等,为了便于添加新的条目,所有这些都保存在数据库表中。

    为此,我们建立一个名为articleClass的表和一个名为articleSubClass的表。

    articleClass表的结构如下:
    articleClassID字段:char类型,长度为2,主键
    articleClassName字段:varchar类型,长度为20
    articleSubClass表的结构如下:
    articleClassID字段:char类型,长度为2
    articleSubClassID字段:char类型,长度为2与articleClassID一起构成主键
    articleSubClassName字段:varchar类型,长度为20


    表建好后,在articleClass表中录入如下数据:如,01、新闻类;02、技术类

    在articleSubClass表中录入:01、01、时事新闻;01、02、行业动态;02、01、操作系统等记录。到这里,数据库方面的准备工作就已做好。

    有了前面做登录例子的*础,理解下面要进行的工作就没有什么难点了,我们现在的工作也在原来mystruts项目中进行。首先,建立需要用到的formbean即ArticleClassForm,其代码如下:

    package entity;
    import org.apache.struts.action.*;
    import javax.servlet.http.*;
    import java.util.Collection;
    
    public class ArticleClassForm extends ActionForm {
      //为select的option做准备
      private Collection beanCollection;
      private String singleSelect = "";
      private String[] beanCollectionSelect = { "" };
      private String articleClassID;
      private String articleClassName;
      private String subI;//子类所在行数
      private String subJ;//子类所在列数
      private String articleSubClassID;
      private String articleSubClassName;
    
      public Collection getBeanCollection(){
        return beanCollection;
      }
    
      public void setBeanCollection(Collection beanCollection){
        this.beanCollection=beanCollection;
      }
    
      public String getSingleSelect() {
        return (this.singleSelect);
      }
      public void setSingleSelect(String singleSelect) {
        this.singleSelect = singleSelect;
      }
      public String[] getBeanCollectionSelect() {
        return (this.beanCollectionSelect);
      }
      public void setBeanCollectionSelect(String beanCollectionSelect[]) {
        this.beanCollectionSelect = beanCollectionSelect;
      }
    
      public String getArticleClassID() {
        return articleClassID;
      }
      public void setArticleClassID(String articleClassID) {
        this.articleClassID = articleClassID;
      }
      public String getArticleClassName() {
        return articleClassName;
      }
      public void setArticleClassName(String articleClassName) {
        this.articleClassName = articleClassName;
      }
    
      public String getSubI() {
        return subI;
      }
      public void setSubI(String subI) {
        this.subI = subI;
      }
    
      public String getSubJ() {
        return subJ;
      }
      public void setSubJ(String subJ) {
        this.subJ = subJ;
      }
    
      public String getArticleSubClassID() {
        return articleSubClassID;
      }
      public void setArticleSubClassID(String articleSubClassID) {
        this.articleSubClassID = articleSubClassID;
      }
    
      public String getArticleSubClassName() {
        return articleSubClassName;
      }
      public void setArticleSubClassName(String articleSubClassName) {
        this.articleSubClassName = articleSubClassName;
      }
    }


    将它放在包entity中。其次,我们的系统要访问数据库,因此也要建立相应的数据库访问对象ArticleClassDao,其代码如下:

    package db;
    
    import entity.ArticleClassForm;
    import db.*;
    import java.sql.*;
    
    import java.util.Collection;
    import java.util.ArrayList;
    import org.apache.struts.util.LabelValueBean;
    public class ArticleClassDao {
      private Connection con;
    
      public ArticleClassDao(Connection con) {
        this.con=con;
      }
      public Collection findInUseForSelect(){
        PreparedStatement ps=null;
        ResultSet rs=null;
        ArrayList list=new ArrayList();
        String sql="select * from articleClass order by articleClassID";
        try{
          if(con.isClosed()){
            throw new IllegalStateException("error.unexpected");
          }
          ps=con.prepareStatement(sql);
          rs=ps.executeQuery();
    
          while(rs.next()){
            String value=rs.getString("articleClassID");
            String label=rs.getString("articleClassName");
            list.add(new LabelValueBean(label,value));
          }
          return list;
        }
        catch(SQLException e){
          e.printStackTrace();
          throw new RuntimeException("error.unexpected");
        }
        finally{
          try{
            if(ps!=null)
              ps.close();
            if(rs!=null)
              rs.close();
    
          }
          catch(SQLException e){
            e.printStackTrace();
            throw new RuntimeException("error.unexpected");
          }
        }
      }
    
      public Collection findInUseForSubSelect(){
        PreparedStatement ps=null;
        ResultSet rs=null;
        PreparedStatement psSub=null;
        ResultSet rsSub=null;
        int i=0;//大类记数器
        int j=0;//小类记数器
        String classID="";
        String subClassID="";
        String subClassName="";
    
        ArrayList list=new ArrayList();
        ArticleClassForm articleClassForm;
    
    
        String sql="select * from articleClass order by articleClassID";
        try{
          if(con.isClosed()){
            throw new IllegalStateException("error.unexpected");
          }
          ps=con.prepareStatement(sql);
          rs=ps.executeQuery();
    
          while(rs.next()){
            i++;
            classID=rs.getString("articleClassID");
            String sqlSub="select * from articleSubClass where articleClassID=? 
                order by articleSubClassID";
            psSub=con.prepareStatement(sqlSub);
            psSub.setString(1,classID);
            rsSub=psSub.executeQuery();
    
            articleClassForm=new ArticleClassForm();
            articleClassForm.setSubI(""+i);
            articleClassForm.setSubJ(""+j);
            articleClassForm.setArticleSubClassID("请输入一个小类");
            articleClassForm.setArticleSubClassName("请输入一个小类");
            list.add(articleClassForm);
    
            while(rsSub.next()){
              subClassID=rsSub.getString("articleSubClassID");
              subClassName=rsSub.getString("articleSubClassName");
              j++;
              //optionStr="articleSubClassGroup[" + i + "][" + j + "]=
    new Option('"+ subClassName +"','"+ subClassID+ "')";
              articleClassForm=new ArticleClassForm();
              articleClassForm.setSubI(""+i);
              articleClassForm.setSubJ(""+j);
              articleClassForm.setArticleSubClassID(subClassID);
              articleClassForm.setArticleSubClassName(subClassName);
              list.add(articleClassForm);
            }
    
            j=0;
          }
          return list;
        }
        catch(SQLException e){
          e.printStackTrace();
          throw new RuntimeException("error.unexpected");
        }
        finally{
          try{
            if(ps!=null)
              ps.close();
            if(rs!=null)
              rs.close();
    
          }
          catch(SQLException e){
            e.printStackTrace();
            throw new RuntimeException("error.unexpected");
          }
        }
      }
    }


    将它保存在db目录中。它们的目的是将文章的类和子类信息从数据库表中读出,以一定的格式保存在集合对象中以供页面显示。

    再次,我们要建立相应的jsp文件,文件名为selectArticleClass.jsp,代码如下:

    <%@ page contentType="text/html; charset=UTF-8" %>
    <%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %>
    <%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %>
    <%@ taglib uri="/WEB-INF/struts-logic.tld" prefix="logic" %>
    <html>
    <head>
    <title>
    选择文件类别
    </title>
    </head>
    <body bgcolor="#ffffff">
    <h3>
    选择文件所属类型
    </h3>
    <html:errors/>
    <table width="500" border="0" cellspacing="0" cellpadding="0">
      <tr>
        <td><html:form name="articleClassForm" type="entity.ArticleClassForm"
          action="selectArticleClassAction.do">
            <table width="500" border="0" cellspacing="0" cellpadding="0">
              <tr>
                <td align="right">文章大类*</td>
                <td>
                  <html:select property="articleClassID" styleClass="word"
                       onchange="articleClassFormredirect(this.options.selectedIndex)">
                    <html:option value="">请选择一个大类</html:option>
                    <html:optionsCollection name="articleClassForm" property="beanCollection" styleClass="word"/>
                  </html:select>
                </td>
              </tr>
              <tr>
                <td align="right">文章小类*</td>
                <td>
                  <select name="articleSubClassID" Class="word" >
                    <option value="">请选择一个小类</option>
                  </select>
                  <SCRIPT language=JavaScript>
                  <!--
                  var articleSubClassGroups=document.articleClassForm.articleClassID.
                       options.length
                  var articleSubClassGroup=new Array(articleSubClassGroups)
                  for (i=0; i<articleSubClassGroups; i++)
                  articleSubClassGroup[i]=new Array()
                  <logic:iterate name="articleSubClassList" id="articleClassForm"
                        scope="request" type="entity.ArticleClassForm">
                    articleSubClassGroup[<bean:write name="articleClassForm"
                            property="subI"/>][<bean:write name="articleClassForm"
                            property="subJ"/>]=new Option("<bean:write name="articleClassForm"
                      property="articleSubClassName"/>","<bean:write name="articleClassForm"
                      property="articleSubClassID"/>")
                  </logic:iterate>
                  var articleSubClassTemp=document.articleClassForm.articleSubClassID
                  function articleClassFormredirect(x){
                    for (m=articleSubClassTemp.options.length-1;m>0;m--)
                    articleSubClassTemp.options[m]=null
                    for (i=0;i<articleSubClassGroup[x].length;i++){
                      articleSubClassTemp.options[i]=new
                Option(articleSubClassGroup[x][i].text,
                       articleSubClassGroup[x][i].value)
                    }
                    articleSubClassTemp.options[0].selected=true
                  }
                 //-->
               </SCRIPT>
                </td>
              </tr>
            </table>
          </html:form>
        </td>
      </tr>
    </table>
    </body>
    </html>


    这里值得重点关注的是其中的JavaScript代码,有兴趣的可以仔细分析一下它们是怎样配合集合中的元素来实现级联选择的。

    最后,为了例子的完整。我们将涉及到action代码和必要的配置代码在下面列出:其中,action的文件名为SelectArticleClassAction.java,代码如下:

    package action;
    import entity.*;
    import org.apache.struts.action.*;
    import javax.servlet.http.*;
    import javax.sql.DataSource;
    import java.sql.Connection;
    import db.ArticleClassDao;
    import java.util.Collection;
    import java.sql.SQLException;
    public class SelectArticleClassAction extends Action {
      public ActionForward execute(ActionMapping actionMapping, ActionForm actionForm,
      HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) {
        /**@todo: complete the business logic here, this is just a skeleton.*/
        ArticleClassForm articleClassForm = (ArticleClassForm) actionForm;
        DataSource dataSource;
        Connection cnn=null;
        ActionErrors errors=new ActionErrors();
        try{
          dataSource = getDataSource(httpServletRequest,"A");
          cnn = dataSource.getConnection();
          ArticleClassDao articleClassDao=new ArticleClassDao(cnn);
    
          Collection col=articleClassDao.findInUseForSelect();
          articleClassForm.setBeanCollection(col);
          httpServletRequest.setAttribute("articleClassList",col);
    
          //处理子类选项
          Collection subCol=articleClassDao.findInUseForSubSelect();
          httpServletRequest.setAttribute("articleSubClassList",subCol);
          return actionMapping.findForward("success");
        }
        catch(Throwable e){
          e.printStackTrace();
          //throw new RuntimeException("未能与数据库连接");
          ActionError error=new ActionError(e.getMessage());
          errors.add(ActionErrors.GLOBAL_ERROR,error);
        }
        finally{
          try{
            if(cnn!=null)
              cnn.close();
          }
          catch(SQLException e){
            throw new RuntimeException(e.getMessage());
          }
        }
        saveErrors(httpServletRequest,errors);
        return actionMapping.findForward("fail");
      }
    }


    将其保存在action目录中。

    在struts-config.xml文件中做如下配置:

    <form-beans>
    中加入
    <form-bean name="articleClassForm" type="entity.ArticleClassForm" />


    ><action-mappings>
    中加入:

    <action name="articleClassForm" path="/selectArticleClassAction" scope="session"
          type="action.SelectArticleClassAction" validate="false">
    <forward name="success" path="/selectArticleClass.jsp" />
    <forward name="fail" path="/genericError.jsp" />
    </action>


    为了对应配置中的
    <forward name="fail" path="/genericError.jsp" />
    ,我们还要提供一个显示错误信息的jsp页面,其代码如下:

    <%@ page contentType="text/html; charset=UTF-8" %>
    <%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %>
    <html>
    <head>
    <title>
    genericError
    </title>
    <link href="css/mycss.css" rel="stylesheet" type="text/css">
    </head>
    <body bgcolor="#ffffff">
    <html:errors/>
    </body>
    </html>


    现在一切就绪,可以编译执行了。在浏览器中输入:http://127.0.0.1:8080/mystruts/selectArticleClassAction.do就可以看到该例子的运行结果了。(T111)

    本文作者:张永美 罗会波 湖北省当阳市国税局 可通过lhbf@sina.com与他们联系

    .

    posted on 2005-01-07 16:36  海天一鸥  阅读(18803)  评论(12编辑  收藏  举报