2005年11月16日

(作者:俞良松 2002年09月26日 本文选自:开放系统世界)

      Struts是源代码开放的企业级Web应用开发框架,它的设计目的是从整体上减轻构造企业Web应用的负担。本文通过一个Struts应用的实例,帮助你迅速掌握Struts。

  Struts是在Jakarta项目下开发的源代码开放软件,由一系列的框架类、辅助类和定制的JSP标记库构成,定位在基于Model 2设计模式的J2EE应用开发。Model 2体系是MVC(Model-View-Controller)体系的一种应用。在MVC体系中,数据模型(Model)、表现逻辑(View)和控制逻辑(Controller)是分离的组件,但它们可以互相通信。Struts力图从整体上减轻构造企业级Web应用的负担,并提供国际化和数据库连接池支持。

  Struts体系可以看成两个相对独立的部分:第一个部分是Struts API,用于编写支持Struts的应用组件;第二部分是Struts的JSP标记库,由html、bean、logic和template四个标记组成。Struts的两个部分有着各自不同的用户。对于规模较小的项目,同一个用户可能同时使用这两个部分;但对于规模较大的项目,通常开发者使用API组件,而负责HTML页面布局的人使用标记库。

  Struts的设计目标是为Model 2 Web应用开发提供一个强大的框架。同时,Struts还包含了一些实用组件,例如Digest,但这些组件并不从属于上面提到的两个部分。

  Struts应用的体系结构

  对于从传统编程环境转入Web开发的人来说,Web编程中令人很不习惯的一个特点是缺乏“程序”。传统的应用总是有主入口点、流程控制和出口点。但在Web网站上,用户可能从任何地方进入,按照一种完全随机的次序访问各个页面,甚至可能跳过多个页面,也可能在一、两个小时内毫无动静。这是HTTP访问的基本特征,无论是Struts还是其他Web编程框架,都无法改变这一点。然而,Struts能够隐藏Web访问固有的“混乱”,帮助开发者建立起清晰和明确的秩序和规则。

  在Struts应用中,有一个称为ActionServlet的主调度程序(或称为分配器),如图1所示。不过,并非所有的请求都必须通过ActionServlet。用户的请求目标可以是非Struts的页面,也可以是那些使用了Struts标记库但不使用Struts请求分配服务的页面。这正是Struts体系的优点之一:按需使用。许多编程框架要求你要么不用,要么全部使用,而且一旦你决定使用,以后要悔改从前的错误就会付出高昂的代价。Struts按需使用的优点与这类系统形成了强烈对比。

  

  图1 Struts框架中的请求处理

  Struts应用由下面这些基本模块构成:

  1.配置信息;

  2.Servlet,主要是Struts的ActionServlet;

  3.动作类(Action),执行逻辑和控制(请求分配)功能,它们由ActionServlet调用;

  4.JSP页面(属于View),常常通过动作类分派;

  5.JSP标记库,根据需要使用;

  6.各种形式的JavaBean,包括用户定义的JavaBean。

  典型的Struts应用要用到三种配置文件:web.xml、struts-config.xml和可选的应用资源文件。

  web.xml是Web应用的标准配置文件,是所有J2EE Web应用必需的组成部分。应用服务器通过该配置文件把URL映射到Servlet和JSP,通过该配置文件为Servlet和JSP指定启动参数。为Struts应用提供的基本web.xml文件很简单,真正必需的只有一个主ActionServlet定义,以及一个确保Struts请求传递到ActionServlet的映射。按照惯例,以“.do”结尾的URL都是Struts请求,例如/login.do。应用服务器利用web.xml文件中的映射,把该请求传递给ActionServlet。接着,ActionServlet决定如何分配该请求。ActionServlet的决定依据是struts-config.xml中定义的规则,和/或是通过ActionServlet派生类额外定义的分配逻辑。

  struts-config.xml称为Struts配置文件。Struts应用是一个依靠struts-config.xml文件把组件连接起来的网络。struts-config.xml文件为Web应用的组件定义了逻辑名称,也定义了它们在Struts框架下的属性和关系,就像web.xml文件在Web应用框架之内定义组件一样。struts-config.xml文件包含了与Struts框架有关的应用信息,这些信息分四个类:

  1.数据源信息,它是可选的。在这里可以指定一个或者多个JDBC数据源,使得数据库定义信息集中化。对于数据库访问,Struts还有一个额外的优点,即支持基本的数据库连接池功能。

  2. Form Bean是JavaBean的一种特殊类型,它简化了Web表单的处理。

  3. Global Forwards是全局性的转发定义信息。Struts动作按照一种“请求—转发”机制运行。为了最大限度地分  离动作模块与转发目标,这里使用了一种映射机制,允许通过同义词引用转发目标。一些目标页面可能被多个动作类引用,例如登录页面,因此可以在全局转发定义部分把逻辑目标页面映射到物理目标页面,避免把这部分信息加入到动作定义部分。

  4. Actions定义了Struts应用体系的请求分配信息,它们是核心分配器的补充定义,负责处理各种具体的请求类型。

一个简单的应用

  基于Struts的Web应用和普通Web应用有着许多同样的要求,但Struts应用也有自己特殊的需求。一个可部署的Web应用应该可组织和构成一个WAR文件。WAR文件是带有图2所示目录结构的JAR包。对于Struts Web应用来说,Web-INF目录下还要加上一些额外的文件,例如struts-confg.xml文件和标记库描述器(TLD)文件。注意:应用的资源应该放入应用的类路径下,也就是Web-INF/lib目录或Web-INF/class目录下的JAR包内。对于大多数简单的Struts页面,我们只用到Struts标记库,而按照MVC的术语就不需要涉及Model和Controller部分,只涉及View。请看图3所示的主页例子。虽然这个页面没有表单,但Struts仍能够在设计这类页面时提供帮助。

  

  图2 Struts应用的目录结构

  

  图3 一个简单的View

  要管理会话,最简单的途径是使用Cookie。会话标识符被传递到客户端之后,客户端把它保存到Cookie,以后的每次请求就把Cookie发送到服务器。然而,和其他的许多Web解决方案一样,Cookie方案也不是万能的,因为一些用户可能不信任Cookie,关闭浏览器的Cookie支持。由于这种情况,URL改写技术就出现了。使用URL改写技术时,整个网站的所有URL后面都将加上会话标识符。虽然这个方案不像采用Cookie方案那样简单、稳固,但它确实行得通。URL改写技术的不稳固是有两方面的原因。首先,和Cookie不同,URL没有过期时间,如果一个带有会话标识的URL被截取后又重新在以后的访问中使用,那么这种URL不会很有用,因为会话一般在一定的时间后会被作废。其次,如果有一个URL链接的后面没有带上会话标识符,整个链都会中断,客户程序无法再次获取会话标识符,除非它备份了带有会话标识符的URL访问历史。

Servlet能够只通过一次方法调用完成URL改写。从技术上讲,JSP也一样能够办到这一点,但一个好的JSP页面应该不包含Java代码,或包含尽量少的Java代码。为此,Struts提供了一个链接标记。本例使用了该标记来维持客户端和服务器之间的会话状态信息。

  绑定View、Model和Controller

  前面的简单页面不需要Struts分配器,因为它只有简单的链接。图4显示了一个比较复杂的“类别”页面。它列举出了数据库中的类别条目,并将这些条目分别链接到对应的编辑页面。为显示这个页面,我们就要用到Struts的ActionServlet分配机制。

  

  图4 类别页面

  在web.xml文件中,放入一项表示所有以“.do”结尾的URL请求必须发送给Struts分配器的声明。这里的分配器可以是org.apache.struts.action.ActionServlet或其扩展类。Struts分配器在启动时读取struts-config.xml文件,并构造出一个动作映射图。本例指定了一个名为ShowCategories的动作类,来处理“ShowCategories”动作。可以看出,Struts应用的基本工作模式是:主分配器调用一个动作分配器,动作分配器确定或构造出Model部分(一个JavaBean或其它Java对象),并把它提供给View(通常是一个JSP页面)。

  本例使用Bean的情况稍微有点复杂,它有多个数据项,因此我们不是使用单个提供数据的Bean,而是要生成一组Bean。遗憾的是,JSP页面以HTML为基础,HTML没有提供循环或其他控制逻辑。不过,Struts的logic:iterate允许对数组进行迭代操作,如下面的代码片断所示:

<table> <logic:iterate id="category" type="com.strutsdemo.Category" name="<%= Constants.CATALOG_CATEGORIES %>" scope="application"> <tr> <td> <html:link page="/editCategory.do" name="category" property="mapping"> 编辑 </html:link> <html:link page="/removeCategory.do" name="category" property="mapping"> 删除 </html:link> <bean:write name="category" property="category"/> </td> </tr> </logic:iterate> </table>



  上面讨论了Model 2体系的Model和View部分,下面来看看Controller部分。Struts体系有一个主控制器,即ActionServlet。ActionServlet负责选择和调用合适的动作控制器—即org.apache.struts.action.Action的扩展类。动作控制器实现了process()方法。process()方法分析从URL请求传入的每一个参数,执行必要的业务逻辑,并返回一个指定了调用链中下一个链接的动作(通常是View)。在本例中,我们想要从数据库提取数据,创建管理这些数据的JavaBean,把多个JavaBean整理成一个数组,再把数组保存到请求的上下文,从而使得作为View的JSP页面能够方便地进行页面布局

为保证上述操作顺利进行,在struts-config.xml文件中加入声明,指定由哪一个动作处理器来处理指定的动作:

<action path="/showCategories" scope="request" type="com.strutsdemo.ShowCategoriesAction" unknown="false" validate="false"> <forward name="viewCategories" path="/       ShowCategories.jsp"/> </action>



  表单处理

  表单处理过程充分体现出Struts的优势。在Web应用中,大部分复杂的HTML处理任务都涉及到表单。表单编辑过程具有类似图5所示的请求或应答结构。更新操作的过程与创建操作的过程相似,但对于更新操作来说,“创建Model”这一步骤变成“装入Model”,而“保存Model”变成了“更新Model”。请注意Web应用的特点:操作过程随时可能中止,这既可能是因为用户通过显式的动作取消了当前的操作,也可能是因为用户没有提交表单,例如用户跳转到了一个不是用来处理当前表单的URL。

  

  图5 表单处理流程

  表单编辑过程分三个阶段:这里分别称之为准备(Preparation)、表现(Presentation)和存储(Preservation)。准备和存储阶段都属于Struts动作,而表现阶段主要是客户端的活动。表1显示了该过程中涉及的各种部件:

  表单Bean(Form Bean)是一种特殊的JavaBean类型,它简化了表单处理。Form Bean从org.apache.struts. action.ActionForm类扩展而来。Form Bean有几个有用的特点,例如,通过reset()方法可以把Bean的属性设置成默认值,通过validate()方法让Bean验证属性的合法性。更重要的是,ActionServlet确保Form Bean被创建且可供它的动作方法调用。HTML标记库还能够确保Form Bean被正确地初始化并从Form View获取数据。

  Form Bean应当属于Model部分,然而,由于它有validate()方法,因此从某些特征来看它更接近分配器。不过,不必太在乎这些概念上的问题。Model 2并不完全等同于MVC,而且一些人已经在责难MVC不外乎是几种简单设计模式的混合物。不管怎样,从应用实践的角度来讲,系统的稳定性远比概念的严格性更重要。在本例中,这个问题更加富有代表性,因为我们把持久化机制也包装到Form Bean里面。从技术上看,Bean数据的持久化副本就是一个View,因此,从这个意义上来讲,我们现在有了一个结合了分配器和View特点的Model。这种设计方式看起来似乎否定了引入Struts之类框架的理由,但实际上,这种设计方式两方面的特点弥补了许多遗憾。

  首先,由于验证代码和SQL代码在很大程度上依赖于Form Bean拥有的属性,所以把它们作为一个单元管理会带来很大的方便。由于这里只对Form Bean的属性感兴趣,“重量级”的分配器和View部件都得到了有效的隔离。其次,Form Bean与HTML标记库一起使用时,Form Bean可以包含其他对象。这些对象可以通过“.”符号应用。使用预定义的Java对象时,“.”引用方式能够带来很大的方便,因为Java不支持多重继承。“.”引用方式避免了手工编写大量get/set代码的繁杂工作。

  当内部对象是EJB时,“.”引用方式带来的方便更加突出,因为在JSP页面中引用EJB时,EJB往往显得很“笨重”。如果EJB嵌入到了Form Bean里面,许多这方面的遗憾就不再存在。更重要的是,它分离了Controller和Model,而且View持久化也简缩到了最简单的程度,因为EJB容器可以处理所有持久化方面的细节。这样,Form Bean就几乎成了一个纯粹的分配器,一切都变得整洁和清晰。

  如果EJB有大量的属性,而且按照ActionServlet通常对Form Bean所做的那样,按照每个属性分别更新的方式进行更新,就会出现大量的RMI调用开销。对于要求较高的应用,更好的选择是利用EJB 2.0本地接口,或者在EJB之前加上一个传统的JavaBean(通常是会话EJB),并把该Bean传递给实体Bean的UpdateAllProperties()业务方法。后面这种方案允许在单个RMI调用中完成所有的更新操作.

准备阶段

  一次典型的编辑会话要求有一个动作处理器准备View,即一个作为View的JSP页面,还要求有第二个动作处理器存储更新后的View。当然,存储操作之后会有第二个属于View的页面被显示,例如一个“数据已经更新,点击此处继续”的页面(参见表1)。

   表1:基于Form Bean的编辑过程要用到的部件

部件

说明

CatalogForm

Form Bean

EditCategoryAction

准备阶段

EditCategory.jsp

编辑

SaveCategoryAction

存储阶段

EditDone.jsp

确认数据已经保存

EditFailed.jsp

“数据没有保存”错误



  下面的代码片断显示了如何在struts-config.xml文件中配置准备阶段:

<action path="/editCategory" scope="request" name="catForm" type="com.strutsdemo.EditCategoryAction" unknown="false" validate="false"> <forward name="success" path="/EditCategory.jsp"/> </action>



  在准备阶段,容器尝试从Session或Request找出指定的Form Bean,这是因为在动作中指定了“name=…”。ActionServlet在struts-config.xml文件的
区域寻找Form Bean的别名,利用Form Bean的别名寻找对应的Java类。如果用户的请求带有参数,其名字匹配Form Bean属性名字的参数将被设置为属性值。Struts扩展了“属性名字”的含义,使得访问Form Bean内嵌对象的属性成为可能。本文的例子也用到了Struts的这一优点。

  准备好Form Bean之后,ActionServlet接着调用动作的process()方法,Form Bean作为参数之一传入process()方法。在这里,我们对Form Bean的属性作最后的调整,调用业务方法,委派作为View的EditCategory,从而生成一个以Form Bean中合适数据为基础的HTML页面。这个页面被传递给客户端,接下来就进入了“表现”阶段。

  表现阶段

  这一阶段用户编辑表单并提交。如果服务器端的应用认为用户提交的内容存在问题,它把表单再次显示给用户,加上适当的提示信息;重复该过程,直至用户提交了合法的表单,或取消了表单处理过程。编辑过程的中止可能是由于用户跳转到了其他页面,或者启动了一个取消动作(例如点击了一个由html:cancel标记定义的按钮)。虽然在理论上,View的验证和再次显示操作应该属于表现阶段,但在Struts应用中,这部分功能在存储阶段实现最方便。

  存储阶段

  准备阶段创建了一个带有“name=”属性定义的动作CatForm,存储阶段要加入另外两个属性,即:“validate=‘true’”和“input=”属性。

<action path="/saveCategory" scope="request" name="catForm" type="com.strutsdemo.SaveCategoryAction" unknown="false" input="/EditCategory1.jsp" validate="true"> <forward name="success" path="/CategoryUpdated.jsp"/> </action>



  设置了“validate=‘true’”属性选项之后,服务器端就会增加一个处理步骤。重新用来自View的数据构造出Form Bean,或更新From Bean的时候,Form Bean的validate()方法会被调用。validate()方法执行必要的合法性验证操作。如果用户的输入数据中存在错误,validate()方法就创建一个或多个ActionError对象。这些ActionError对象包含了错误信息源ID和表单输入域的名称。这些ActionError对象被收集和整理到一个ActionErrors对象,随后ActionErrors对象由validate()方法返回。如果用户输入的数据不包含错误,validate()返回null。

  由于指定了“input=”属性,一旦出现了错误,动作会被忽略,而“input=”指定的View被显示。这个View既包含Form Bean,也包含当前出现的错误对象集合。一般地,这个输入页面就是原来执行编辑功能的JSP页面。

  大多数Struts的html标记有对应的HTML标记,但Struts有一个HTML没有的标记,即标记。要中止表单编辑过程,用户既可以手工输入URL,也可以点击不指向存储动作处理器的链接。因此,用标记定义的“取消”按钮,不是取消编辑操作的唯一方法。

  假设validate()方法没有发现任何错误,且用户没有点击“取消”按钮,存储动作的process()方法将被调用。在本例的process()方法中,我们调用了Form Bean的save()方法把数据写入持久性存储设备,然后根据写入操作是否成功,显示“存储操作成功”或“存储操作失败”的View。

构造和运行Struts应用

  要构造和运行本文的示例应用,你必须了解如何使用Jakarta的Ant工具。如果你还不了解Ant,现在该是学习它的时候了!赶紧到网站下载Ant,通常要解开压缩,设置一下ANT_HOME环境变量,然后把Ant加入到执行路径就可以了。

本文示例的build.xml需要稍微定制一下,修改指示本地Tomcat位置的配置,使它能够找到在Tomcat下编译所必需的类。另外,你还要有一份Struts的JAR。你可以去下载最新的版本。

  struts-config.xml文件是粘合Struts应用各个部分的配置文件。在部署完成后的Web应用中,struts-config.xml在Web-INF目录下。你应该修改一下数据源配置,使之符合你当前使用的DBMS环境。数据模型和SQL模式文件在下载包的DBMS目录下,SQL文件针对PostgreSQL DBMS编写。

  示例中src/com/strutsdemo/ShowCategoriesAction. java是一个简单的分配器。ActionForward()是请求分配方法,从ActionServlet调用。该方法可以完成主要的工作,例如分析请求参数、执行计算,以及构造出View使用的JavaBean。另外,该方法还要根据处理结果,确定下一个要显示的是什么页面:可能是预设的多个页面之一,也可能是一个错误信息页面。

  ActionForward()的请求分配过程

  当然,最复杂的处理过程与表单有关。ActionForward方法的请求分配过程是:

  1. ActionServlet,对请求进行解码。由于为动作指定了Form Bean,ActionServlet处理Form Bean(参见下面有关“ActionServlet如何使用Form Bean”的说明)。然后,请求传递给了EditCategoryAction。

  2. EditCategoryAction;准备处理View,或者从数据库装入现有数据,或者创建新的数据项。动作处理器利用Mapping.findForward把控制传递给EditCategory.jsp。

  3. DitCategory.jsp,显示出Form Bean,允许用户编辑数据。用户提交数据后,控制转到ActionServlet。

  4. ActionServlet,对请求进行解码。这一次,Form Bean将从View的数据初始化,因为它是一个Struts的JSP表单页面。由于有Form Bean,且struts-config.xml中指定了“validate=‘true’”,名为“catForm”的Form Bean的validate()方法被调用。如果用户提交的数据未能通过合法性验证,则控制转到EditCategory1.jsp。

  5. EditCategory1.jsp,它只是EditCategory.jsp略加修改后的一个版本。如果有必要,原始编辑页面和带有错误提示的编辑页面可以使用同一个View。Struts的JSP标记能够帮助我们轻松地办到这一点。该页面提交给/saveCategory.do。这样,用户就在这几个页面之间绕圈子,直到他跳转到一个与编辑操作无关的页面,或者他提交的数据通过了合法性验证。

  6. 如果Form Bean合法性验证通过,ActionServlet把请求(包括Form Bean)传递给SaveCategoryAction。在这个例子中,“save”可能意味着创建操作,也可能意味着更新操作,具体由URL提供的选项决定。写入数据的操作通过调用Form Bean的store()方法完成。注意:实际的应用应当使用某种类型的事务管理机制(或使用EJB,因为EJB有内建的事务管理机制),以避免并发访问带来的问题。

  ActionServlet如何使用Form Bean

  涉及Form Bean的ActionServlet处理过程包含六个步骤:

  1. 找到或创建Form Bean;

  2. 据从HTTP请求传入的相应数据,更新Form Bean的各个属性;

  3. 检查用户是否点击了“取消”按钮。如是,跳过步骤4和步骤5;

  4. 验证Form Bean数据的合法性;

  5. 如数据未能通过合法性验证,发送“input=”参数中指定的View;

  6. 否则,把Form Bean传递给动作处理器

2005年11月12日

本系列源改编自O’Reily的Strus Cookbook,后因时间关系没再进行。如果以后有时间还需继续,可能会更简化一些 。

Struts秘籍之第1段:配置Struts应用

Struts秘籍之起式:第1.2式:部署Struts示例应用

Struts秘籍之起式:第1.3式:迁移至Struts 1.1

Struts秘籍之起式:第1.4式:升级至Struts 1.2

Struts秘籍之起式:第1.5:JSP 应用转到Struts

Struts秘籍之起式:第1.6式:管理Struts配置文件

Struts秘籍之起式:第1.7式:使用Ant进行构建和部署

Struts秘籍之起式:第1.8式:用XDoclet 产生Struts配置文件

Struts秘籍之第2段:UI

Struts秘籍之第1段:第2.1式:用插件来进行应用初始化

Struts秘籍之第1段:第2.1式:关于标签库声明

Struts秘籍之第1段:第2.3式:在JSP中使用常数

Struts秘籍之第1段:第2.4式:多配置文件

Struts秘籍之第1段:第2.5. 将应用重构为模块

Struts秘籍之第1段:第2.6. 使用多个资源束

Struts秘籍之第1段:第2.7. 访问来自于数据库中的消息资源

Struts秘籍之第1段:第2.8. 有选择地禁止Action

Struts秘籍之第2段:第3.1式: 使用JSTL

Struts秘籍之第2段:第3.2式:使用Struts-EL 标签

Struts秘籍之第2段:第3.3式:显示索引属性

Struts秘籍之第2段:第3.4式:在表单中使用索引属性

Struts秘籍之第2段:第3.5式: JSTL循环中使用索引属性

Struts秘籍之第2段:第3.6式: 从图像提交表单

Struts秘籍之第2段:第3.7式: 动态产生JavaScript

Struts秘籍之第2段:第3.8式:使用JavaScript动态改变选择项

Struts秘籍之第2段,第 3.9式,产生动态选择列表项目

Struts秘籍之第2段:第3.10:过滤文本输入