UML软件工程组织

克服Struts开发障碍
Michael Coen,Amarnath Nanduri著;陈姣姣 译 选择自 article.itebook.net
克服Struts开发障碍
一、摘要

建立和维护企业应用程序非常困难。而为这些应用设计出上乘的、易于维护的用户界面则是所有工作中最让人畏惧的任务。来自Apache Jakarta 项目的Struts框架为J2EE(Java2平台企业版)带来了Model 2 结构。在本文中,两位作者讨论了开发者在使用Struts 的过程中所遇到的问题,以及简化这些问题的相应方法。

除非你过去几年内潜居于石洞之中,否则你不可能没听说过Struts framework。Struts是由Apache软件基金会最初发起的开源,主要是为了促进Web应用演示层内的模型-视图-控制器(MVC)设计范例。truts 提供了使用Service的MVC模式给Worker 模式。一个设计优秀的结构总是力争耦合宽松、结合性高。Struts为在多捆绑的企业Web应用的演示层实现这个目标提供了一个机制。 实现企业应用结构所面对的最让人望而生畏的任务之一就是演示层的创建和维护。用户期望得到非常功能化的、坚固的、和优雅的灰土用户界面。因此,演示层的代码库使得应用层超负荷运行。另外,不同的显示平台如无线电话和PDAs 的出现使得原本复杂的状况更加复杂的多。

各种不同的书和文章已经讲述了Struts的内部工作原理并且教我们如何使用这个框架。本文详细阐述了使用Struts 的Web应用开发者所遇到的问题,以及如何解决这些问题。下列方法中有许多可以应用到不同的MVC框架中如即将上市的JavaServer Faces 规范。 Craig R. McClanahan,Struts的创始人之一,造就了这个规范。

本讨论的主题包括:在使用Struts框架,用BEA WebLogic Server建立J2EE(Java2平台企业版)应用的过程中出现最多问题的所有区域。我们将讨论下列专题:
·创建/维护 struts-config.xml
·表格/会话期管理
·Struts 映射和用户界面的关系
·管理Back按钮
·用户认证
·用户界面控制流程
· 异常处理
·测试

二、挑两个,早上呼我

Struts框架毫无疑问,减轻了企业应用程序的用户界面的开发和维护。但是,即使只是在一个简单的应用中使用了Struts ,开发者也会迅速的认识到struts-config.xml这个恶魔。 这个文件很有可能迅速变得难于处理。在建立企业应用时,struts-config.xml 能够多出 500个动作映射,使得自身变得真正地难于管理。

我们推荐两个工具来帮助治理这个头疼的问题。首先,使用来自Alien-Factory 的Microsoft Visio 和StrutsGUI 文档化你的用户界面流程。StrutsGUI是一个Visio 模版,它对使用Struts 术语描述用户流程图有帮助。在Struts 模版内有一个隐藏的功能:只要右键点击该项,选择Edit Title Properties,然后选择Tools项,你就能够在该图的基础上生成struts-config.xml 文件。例如,图1中显示的简单应用生成了如下列代码所示的struts-config.xml :



Figure 1. StrutsGUI model. Click on thumbnail to view full-size image.



<?xml version="1.0" encoding="ISO-8859-1" ?>

<!-- Struts Config XML - Sample Struts App -->
<!-- ===================================== -->

<!-- AutoGenerated from : c:\dev\javaworld\app\sample.vsd -->
<!-- AutoGenerated on   : 02-18-2003 23:05:47 -->
<!-- AutoGenerated by   : Struts GUI v2.11   (c)2002 Alien-Factory -->
<!--                    : See 'http://www.alien-factory.co.uk' for details -->

<!-- GET YOUR STICKY FINGERS OFF! i.e. Do not edit. -->

<!DOCTYPE struts-config PUBLIC
      "-//Apache Software Foundation//DTD Struts Configuration 1.0//EN"
      "http://jakarta.apache.org/struts/dtds/struts-config_1_0.dtd">

<struts-config>

  <!-- ====================== Form Bean Definitions =================== -->
  <form-beans>
  </form-beans>

  <!-- ================= Global Forward Definitions =================== -->
  <global-forwards>
  </global-forwards>

  <!-- ======================= Action Definitions ===================== -->
  <action-mappings>
    <action  path="/Login"
             type="com.agilquest.onboard.presentation.actions.LoginAction">
      <description>Authenticates and authorizes a user.</description>
    </action>
  </action-mappings>
  
</struts-config>


为了使你的用户界面流程图更加复杂,我们推荐在使用StrutsGUI 时增加一个步骤。在你的StrutsGUI Visio 文档内,你可以轻易的将每个JSP (JavaServer Pages)页链接到它在应用中的实际屏幕快照。创建这个界面不但确实有助于应用程序的文档化,更重要的是,它成为了培训新的从事用户界面设计的开发者的一件极好的工具。

另一个帮助管理Struts应用的工具就是由James Holmes 发明的Struts Console。本质上,这个工具提供了一组设备,这些设备使你能够得到与StrutsGUI 相同的终点,但是它们在途径和长度上有区别。这两个工具都执行良好,其中任何一个都可以增强基于Struts的企业应用的可维护性。

三、现在,我将表格放在哪里?

ActionForm 会话期管理有点棘手。ActionForm的周期怎样?它在请求周期内或者会话期周期内吗?为了得到它所代表的功能周期,方案之一就是将ActionForm置于会话期之内。在这种情况下,你通常怎样维护这些ActionForm 对象呢?谁知道不再需要他们时删除他们要承担什么责任呢?典型的情况是:用户通过菜单从一个功能转为使用另一个功能。在这种情况下,原来的ActionForm 对象就应该从会话期删除,并且创建新的ActionForm 对象。这时还应该出现一个集中的Action类,MenuAction,它只处理菜单切换。这个Action 类从会话期删除多余的ActionForm 对象。然后将用户前进到创建新ActionForm 对象所在的新页面。

在这种情况下,站在用户的立场上或者基于用户权限,我们应该如何显示不同的菜单项呢?这个菜单也应该国际化,并且它修改时应该以用户权限为基础;也就是说,如果许可修改了,菜单也要相应的修改。有一种方法可以持续用户的权限。当用户注册时, MenuFactory根据这些权限创建菜单,为了增加安全性, MenuAction类在允许用户进行到他所选择的功能之前需要认证用户。命名struts-config.xml 中的ActionForm 对象的首要规则是对象名以Form 结束,从而简化了会话期内这些表格的维护。例如: ReservationForm, SearchUserForm, BankAccountForm, UserProfileForm,等等。

下列代码描述了一个具有Action 映射的普通菜单切换动作,它进一步阐明了ActionForm(s)管理:


public class MenuAction {

  public ActionForward perform(ActionMapping       _mapping,
                               ActionForm          _form,
                               HttpServletRequest  _request,
                               HttpServletResponse _response)
                                  throws IOException, ServletException {

    // Check end-user permissions whether allowed into the requested     
    // functionality 
    checkIfUserAllowedToProceed(_mapping, _form, _request, _response); 

    // Clean up the session object (this logic is in its own method)
    String formName = null; 

    HttpSession session = _request.getSession();
    Enumeration e = session.getAttributeNames();  

    while(e.hasMoreElements()) {
     
      formName = (String)e.nextElement();

      if (formName.endsWith("Form")){
        session.removeAttribute(formName);
      }    
    }

    // Now find out which functionality the end-user wants to go to
    String forwardStr = _request.getParameter("nextFunctionality");

    if (forwardStr != null && forwardStr.trim().length() > 0){
      return _mapping.findForward(forwardStr);
    }
    else {
      return _mapping.findForward("index");
    }
  }  
}


下列Action映射就是一个阐述如何以菜单选择为基础实现动作的例子:


<!-- A generic menu action that forwards the user from one 
     functionality to another functionality (after checking permissions)
-->
<action path="/menuAction"
        type="x.y.z.MenuAction"
        input="/menu.jsp">      
  <forward name="create_reservation" path="/actionResv.do"/> 
  <forward name="index"              path="/menu.jsp"/> 
  <forward name="add_person"         path="/actionPerson.do"/> 
  <forward name="logout"             path="/actionLogout.do"/> 
</action>


例子和映射都是可以自我解释的。

四、再说一次,我们是怎样关联的?

任何JSP页的许多输入点与许多现有点之间都有一个关系,这取决于页面本身的复杂程度。认识到这些关系对于理解和维护用户界面是至关重要的。我们已经将JSP页和Action 类之间的关系定义为:
·1:1 关系
·1:N 关系
·N:N关系

1:1 关系 在1:1 关系中,用户通过Action类从一个JSP页切换到另一个页面;这就使得JSP页和Action 之间容易形成一个紧密的耦合。唯一的额外开销就是struts-config.xml 中有一个Action映射。这个在struts-config.xml 中只有一个Action 映射的简单Action 可用于从一个页面切换到另一个页面。直接通过一个JSP页访问另一个JSP页是不太现实的;他不能够检查转向目标JSP页的用户权限(如果可行的话)。他还导致了维护方面的问题。为了避免这些问题,可以总是通过Action 类从一个JSP页转向另一个JSP页:


<!-- A generic action that forwards request from one JSP page to another JSP page -->
<action path="/forwardAction"
        type="x.y.z.One2OneAction"
        input="/test1.jsp">      
  <forward name="continue"  path="/test2.jsp"/> 
</action>


1:N关系 稍微复杂一点的关系就是JSP页有多个现有点但是只有一个输入点,也叫做1:N关系。在这种情况下,总是使用一个单一 Action类分支到不同的目标。这就保证了Action 在将用户推进到目标之前能够检查不同的情况或者权限。唯一的额外开销就是在struts-config.xml 中有一个Action 映射。这也推动了JSP页与Action 映射之间的1:1 映射。下面的Action映射标出了一个映射,它有一个单一输入点和多个前推,多个前推代表着多个现有点:


<!-- A generic action that forwards request from one JSP page to different 
     branches depending on the selected hyperlink, by the end-user
-->
<action path="/branchAction"
         type="x.y.z.One2NAction"
         input="/test1.jsp">      
  <forward name="target1"   path="/test2.jps"/> 
  <forward name="target2"   path="/test3.jsp"/> 
  <forward name="target3"   path="/someAction.do"/> 
</action>


N:N 关系 最复杂的关系,即N:N关系,指的是JSP页或者Action 类有多个输入点和多个现有点。N:N 关系是频繁出现在企业应用程序中的一个有趣和复杂的部分。N:N关系最初应用在不同的JSP页访问一个公共JSP页或者一个公共Action类的情况下。假设用户进入的JSP页是一个网络中心(特别是这个JSP页由不同的JSP页都可到达),但是用户又想返回或者取消这个流程;那么开发者就处于进退两难的局面:不知道怎样将用户送往正确的页面。

另一个场景是:Action类接合到数据库(通过不同的功能函数或者JSP页),并且出现错误。以用户原来所在的位置为基础,将用户发送回原来所在的位置或者适当前推,这需要仔细推敲。struts-config.xml映射证明是没有帮助的,因为输入域是一个确定的JSP页或者 Action 类。我们创建的结构应该足够灵活,这样开发者不必在struts-config.xml 折腾就能够轻而易举的修改流程逻辑。这就是N:N关系所要解决的问题。通过实现一个能够灵活发送用户到目标所在地或者目的文件的界面,这个值修改起来就比较容易。下面的Action映射给出的映射有多个输入点和多个前推,这些前推代表多个现有点:


public class N2NAction {

  public ActionForward perform(ActionMapping      _mapping,
                               ActionForm         _form,
                               HttpServletRequest _request,
                               HttpServletResponse _response)
                                  throws IOException, ServletException {

    N2NInterface if = (N2NInterface)_form;

    //Execute some business functionality here
    try{
      //Business logic successful?
               
    }
    catch(Exception e){

      //Indicates failure
      return _mapping.findForward(if.getSource()); 
    }

    //Indicates success
    return _mapping.findForward(if.getDestination());            

  } 

}

<!-- A generic action that forwards request from one JSP page to another JSP page -->
<action  path="/sourceAndDestinationAction"
         type="x.y.z.N2NAction"
         input="/test1.JSP">      
  <forward name="source1"          path="/source1.JSP"/> 
  <forward name="source2"          path="/source2.JSP"/> 
  <forward name="source3"          path="/someAction.do"/> 
  <forward name="destination1"     path="/destination1.JSP"/> 
  <forward name="destination2"     path="/destination1.JSP"/> 
  <forward name="destination3"     path="/destination2.JSP"/>
</action>

   A hyerplink can be something like
   <a href="sourceAndDestinationAction.do?
    source=source1&destination=destination1">click me</a>
   <a href="sourceAndDestinationAction.do?
    source=source2&destination=destination2">click me too</a>


所有的ActionForms 默认地都必须所有这三种关系(通常通过界面)。使用普通的Action 类,你可以自由地在用户界面流程中移动。
五、Back之痛!

设计演示层,有一个好方法就是按照功能设计。例如:在一个预订系统中预订的时候,将所有的相关动作类封装到一个包装内如


com.companyname.productname.presentation.
   reservation.mak


就是一个不错的方法。深度封装也可以应用到ActionForm类中。表格应该处在功能周期的会话期内。这就保证了该完整功能所需的数据放在表格对象本身之内。这样用户就能够前进到功能的任何页面并且找到该页上显示的正确数据。接着用户还能够在最终保存该数据之前更新数值。因此,就会产生一个有趣的两难局面:当用户确认数据、按下浏览器中的Back 按钮、修改并再次提交的话会发生什么事情?例如,在创建了数据库中的预定后,用户返回并试图再次提交同样的数据。演示层必须在应用层有机会抱怨之前先捕捉到这个错误。处理这种情况的方法之一就是在提交之前创建一个令牌,提交之后检查令牌的有效性,并立即修改令牌值——这样用户就不能够再次使用Back 按钮来提交同样的值。

这个方法的弊病之一就是需要管理令牌:例如,如果用户试图保存数据失败了,但是令牌值却已经修改了。如果这样的话,用户就不能够修改数据,和在不重新设置令牌的情况下重新提交。相应的Action 类也不会允许提交。那么到底什么时候才应该重置令牌呢?用户可以遍历六个页面,那么在确认它的修改时就会收到错误,然后定向到六个页面中的任何一个页面。

要管理这个问题,就需要在用户请求一个特别功能时创建一个表格并在会话期内存储一个默认的令牌值。在保存之前,用户可以多次使用Back按钮进行修改。一旦用户提交了这些修改,只有在保存成功之后才能重新设置令牌。如果确认失败,用户可以使用Back 按钮或者前进到产生失败的页面,修改数据然后重新提交数据。一旦确认成功的话,令牌值就会修改。ActionForm 对象本身也可以包含令牌值(令牌值可以通过程序设置)。还有一个方法就是,在第一次提交之后,Submit 按钮就变得无效、不允许用户再次提交直到有事情发生。我们建议每个ActionForm 处理他自己的令牌,应用至多是使用不止一个不同的ActionForm对象来处理一个特殊功能。


六、你是谁,在这做什么?

企业应用必须设计成能够同时支持多个认证模式。这对于来自ISVs(独立的软件开发商)的软件更为重要。例如,假设一个应用的认证要求是单一登录、询问用户密码、条形码认证或者指纹认证,甚至是将来有可能出现的声音认证。JAAS (Java 认证和授权服务),这个可插入的认证机制,证明可用于这个方面。默认的, Struts 1.1 支持JAAS。JAAS也可用在具有极好结果的Struts 1.0中。对于认证,一个很好的继承性设计将会推动使用不同的认证动作类,每个动作类都有他自己唯一的Action 映射。这种设计方法将会推动对不同认证的同时支持。如果需要添加新的认证机制,只要用一个支持该认证的Action类在struts-config.xml内创建一个新的动作映射即可。

七、我们去往何处?

流程控制器的设计目的是限制和引导用户通过应用程序,它在保证用户遵循特殊流程方面很实用。让流程控制器以系统的地位为基础证明是有用的。无论用户什么时候试图访问系统中的功能,流程控制器都可以确保用户有权进入到特殊流程中。而且,就算出现了意外情况,用户也能够被引导到恰当的页面。放置流程控制器最好的地方是在超类中。只有在流程控制器满意之后用户才允许执行特定动作。

图 2 给出了一个简单的继承性模型,它可用来使用合并到用户认证中的流程控制器。



Figure 2. Simple authentication class model


八、犯错乃人之常情...我们不是圣贤

Struts框架通过ActionError和ActionErrors类为我们提供了异常处理结构。 ActionError就是——存在于Action 或者ActionForm类中的错误,或者应用层给出的错误。ActionError 类典型地可通过使用简单的钥匙构造,或者通过使用钥匙/值对来构造。如果你的应用有国际化关系,那么该钥匙可用于查找国际化信息资源数据库。如果这些国际化关系不存在的话,那么取代值可用于显示错误信息。至多有四个占位符设置在对象排列中,每个占位符都包含整个错误信息的一个单独部分。这些值可构造得与MessageFormatter 类相似。

ActionErrors类,扩展了ActionMessage 类,它是具有单一公共方法的ActionError 类的集合,这个单一公共方法是add(java.lang.String property, ActionError error)。这个方法签名中的第二个参数是直接的:他指的是已经存在的真实错误(ActionError)。通过将错误信息与指定的域关联起来,属性参数可用于域级别的校验。既然错误信息与指定域关联,你可以轻易地将信息定位于有疑问的域附近或者定位于它能产生最大意义的任何地方。例如,如果你在用户的逻辑标记符之上进行域级别的校验,并且在类LoginAction 中出现错误,你可以使用下列代码片断:


ActionErrors errors = new ActionErrors();
errors.add("logiinID", new ActionError("loginID.invalid"));
saveErrors(_request, errors);
return (new ActionForward(_mapping.getInput()));


字符串"loginID.invalid"就是用于信息资源数据库中国际化字符串值的钥匙。为了显示错误信息,在你的login.jsp中,使用下列HTML:


<font color="red"><html:errors property="loginID"/></font>


有了对用于异常和异常链的Struts 框架设备的这个特别理解,要管理整个企业应用结构中的错误需要一个更加通用的方法。企业应用异常框架中最重要的宗旨有:
·开发异常类体系
·从系统异常中退耦用户异常
·给出异常的内容、类型和严重级别
·退耦异常处理和应用日志
·创建用于异常链的工具
·具体化用于国际化用途的异常字符串

许多文章和关于这个问题的完整书籍,都讨论了使用Java的异常处理。Brian Goetz,在他的JavaWorld 系列 "Exceptional Practices" 中提供了关于这个主题的非常好的建议。

九、测试,就是用户想要的!

尽管许多开发者都把测试看作是软件开发的最不吸引人的部分,测试企业应用对于它的整体成功还是很关键的。许多灵活的方法,如极限程序设计(XP),将测试放在企业应用开发的最前部。这种强调实际上是相当爽的。为了简洁起便,我们只讨论用于Struts 类的单元和集成测试策略。

两个原始策略可用于服务器端的测试:模拟对象(MO)测试和容器内(IC)测试。模拟对象测试从本质上扩展了"多树桩的"行为,并且它是自我说明的。从本质上说,开发者负责模拟定义接口的类,这些接口归进行测试的类使用。使用这个方法,开发者可以模拟容器。模拟对象测试也被叫做endotesting ,因为设置测试的动作是在受限环境中的类的内部进行的。

容器内测试这个策略应用于下列情况:在产品环境中将要使用的实际容器也用于整个单元测试过程中。这些测试案例必须至少合并到使用Ant的每夜建立过程中。单元测试Java类的有效方法之一就是使用JUnit。他是用来测试普通JavaBeans 的一件极好的工具。另外一个重要的开源工具就是Cactus。Cactus,JUnit框架的扩展,可以在单元级别上测试服务器端的代码。Cactus 支持用于服务器端测试的容器内方法。这两个测试工具都是很有用的,它是任何追求品质的企业应用开发团队的基石。

对于测试Struts来说,普遍接受的策略就是通过应用层测试演示层。你选择模拟对象测试方法还是容器内测试方法,主要取决于你们团队的需要和安慰级别。要自动执行Struts 测试,需要创建一个名为StrutsTestCase 的、对于JUnit测试方法的扩展,它对MO 和 IC都支持。使用StrutsTestCase 测试Action 和ActionForm 类,是整个测试策略的一部分,它应该包括这里讨论的回归测试工具。其他工具如Apache JMeter 提供了应力/负载测试。

十、简化Struts 开发

总的来说,我们讨论了你应该如何简化struts-config.xml文件的设计和维护,定义了错误处理和测试策略,简化了常见问题如处理Web浏览器的Back按钮,基于会话期的表格管理以及用户认证。最后,我们还清楚的文档化了用户界面控制流程的管理。我们的经历告诉我们:开发高品质的企业应用是困难的。当你使用Struts框架创建企业应用时,融合我们的建议到你的开发计划中,你就能够使你的设计、开发和维护费用减至最低。

 

 

版权所有:UML软件工程组织