IBM®
跳转到主要内容
    中国 [选择]    使用条款
 
 
Select a scope:Search for:    
    首页    产品    服务与解决方案     支持与下载    个性化服务    
跳转到主要内容

developerWorks 中国  >  WebSphere  >

将状态模式应用于 WebSphere Portal V5 Portlet: 第 1 部分:概述

developerWorks
文档选项

未显示需要 JavaScript 的文档选项


级别: 初级

Tim Hanis (hanistt@us.ibm.com), 高级软件工程师, IBM
Skyler Thomas (tskyler@us.ibm.com), 执行顾问, IBM
Chris Gerken (cgerken@us.ibm.com), 高级软件工程师, IBM

2004 年 3 月 01 日

IBM WebSphere Portal(以下简称为 WebSphere Portal)提供了构建企业应用程序的平台。开发人员正在使用它来构建显示极为复杂的行为的门户和 Portlet,它们远远超越了简单的 HelloWorld Portlet。J2EE TM模型提供了在 Servlet 领域中处理复杂的页面流的定义完善的框架。Portlet 开发人员需要类似的框架来支持他们的 Portlet。本文描述了状态转移模式,您可以把它应用到用 WebSphere Portal V5 Portlet API(也称作 Portlet API 1.2)开发的 Portlet 中。

引言


本文取代了具有相似名称的一篇文章。本文更新的目的是为了与 IBM WebSphere Portal V5.0 Portlet API保持一致,并删除对已弃用的方法和类的引用。具体说来,就是修改了模式实现,以删除对已弃用的类 org.apache.jetspeed.portlet.DefaultPortletAction 和已弃用的方法: org.apache.jetspeed.portlet.PortletURI.addAction(PortletAction)org.apache.jetspeed.portlet.event.ActionEvent.getAction() 的引用。

第 2 部分中的示例实现现在是 J2EE 1.3应用程序,并且它使用了 WebSphere Portal V5 开发工具。

一般来说,WebSphere Portlet 开发人员已依赖于 IBM Portlet API 作为他们的实现的基础。该 API 提供了用于开发全功能 Portlet 的组件。然而,它并没有为实现设计完善的页面导航模型提供帮助。在创建复杂的 Portlet 时,开发人员必须处理 Portlet中的视图导航,并考虑如何高效地使用操作事件和侦听器来响应用户操作事件(例如单击按钮或链接)。

以下将要讨论的状态模式设计为 Portlet应用程序的设计提供了一个很好的途径,可以帮助组织页面转换,并在模型-视图-控制器(Model-View-Controller,MVC)设计模式中实现职责的分离。如果您选择使用 IBM Portal API 来设计 Portlet,您就需要用到类似这样的方法。

本文描述了一个模式的实现。我们推荐 Struts 作为 Portlet 开发的框架解决方案。Jakara Struts 项目是一个 Apache Software Foundation 开放源代码项目,它提供了处理这些同样的 MVC设计和页面控制流问题的框架。它还提供了对异常处理、错误处理、输入校验、表单以及国际化的支持。您可以使用 Struts 框架来编写应用程序并将其作为 Portlet 部署。要了解相应的使用 Struts 实现的样本程序,请参阅 开发和部署作为 WebSphere Portal V5 Portlet 的 Struts 应用程序

Portlet 开发指南和示例实现使您能够很好地理解 Portlet API。然而,复杂过程的控制的实现超出了 API 的范围。如果您没有设计完善的方法来实现控制逻辑,那么您最终创建的 Portlet 的代码中将有相当多的一部分专用于考虑用户请求的目的。这些代码不仅是重复的,它们还使 Portlet 更难被读懂,因为您需要费很大力气去理解控制逻辑以了解 Portlet 的实际工作。控制逻辑也容易出现错误,因为它依赖于一些方面,例如在 Portlet 方法中依赖于字符串名称匹配以使事件与侦听器绑定、使侦听器操作与行为绑定。它还会使业务逻辑变得更复杂,因为控制功能的实现可能类似于业务逻辑。

为了解决控制逻辑的这些问题,您可以把您的应用程序看成是一组 Portlet操作和状态。然后,在给出了定义完善的状态转换的方法后,您就可以删除Portlet 应用程序中麻烦的控制逻辑代码。本文讨论如何把这种相同的方法应用到 portlet 开发的状态管理中并创建可再用的状态模式以使这个问题得到一般的处理。





回页首


标准的实现方法的问题


考虑简单的基于 MVC 的 Portlet,它从数据库中检索项的列表,并把列表呈现给用户。在用户选择了项后将显示详细的视图,该视图显示的是被选项的详细信息。这个详细视图可能涉及向数据库再次发出请求。这里的要点是需要分析两个不同的控制流。

在第一步中,控制器必须调用业务对象来创建合适的Bean,这些 Bean 被传递给 Java TMServer Page(JSP)以用于主视图中的显示。在第二步中,控制器在用户的请求中检索被选项的指示器,然后调用业务对象来创建合适的Bean,这个 Bean 被传递给不同的 JSP以用于详细视图中的显示。

在 MVC实现中,视图组件显然是不同的;这两种视图需要有两种不同的 JSP。您可能需要访问不同的业务模型组件来生成每个请求所需的Bean。控制器函数需要知道:

  • 调用哪些业务方法
  • 生成哪些Bean
  • 把这些请求对象上的 Bean放在哪里
  • 如何调用合适的 JSP

这就是代码变得复杂的地方。如果使用Servlet 编程,那么您可能选择把它们实现为不同的 Servlet 以使每个合适的Servlet 类的服务方法自然地分隔每个请求的控制器函数。如果您更喜欢使用分派器方式而不是不同的Servlet 实现,那么您必须使用框架(例如 Struts)来实现类似的 MVC 设计。

这是复杂的任务,因为您不能选择使用多个Portlet 类来实现一个 Portlet。您不能直接处理 Portlet类。相同的 Portlet 的相同的服务方法处理返回给 Portlet的所有 HTTP 请求。因此,您需要实现控制代码以:

  • 确定正在请求哪个操作
  • 需要进行哪些处理
  • 使Portlet 处于哪个状态
  • 显示什么东西来返回给用户




回页首


状态模式实现的组件


通过把状态模式应用到 Portlet中,您可以简洁地实现过程控制。您将在状态模式中使用如下组件,如图1 所示。


图 1. 状态模式的交互图。
状态模式的交互图

StateManagerPortlet


这是主要的 Portlet 类。它是 Portlet无关的,一般来说,它将包括所有特定于 Portlet 的代码。该类被用作分派器以支持驻留着Portlet 代码的操作和状态类。StateManagerPortlet 实现了 actionPerformeddoViewdoEditdoHelpdoConfigure 方法。

actionPerformed 方法只是获取操作类的当前的实例,然后分派到它的 actionPerformed 方法。类似地, do 方法获取当前的状态对象,然后分派到它的 perform 方法。所以,StateManagerPortlet 不需要知道当前的 Portlet实现的任何细节,也没有大量用来确定下一步处理什么的 if和检查。只要您熟悉状态与操作之间的流程,处理就能正确地进行,而您不必编写太多的、多余的控制逻辑代码。

ActionClassManager


在 WebSphere Portal V4.2中,该方法把一个 Action 类实例添加到已弃用的 PortelURI中。这个 API 是有用的,因为您可以从 PortletEvent对象中检索您的 Action子类实例,并把该实例作为我们状态转换的一部分分派到它的 actionPerformed 方法中。推荐的 API 将使用字符串而不是 Action 来添加到PortletURI 并且从 Portlet Event 中进行检索。该类提供了从给定的字符串到Action 类的实例的映射。

Action


实现这个接口的类将实现 actionPerformed 方法。该方法执行任何所需的操作以实现操作请求所需的函数。但是,实现的函数特定于正被调用的操作事件。某个操作类的个别的 actionPerformed 方法仅包含该操作的代码。该方法还为进一步的处理设置当前状态。在这个流程过程中,操作被调用后,它进行特定于它的函数的工作,然后为下一次转换设置状态。

InitialStateManager


该类为支持的每个 Portlet方式提供初始的状态。

State

实现这个接口的类需实现 perform 方法。该方法可被 StateManagerPortlet 的 do 方法调用,它包含一般驻留在这些方法中的代码。同样,这些代码特定于它们所在的类的状态从而降低了复杂性。

一般来说,State的 perform 方法将调用 JSP来显示它的结果。用户接口可能让用户来设置 Portlet中的其他操作。JSP使操作类与页面上的每个操作关联。在用户调用其中的一个操作时,StateManagerPortlet的 actionPerformed 方法将调用合适的操作类实例,接着发生了状态转换。状态类并不负责状态管理和转换。

操作和状态


应用状态模式将使实现变得更简洁,还有,随着 Portlet 方式的变化,状态总是被存储。如果您在会话中使用 actionPerformed 方法来检索表单数据并存储它,您将避免在门户页面被刷新时遇到与表单数据未被再次提交有关的任何问题。

有了这种模式后,您就可以容易地确定何时何地从数据源检索数据、何时应该从高速缓存中检索数据。因为在门户页面刷新时不调用 actionPerformed 方法,所以您可以把数据访问代码放在操作状态中并在那里使用高速缓存来存储它们。在刷新门户页面时,为了避免在获取数据时走弯路,状态类可以使用高速缓存中的数据。





回页首


实现细节


请考虑以下情形。您的Portlet在主页面上向用户展示项的列表;用户可以通过选择一个项来获取更详细的信息(展示在另一个页面中)。用户还能够添加、编辑和删除项。在数据库中这些数据是持久的。在这个示例中,项是地址簿中的联系人。

从可视的角度看,以上场景的实现可能需要如下页面:

  • 主视图页面(Main view page)--显示联系人列表和用来选择一个联系人以获取更多信息的选项
  • 详细信息视图页面(Detail view page)--显示被选联系人的详细信息
  • 主编辑页面(Main edit page)--显示联系人列表和用于添加、删除或修改的选项
  • 添加条目页面(Add entry page)--显示表单以获取需添加的联系人信息
  • 修改条目页面(Modify entry page)--显示相似的表单,其中插入了现有的数据以用于修改

在这种情况下,您没有删除条目的显式页面。用户可以从主编辑页面中选择需删除的联系人条目。没有确认页面或成功执行的页面。进行条目删除处理并刷新主编辑页面。若出错,则显示适当的消息,但在正常处理时,没有与该操作关联的视图。

假设您想让有查看 Portlet 的权限的用户使用主视图页面和详细信息视图页面;让有编辑 Portlet 的权限的用户使用添加、编辑和删除功能。在这种情况下,您可在 doView Portlet 方法中控制主视图页面和详细信息视图页面。您可在 doEdit 方法中控制剩余的页面。

使用标准的方法


首先,请考虑 doView 方法。您可以使用标准的 Portlet 编程技术来实现:

public void doView(PortletRequest request, PortletResponse response)
   throws PortletException, IOException {
   String oid = request.getParameter("selectedContact");
   if (oid == null) {
      // Main view processing goes here
      portletContext.include("main_view.jsp", request, response);
   else {
      // Detail view processing goes here
      portletContext.include("detail_view.jsp", request, response);
   }
}
                

然而,您意识到如果您试图在每次刷新时从 HTTP 请求中检索 selectedContact 索引,那么,在您的 Portlet 因门户页面被刷新而被刷新时,即使用户正在使用页面中不同的 Portlet,表单数据也会丢失并且该实现将使 Portlet 返回到主视图。所以,请使用操作侦听器来获取被选的联系人的值并在会话中设置它,以使今后的刷新可获取正确的值并保留适当的页面设置。

现在, doView 代码与下面的代码相似;在操作侦听器中实现的 actionPerformed 方法检索适当的值并把它放在会话中。当然,您还需要在 Portlet URI 中指定操作事件以使侦听器被调用。

public void doView(PortletRequest request, PortletResponse response)
   throws PortletException, IOException {
   PortletSession session = request.getPortletSession(false);
   if (session != null) {
      String oid = (String)session.getAttribute("oid");
      if (oid == null) {
         // Main view processing goes here
         // URI and action listener for showing the contact details
         PortletURI portletURI = response.createURI();
         portletURI.addAction("detailRequest");
         request.setAttribute("detailURI", portletURI.toString());
         portletContext.include("main_view.jsp", request, response);
      else {
         // Detail view processing goes here
         // Get bean for given OID
         session.removeAttribute("oid");
         portletContext.include("detail_view.jsp", request, response);
      }
   }
   else
      response.getWriter().println("You must log in first");
}
public void actionPerformed(ActionEvent event) throws PortletException {
      String actionName = event.getActionString();
      PortletRequest request = event.getRequest();
      // Show the contact detail view. Get the contact oid number
      // of the selected contact and put it on the session object
      if (actionName.equals("detailRequest")) {
         String oid = request.getParameter("selectedContact");
         request.getSession(true).setAttribute("oid", oid);
      }
}
                

在这里,不少代码仅仅被用来管理主视图与详细信息视图之间的页面转换。您还没有编写任何有关应用程序的业务逻辑的代码。在编写这些代码时很容易把错误引入到这些代码中。还有,您需要在各个地方实现组件的组成部分以使过程成功完成;这些组件常常依赖于字符串匹配以使事件与适当的操作绑定或在管理过程控制的代码中设置和读取标志。

如果在控制代码中包括更多的添加、编辑和修改操作,那么控制代码将变得更复杂。以下源代码显示的是该实现的 actionPerformed 方法。这里没有显示 doEdit 方法中相应的代码,这些代码用适当的操作来设置 portletURI、询问事件标志、设置和除去会话中的数据和标志。

public void actionPerformed(ActionEvent event) throws PortletException {
       String actionName = event.getActionString();
       PortletRequest request = event.getRequest();
       PortletSession session = request.getPortletSession();
       //  Show the contact detail view.  Get the contact oid number
       //  of the selected contact and put it on the session object
       if (actionName.equals("detailRequest")) {
          String oid = request.getParameter("selectedContact");
          session.setAttribute("oid", oid);
       }
       // Handle the request to go to the add view
       if (actionName.equals("ADD_REQUEST")) {
          session.setAttribute("NEXT_EDIT_PAGE", "ADD_PAGE");
       }
       // Handle the request to go to the edit view
       if (actionName.equals("EDIT_REQUEST")) {
          session.setAttribute("NEXT_EDIT_PAGE", "MODIFY_PAGE);
       }
       // Handle the reqeust to delete content
       if (actionName.equals("DELETE_REQUEST"))
          ContactsManager.getInstance().(deleteContact(request));
       // Handle the Add Content event
       if (actionName.equals("ADD_CONTACT"))
         ContactsManager.getInstance().addContact(request));
       // Handle the Modify Content event       if (actionName.equals("EDIT_CONTACT"))
       ContactsManager.getInstance().modifyContact(request));
} 
                





回页首


如何用状态模式来使它更改?


如何来改进这个实现呢?首先,请记住这个应用程序表示一组应用程序操作和状态。操作是实现操作接口的类,这个类实际完成某个应用程序任务或操作的处理。这就是目前存在于 actionListener 类的 actionPerformed 方法中的应用程序代码的一部分;只有这部分特定于单个操作事件。

状态是实现状态接口的类,这个类表示由于应用了操作而产生的 Portlet 的效果。一般来说,这个类有可视组件。

这样的应用程序往往包括如下内容:

操作:

  • 显示主页面(Show Main Page)
  • 显示详细信息页面(Show Detail Page)
  • 显示主编辑页面(Show Main Edit Page)
  • 显示添加联系人页面(Show Add Contact Page)
  • 添加联系人(Add a Contact)
  • 显示修改联系人页面(Show Modify Contact Page)
  • 修改联系人(Modify a Contact)
  • 删除联系人(Delete a Contact)

状态:

  • 主视图页面(Main View Page)
  • 详细信息视图页面(Detail View Page)
  • 主编辑页面(Main Edit Page)
  • 添加联系人页面(Add Contact Page)
  • 修改联系人页面(Modify Contact Page)

现在来确定这个应用程序可用的状态转换。通过把适当的操作应用到当前状态(这将导致应用程序进入另一个特定状态)来管理转换。

2. 状态图
状态图

在理解了这些 Portlet 知识后,您可以使用状态模式来创建这个应用程序。在构建应用程序时,您可以把公共的结构用于状态转换,这一结构使您可以删除过多的 if 语句(这些语句使用字符串匹配和多个被设置的标志。)。





回页首


状态模式如何工作?


下面的代码显示了 StateManagerPortlet 类中的 doView 方法的简单处理。其他的 do 方法与 doView 方法相似。您可以容易地把这些处理放到服务方法中。在这种情况下,您可以通过把 portlet 方式用作键来从会话中查找状态对象。之所以这样显示的原因是您一般在 do 方法中编写代码而您更熟悉这种比较。还有,您可以把缺省状态抽取到属性文件或另一个初始化参数中,以除去连到这个类的连接。

Public void doView(PortletRequest request, PortletResponse response)
   throws PortletException, IOException {
   // Ensure we are logged in
   PortletSession session = request.getPortletSession();
   User user = request.getUser();
   if (session == null) || user == null)
      throw new MessageException("Login to the portal first");
   //  Get the portlet state object from session, if not there get the initial
   //  state for the mode.
   State nextState = (State) session.getAttribute(request.getMode().toString());
   if (nextState == null)
      nextState = InitialStateManager.getInitialState(request.getMode());
   // Dispatch to state handler
   nextState.performView(request, response,
      getPortletConfig().getContext());
}
                

下面的代码显示了 StateManagerPortlet 类的 actionPerformed 方法中的简单处理。同样,这个方法仅仅获取由操作名称字符串确定的操作类实例并调用它的 actionPerformed 方法。

Public void actionPerformed(ActionEvent event) throws PortletException {
   // Execute the perform action method for the event
   PortletContext portletContext = getPortletConfig().getContext();
   PortletRequest request = event.getRequest();
   //  Get the action handler class and dispatch
   String actionClassName = event.getActionString();
   Action action = ActionClassManager.getAction(actionClassName);
   //  Dispatch to that class event handler; register the next state
   action.actionPerformed(request, portletContext);
   action.setState(request);
}
                

然后,操作类的 actionPerformed 方法执行正常的处理并使会话处于正确的状态以使流程控制正确地执行下去。请记住,一般来说,操作类和状态类没有或只有很少的字段。所以,把它们添加到会话对象的开销是最小的。

因为您不必为了在 Portlet do 方法中实现这个模式而作任何更改,所以您不会显示状态类执行方法的等价方法。但是,为了包括用于状态的刷新方法,您可以扩展这里的模式。刷新方法将逻辑地抽取负责 Portlet 数据检索的应用程序代码,以使您可以分离所需的逻辑以确定 Portlet 刷新导致刷新源数据还是导致从高速缓存中检索源数据。

在 StateManagerPortlet 要求状态类把 HTML 片断写给响应(或调用 JSP 来这样做)前,StateManagerPortlet 可以要求状态类刷新自己。在刷新方法中,您可以包括刷新或再次检索数据的逻辑以用于显示。

例如,显示变化的数据的 Portlet 可能需要在每次刷新屏幕时重访数据源。您可以把这样做的逻辑包括在刷新方法中。另一方面,显示稳定的信息的 Portlet 很可能把数据访问逻辑放在初始状态惰性初始化逻辑中并在高速缓存中保存结果。用于这种类型的稳定数据状态的刷新方法将不做任何事。

因为刷新方法中的业务逻辑可能导致转换到不同的状态(包括错误状态),所以您可以扩展状态模式以支持状态之间的状态转换。





回页首


结束语


随着 WebSphere Portal 成为企业门户应用程序的首选平台,用于 Portlet 开发的定义完善的框架和模式正变得越来越重要。本文讨论了对管理 Portlet 中复杂的应用程序页面流程的模式的需求,这将实现高效的处理并使您得到简洁的代码并容易地调试、维护和改进应用程序。



参考资料



作者简介

Tim Hanis 是 WebSphere Portal 方面的高级软件工程师,他在 IBM Research Triangle Park 实验室(位于北卡罗莱纳州的罗利)工作。


Skyler Thomas 是 IBM WebSphere Portal 方面的执行顾问,他在 IBM Research Triangle Park 实验室(位于北卡罗莱纳州的罗利)工作。


Chris Gerken 是 IBM WebSphere Portal 方面的高级软件工程师,他在 IBM Austin 实验室(位于得克萨斯州的奥斯丁)工作。




对本文的评价

太差! (1)
需提高 (2)
一般;尚可 (3)
好文章 (4)
真棒!(5)

建议?




回页首


IBM 公司保留在 developerWorks 网站上发表的内容的著作权。未经IBM公司或原始作者的书面明确许可,请勿转载。如果您希望转载,请通过 提交转载请求表单 联系我们的编辑团队。
    关于 IBM 隐私条约 联系 IBM 使用条款