引言
IBM® WebSphere® Portal(以下简称为 Portal)正在迅速成为构建企业类应用程序的标准平台。为了超越简单的 HelloWorld portlet,开发者已开始为构建 portlet 建立标准的框架。许多客户想构建显示极为复杂的行为的 portlet。目前,J2EE™ 模型提供处理复杂行为的定义完善的框架。开发者可以使用 Struts、EAD4J、Turbine 和 JSP Model-2 模型-视图-控制器(Model-View-Controller,MVC)策略来处理 servlet 世界中复杂的页面流程。Portal 本身提供了页面级别的导航框架。然而,开发者目前需要处理个别的 portlet 的行为。在创建更为复杂的 portlet 时,开发者必须处理 portlet 中的视图浏览,必须考虑如何高效地使用操作事件和侦听器来响应用户操作事件(例如单击按钮或链接)。
portlet 开发指南和示例实现使您能够很好地理解 portlet API。然而,复杂过程的控制的实现超出了 API 的范围。如果您没有设计完善的方法来解决如何最佳地实现控制逻辑,那么您最终创建的 portlet 的代码中有相当多的一部分专用于考虑用户请求的目的。
这些代码不仅是重复的,它们还使 portlet 更难被读懂,因为您需要费很大力气去理解控制逻辑以了解 portlet 正在进行的实际工作。控制逻辑也容易出现错误,因为它在 portlet 方法中依赖于字符串名称匹配以使事件与侦听器绑定、使侦听器操作与行为绑定。它还使业务逻辑变得更复杂,因为控制功能的实现可能类似于业务逻辑。
为了解决控制逻辑的这些问题,您可以把应用程序看成是一组 portlet 操作和状态。然后,在给出了定义完善的状态过渡的方法后,您就可以删除 portlet 应用程序中麻烦的控制逻辑代码。本文讨论如何把这种相同的方法应用到 portlet 开发的状态管理中并创建可再用的状态模式以使这个问题得到一般的处理。您将发现把状态模式应用于 portlet 会产生一个更干净的 portlet 实现。
标准的实现方法的问题
简单的基于 MVC 的 portlet 可提供项的列表,这些项是从数据库中被检索而来,然后被发送到主视图供用户选择。当用户选择了项后将显示详细的视图,该视图显示的是被选项的详细信息。这可能涉及向数据库再次发出请求。这里的要点是现在需要分析两个不同的控制流程。
在第一种情形中,控制器必须调用业务对象来创建合适的 bean,这些 bean 被传递给 JSP 以用于主视图中的显示。在第二种情形中,您必须在用户的请求中检索被选项的指示器。您可以调用业务对象来创建合适的 bean,然后,这个 bean 被传递给不同的 JSP 以用于详细视图中的显示。
在 MVC 实现中,视图组件显然是不同的;这些视图需要有两种不同的 JSP。您可能需要访问不同的业务模型组件来生成每个请求所需的 bean。控制器函数需要知道调用哪些业务方法、生成哪些 bean、把这些请求对象上的 bean 放在哪里以及如何调用合适的 JSP。
这就是代码变得复杂的地方。如果使用 servlet 编程,那么您可能选择把它们实现为不同的 servlet 以使每个合适的 servlet 类的服务方法自然地分隔每个请求的控制器函数。如果您更喜欢使用分派器方式而不是不同的 servlet 实现,那么您必须使用框架(例如 Struts)来实现类似的 MVC 设计。
这是复杂的任务,因为您不能选择使用多个 portlet 类来实现一个 portlet。您不能直接处理 portlet 类。相同的 portlet 的相同的服务方法处理返回给 portlet 的所有 HTTP 请求。因此,您需要实现控制代码以确定正在请求哪个操作、需要进行哪些处理、使 portlet 处于哪个状态以及显示什么东西来返回给用户。
状态模式实现的组件
通过把状态模式应用到 portlet 中,开发者可以干净地实现过程控制。状态模式使用以下组件:
StateManagerPortlet。这是主要的 portlet 类。它是 portlet 无关的,一般来说,它也是您编写所有特定于 portlet 的代码的地方。该类被用作分派器以支持驻留着 portlet 代码的操作和状态类。StateManagerPortlet 实现了 actionPerformed 方法,也实现了 doView、doEdit、doHelp 和 doConfigure 方法。actionPerformed 方法只是获取操作类的当前的实例,然后分派到它的 actionPerformed 方法。类似地,do 方法获取当前的状态对象,然后分派到它的 perform 方法。所以,StateManagerPortlet 不需要知道当前的 portlet 实现的任何细节,也没有大量用来确定下一步处理什么的 if 和检查。只要您熟悉状态与操作之间的流程,处理就能正确地进行,而您不必编写太多的、多余的控制逻辑代码。 Action。实现这个接口的类将实现 actionPerformed 方法。该方法执行任何所需的操作以实现操作请求所需的函数。但是,实现的函数特定于正被调用的操作事件。某个操作类的个别的 actionPerformed 方法仅包含该操作的代码。该方法还为进一步的处理设置当前状态。在这个流程过程中,操作被调用后,它进行特定于它的函数的工作,然后为下一次过渡设置状态。 State。实现这个接口的类需实现 perform 方法。该方法可被 StateManagerPortlet 的 do 方法调用,它包含一般驻留在这些方法中的代码。同样,这些代码特定于它们所在的类的状态从而降低了复杂性。一般来说,State 的 perform 方法将调用 JSP 来显示它的结果。UI 可能让用户来设置 portlet 中的其他操作。JSP 使操作类与页面上的每个操作关联。当用户调用其中的一个操作时,StateManagerPortlet 的 actionPerformed 方法将调用合适的操作类实例,接着发生了状态过渡。状态类并不负责状态管理和过渡。 StateURI 定制标记。该类提供 JSP 所使用的定制标记。前面已提到,JSP 使操作实例与页面上的操作关联。这种关联是通过生成的 PortletURI 来完成的。在给出操作类名后,该定制标记提供的函数可生成 URI。您可以删除门户代码中的 URI 创建代码。有了这个标记和操作状态结构后,您不必再提供管理 portlet 中状态过渡的代码。 操作和状态
前面已提到,通过把状态模式应用到 portlet 中,portlet 的实现将变得更干净。还有,随着 portlet 方式的变化,状态总是被存储。如果您在会话中使用 actionPerformed 方法来检索表单数据并存储它,那么您可以避免在门户页面被刷新时遇到与表单数据未被再次提交(repost)有关的任何问题。有了这种模式后,您可以容易地确定何时何地从数据源检索数据、何时应该从高速缓存中检索数据。由于在门户页面刷新时不调用 actionPerformed 方法,所以您可以把数据访问代码放在操作状态中并在那里使用高速缓存来存储它们。在刷新门户页面时,为了避免在获取数据时走弯路,状态类可以使用高速缓存中的数据。
实现细节
请考虑以下情形。您的 portlet 在主页面上向用户展示项的列表;用户可以通过选择一个项来获取(展示在另一个页面中的)更详细的信息。用户还能够添加、编辑和删除项。在数据库中这些数据是持久的。在这个示例中,项是地址簿中的联系方式。
从可视的角度看,以上情形的实现可能需要以下页面:
主视图页面— 显示联系方式列表和用来选择一个联系方式以获取更多信息的选项 详细信息视图页面— 显示被选的联系方式的信息 主编辑页面— 显示联系方式列表和用于添加、删除或修改的选项 添加条目页面 — 显示表单以获取需添加的联系方式信息 修改条目页面— 显示相似的表单,其中插入了现有的数据以用于修改
在这种情况下,您没有删除条目的显式页面。其行为是这样的:用户可以从主编辑页面中选择需删除的联系方式条目。没有确认页面或成功执行的页面。进行条目删除处理并刷新主编辑页面。若出错,则显示适当的消息,但在正常处理时,没有与该操作关联的视图。
假设您想让有查看 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();
PortletAction portletAction =
new DefaultPortletAction("detailRequest");
portletURI.addAction(portletAction);
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 {
DefaultPortletAction portletAction =
(DefaultPortletAction) event.getAction();
if (portletAction instanceof DefaultPortletAction) {
DefaultPortletAction action = (DefaultPortletAction) portletAction;
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 (action.getName().equals("detailRequest")) {
String oid = request.getParameter("selectedContact");
request.getSession(true).setAttribute("oid", oid);
}
}
}
在这里,不少代码仅仅被用来管理主视图与详细信息视图之间的页面过渡。您还没有编写任何有关应用程序的业务逻辑的代码。在编写这些代码时很容易把错误引入到这些代码中。还有,您需要在各个地方实现组件的组成部分以使过程成功完成;这些组件常常依赖于字符串匹配以使事件与适当的操作绑定或在管理过程控制的代码中设置和读取标志。
如果在控制代码中包括更多的添加、编辑和修改操作,那么控制代码将变得更复杂。以下源代码显示的是该实现的 actionPerformed 方法。这里没有显示 doEdit 方法中相应的代码,这些代码用适当的操作来设置 portletURI、询问事件标志、设置和除去会话中的数据和标志。
public void actionPerformed(ActionEvent event) throws PortletException {
DefaultPortletAction portletAction =
(DefaultPortletAction) event.getAction();
if (portletAction instanceof DefaultPortletAction) {
DefaultPortletAction action = (DefaultPortletAction) portletAction;
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 (action.getName()Equals("detailRequest")) {
String oid = request.getParameter("selectedContact");
session.setAttribute("oid", oid);
}
// Handle the request to go to the add view
if (action.getName()Equals("ADD_REQUEST") {
session.setAttribute("NEXT_EDIT_PAGE" "ADD_PAGE";
}
// Handle the request to go to the edit view
if (action.getName()Equals("EDIT_REQUEST") {
session.setAttribute("NEXT_EDIT_PAGE" "MODIFY_PAGE);
}
// Handle the reqeust to delete content
if (action.getName()Equals("DELETE_REQUEST")
ContactsManager.getInstance().(deleteContact(request));
// Handle the Add Content event
if (action.getName()Equals("ADD_CONTACT")
ContactsManager.getInstance().addContact(request));
// Handle the Modify Content event
if (action.getName()Equals("EDIT_CONTACT")
ContactsManager.getInstance().modifyContact(request));
}
}
如何用状态模式来使它更改?
如何来改进这个实现呢?首先,请记住这个应用程序表示一组应用程序操作和状态。操作是实现操作接口的类,这个类实际完成某个应用程序任务或操作的处理。这就是目前存在于 actionListener 类的 actionPerformed 方法中的应用程序代码的一部分;只有这部分特定于单个操作事件。
状态是实现状态接口的类,这个类表示由于应用了操作而产生的 portlet 的效果。一般来说,这个类有可视组件。
这样的应用程序往往包括以下这些东西:
操作:
显示主页面 显示详细信息页面 显示主编辑页面 显示添加联系方式页面 添加联系方式 显示修改联系方式页面 修改联系方式 删除联系方式 状态:
主视图页面 详细信息视图页面 主编辑页面 添加联系方式页面 修改联系方式页面 现在来确定这个应用程序的可用状态过渡。通过把适当的操作应用到当前状态(这将导致应用程序进入另一个特定状态)来管理过渡。
图 1. 状态图
在理解了这些 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();
if (session == null)
throw new MessageException("Login to the portal first");
// Get the state request from session; main view as default
State nextState = (State) session.getAttribute(Action.VIEW_STATE);
if (nextState == null)
nextState = new MainViewState();
// 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();
Action action = (Action) event.getAction();
action.actionPerformed(request, portletContext);
}
然后,操作类的 actionPerformed 方法执行正常的处理并使会话处于正确的状态以使流程控制正确地执行下去。请记住,一般来说,操作类和状态类的字段很少甚至于没有字段。所以,把它们添加到会话对象的开销是最小的。以下是操作类的 actionPerformed 方法的代码,这是一段添加联系方式的代码:
public void actionPerformed(PortletRequest request,
PortletContext portletContext) {
// Add the contact
ContactHelper contactHelper = ContactHelper.getInstance();
contactHelper.addContact(request);
// Set next portlet state ?Main Edit
State nextState = new MainEditState();
PortletSession session = request.getPortletSession();
session.setAttribute(EDIT_STATE, nextState);
}
您不必为了在 portlet do 方法中实现这个模式而作任何更改,所以您不显示等同的状态类 perform 方法。但是,为了包括用于状态的刷新方法,您可以扩展这里的模式。刷新方法将逻辑地抽取负责 portlet 数据检索的应用程序代码,以使您可以分离所需的逻辑以确定 portlet 刷新导致刷新源数据还是导致从高速缓存中检索源数据。
在 StateManagerPortlet 要求状态类把 HTML 片断写给响应(或调用 JSP 来这样做)前,StateManagerPortlet 可以要求状态类刷新自己。在刷新方法中,您可以包括刷新或再次检索数据的逻辑以用于显示。例如,显示变化的数据的 portlet 可能需要在每次刷新屏幕时重访数据源。您可以把这样做的逻辑包括在刷新方法中。另一方面,显示稳定的信息的 portlet 很可能把数据访问逻辑放在初始状态惰性初始化逻辑中并在高速缓存中存储结果。用于这种类型的稳定数据状态的刷新方法不做任何事。
因为刷新方法中的业务逻辑可能导致过渡到不同的状态(包括错误状态),所以您可以扩展状态模式以支持状态之间的状态过渡。
结束语
随着 WebSphere Portal 成为企业门户应用程序的首选平台,用于 portlet 开发的定义完善的框架和模式正变得越来越重要。本文讨论了对管理 portlet 中复杂的应用程序页面流程的模式的需求,这将实现高效的处理并使您得到干净的代码并容易地调试、维护和改进应用程序。