Struts 和 JavaServer Faces 等 Web 框架只关注 Web 应用程序中的前进导航。在本文中,Maurizio Albari 介绍了一种改善 Web 应用程序后退导航的框架,这是通过保存已访问 Web 页面的服务器端导航历史和已访问 Web 页面的有名序列(即 Webflows)来实现的。通过该框架,还可以使用服务器端导航历史自动清理 HTTP 会话,从而提高应用程序性能。更好的是,对于前进导航,您仍可以使用自己喜欢的 Web 框架。
WebFlow Navigation Manager 框架(我将其简称为 WFNM)是一种 Web 框架,它关注当前框架,例如 Struts 或 JavaServer Faces (参阅 参考资料 获取关于它们的链接)不能管理的问题。该框架并没有另起炉灶,它与其他框架是互补的,并且可以与它们一起使用,甚至可以用于基于 servlet 和 JSP 页面的应用程序。WFNM 是在 Lesser GNU Public License 下发布的,因此包含 WFNM 代码的二进制文件可以在任何商业 Web 应用程序中使用。
WFNM 框架为应用程序提供以下两种主要的功能:
- 改善 Web 应用程序的后退导航
- 自动清理 HTTP 会话
通过引入 Webflow 的概念,即已访问 Web 页面的有名序列,可改善后退导航。实际上,后退导航在两个不同级别上得到改善:
- 页面级:该框架保存已访问 Web 页面的服务器端历史,以便于开发将用户带回之前页面(或重新装载当前页面)的服务器端动作。
- Webflow 级:这种服务器端导航历史还便于用户回到前面的 Webflow,甚至根据提供的名称回到之前访问的 Webflow。
WFNM 还利用上述功能提供一种自动会话清理机制,该机制使开发人员可以更安全地使用 HTTP 会话。如果代码不恰当地使用 HTTP 会话,则会导致对应用程序和物理内存的不适当的消耗。WFNM 提供的自动会话清理机制使开发人员可以将 HTTP 会话划分成不同的作用域,并为 Web 应用程序引入一种类似于 Java 虚拟机的垃圾收集器机制的机制。
没有变量作用域概念的编程语言,或者没有垃圾收集器的 Java 虚拟机,都是难以想象的。然而,如今开发出来的大多数 Web 应用程序都没有那样的概念。实际上,WFNM 框架正尝试解决这些问题。现在,我将更详细地展示它的工作原理。
要更好地理解 WFNM 背后的动机,可以看看下面的例子。假设有一个具有如图 1 所示导航的 Web 应用程序。该应用程序由 4 个页面组成:b1、b2、b3 和 b4。
通常,可以通过一个 XML 配置文件,对 Struts 或 JavaServer Faces 等框架进行配置,以管理这些页面之间的前进导航。但是,这些框架通常不能处理动态的后退导航。
对于页面 b2 和 b3,惟一可能的前页是 b1,所以可以使用基于配置文件的方法从这些页面向后导航。但是,对于页面 b4 呢?b4 有两个可能的前页 b2 和 b3,这取决于用户采用哪条前进路径到达页面 b4。在本场景中,服务器端动作可以使用已访问页面的服务器端导航历史帮助用户返回到正确的前页。
假设有一个 Java factory,它可以根据类似于图 1 的会话计算前页:
String url = DynamicFactory.getPreviousPage(session); |
使用该信息可以使 Web 框架(如果使用了的话)返回到前页。但是,如何实现具有这种功能的 Java factory 呢?目前这点还不用考虑。现在假设您已经有了一个这样的 factory:我将在本文的后面讨论如何实现它。
Web 应用程序通常不是一个大单块。它们由多个部分组成,每个部分有一种特定的功能。
通常,一个 Web 应用程序的某些部分对于很多用例是公共的。例如,一个 Java 方法调用另一个方法(该方法本身可以被其他方法调用),当它结束时,将控制返回给调用方法。类似地,在 Web 应用程序中,不同的页面可以调用应用程序的一部分。当被调用的功能运行完毕时,控制返回到调用者页面。
如上所示,我将 Web 应用程序的一个部分(即已访问 Web 页面的一个序列)称作一个Webflow。每个 Webflow 有一个名称。
现在,我可以介绍一个更复杂的场景,该场景包含 3 个 Webflows:B(黄色页面 b1、b2、b3 和 b4)、C(橙色页面 c1、c2、c3 和 c4)以及 D(绿色页面 d1、d2 和 d3),如图 2 所示。
前面提到的 Web 页面的导航问题对于 Webflows 仍然有效:如果页面 d3(在 Webflow D)上的用户想返回到前一个 Webflow(即前一个 Webflow 中最近访问的页面),它可能是 Webflow B(页面 b4),也可能是 Webflow C(页面 c4),那么该怎么办呢?
显然,在这个场景中,服务器端动作可以使用已访问 Webflow 的服务器端导航历史来返回到前一个 Webflow(比如前一个 Webflow 中最近访问的页面),甚至可以根据给定的名称返回到前面特定的 Webflow。
例如,假设有一个 Java factory,它可以根据会话计算前一个 Webflow,如清单 2 所示,或者根据之前访问的有名称的 Webflow,如清单 3 所示。
String url = DynamicFactory.getPreviousWebflow(session); |
清单 3. 获得前一个有名称的 Webflow
String url = DynamicFactory.getPreviousWebflow(session,"A"); |
在本场景中,可以使您的 Web 框架(如果使用了的话)返回到前一个 Webflow 中最近访问的页面。
在 Web 应用程序的导航期间,任何时候控制器都可以获得当前 Webflow 的名称,如清单 4 所示:
String webflowName = DynamicFactory.getCurrentWebflowName(session) |
控制器还可以发现一个 Webflow 在用户导航期间是否被访问过,如清单 5 所示:
boolean visited = DynamicFactory.isWebflowVisited (session,"B"); |
要实现以上功能,需要一种简单的方式来定义 Webflows。换句话说,如何为一个已访问 Web 页面序列指定名称?
第 3 版的 Enterprise JavaBeans 规范使用注释将元数据添加到 Java 源代码中,您可以从中得到灵感。这里不是使用一个单独的 XML 文件(当该文件发生变化时,它可能与相关的代码失去同步),而是用某种 Web 标记来表明一个页面是一个 Webflow 中的一项。清单 6 显示了这是如何实现的。
<wfnm:webflow name="D"/> |
在 图 2 显示的例子中,这个标记被同时应用到页面 d1 和 d2 上,因为它们都是 Webflow D 的一部分。如果页面 b1 和 c1 是 Web 应用程序的入口点,或者可以从前一个假定的 Webflow A 中调用它们,那么必须将类似的标记应用到页面 b1 和 c1(您不需要将这些标记添加到每个页面,虽然您可以这么做)。
很多 Web 应用程序都有一个登录页面。有时候,这个页面就是第一个页面,它保护着整个 Web 应用程序。此外,很多 Web 应用程序还有主页。
在这个例子中,登录页面被称作 a1,主页被称作 a2。它们都是名为 A 的 Webflow 的一部分。将此与 图 2 中显示的例子相结合,现在您有了一个完整的 Web 应用程序,如图 3 所示。
用户打开一个浏览器,输入一个 URL,并到达登录页面 a1。如果验证成功执行,则用户到达主页 a2。该 Web 应用程序允许两个用例:一种是从页面 b1 开始,一种是从页面 c1 开始。
每个用例由两个步骤组成:第一个用例需要 Webflow B 和 Webflow D,第二个用例需要 Webflows C 和 D;Webflow D 是这两个用例共用的。
在这个场景中,在 Webflow D 的导航期间,通过使用 DynamicFactory
很容易返回到之前的 Webflow。
为了实现已访问页面和 Webflow 的服务器端导航历史,可以使用一个栈中栈 数据结构:一个已访问的 Webflow 栈,每个 Webflow 包含一个已访问页面的栈。
例如,假设 Web 应用程序的导航遵循以下路径:
a1->a2->b1->b2->b4->d1->d3 |
在这种情况下,栈中栈数据结构的状态类似于图 4。
大多数 Web 应用程序都类似于前面展示的场景。导航从一个主页(a2)开始,遵循一个用例,然后提供返回到主页的导航。通常,企业信息系统(比如关系数据库)的更新会在用例期间被持久化,所以当返回到主页时,放在 HTTP 会话中的对象通常不再有用。
但是,当 HTTP 会话中的对象没有存在的必要时(用例结束和返回主页之前),有多少开发人员记得删除这些对象呢?
在对象不再有用时自动清理 HTTP 会话的结构化方法可以防止使用过期的对象。它还为开发人员提供了更多的自由,使他们可以将更多的对象放入到 HTTP 会话中,因为他们确信一个框架会自动删除不再有用的对象。这些添加的对象有可能改善用户体验。
HTTP 会话清理可以提高 Web 应用程序的性能,因为它更少地使用 HTTP 会话,而应用服务器可以节省一项重要的资源:内存。与网络环境中防火墙保证通信安全的方式相同,自动会话清理框架保证需要对象时才将它保留在 HTTP 会话中。使用一种简单的默认策略,并逐例处理异常,应该就可以了。
但是,如何实现具有以上特性的框架呢?假设在导航到栈中栈数据结构的一个特定元素期间(即导航到一个页面或 Webflow,如 图 4 所示),您已经能够关联放入 HTTP 会话中的每个对象。与一个元素相关联的对象为该元素所有。
算法如下:当从栈中删除一个对象的所有者时(对于页面是内部栈,对于 Webflow 是外部栈),将自动删除该对象。这意味着当用户从一个页面返回到前一个页面时,将删除该页面所拥有的对象,而当用户从一个 Webflow 返回到前一个 Webflow 时,将删除该 Webflow 所拥有的对象。根据用于将 HTTP 会话中的每个对象关联到栈中栈数据结构的一个元素的策略,可以为这些对象定义一种作用域。
为了更好地理解所有权关系的概念,可以将栈中栈数据结构与每个元素所拥有的对象结合起来。例如,假设您正在使用 Struts,Web 页面在 /jsp/struts 目录中(相对于文档根目录)。如果当前 Webflow 拥有在导航期间放入到 HTTP 会话中的对象,那么栈中栈数据结构则如图 5 所示。
如果当前页面拥有导航期间放入到 HTTP 会话中的对象,那么栈中栈数据结构看上去如图 6 所示。
当选择将一个对象关联到一个页面或 webflow 的策略时,重要的是定义它的作用域以及将其从 HTTP 会话中删除的时间。这种策略必须足够灵活,能处理所有可能的情况,并减少配置工作。您需要在三个级别上配置框架:
- 总体框架:框架有一个默认的配置,它对于所有 Webflow 和所有放入到 HTTP 会话中的对象都是有效的。使用一个集中的配置 singleton 来实现它,对于这个 singleton,可以使用一个属性文件进行配置。可以将清单 7 所示的一行插入到这个文件中。
清单 7. 设置默认所有权关系
net.sf.wfnm.DEFAULT_OWNERSHIP=...
- 单个 Webflow:如果 Webflow 的默认配置不适合于一个特定的 Webflow,那么可能需要单独改变针对那个 Webflow 的策略。例如,在定义该 Webflow 的标记中指定所有权关系,如清单 8 所示。
清单 8. 为特定 Webflow 设置所有权关系
<wfnm:webflow name="D" owner="..."/>
- 单个对象:如果一个对象被放入到 HTTP 会话中,并拥有一个特定的键,那么将它关联到特定的所有者。可以通过使用一个适当的标记来完成该项工作,如清单 9 所示。
清单 9. 设置对象的所有权关系
<wfnm:owner key="objectKey" owner="..."/>
在继续描述 owner
属性可用的值之前,我想讨论一下命名惯例。由于栈中第一个 Webflow 通常是主页,而其他 Webflow 则是不同的用例,它们最终都返回到第一个 Webflow,我将第一个 Webflow 称作全局 Webflow,而将随后的 Webflow 称作工作 Webflow。以此为基础,考虑 owner
属性可能的值:
page
通过导航到达的当前页面拥有这个对象;当用户返回到前一个页面时,它将被删除。webflow
:通过导航到达的当前 Webflow 拥有这个对象;当用户返回到前一个 Webflow 时,它将被删除。previous
:前一个 Webflow 拥有这个对象;当那前一个 Webflow 被从栈中删除时,它将被删除。working
:工作 Webflow 拥有这个对象;当工作 Webflow 结束,用户返回全局 Webflow 时,它将被删除。global
:全局 Webflow 拥有这个对象;当离开框架时(例如遇到一个<wfnm:reset/>
),它将被删除。none
:没有 Webflow 拥有这个对象;它永远不会被框架自动删除。
例如,可以在一个属性文件中用一行全局地配置框架,如清单 10 所示。
net.sf.wfnm.DEFAULT_OWNERSHIP=webflow |
如果这个默认配置不适合一个特定的 Webflow,那么可以用一个不同的所有权关系定义这个 Webflow,如清单 11 所示。
清单 11. 将 Webflow 所有权关系设置为 previous
<wfnm:webflow name="D" owner="previous"/> |
如果这个默认配置不适合一个特定的对象,那么可以为放入到 HTTP 会话中的对象定义所有权关系,如清单 12 所示。
<wfnm:owner key="objectKey" owner="global"/> |
我在前面假设您已经能够魔术般地 实现以下特性:
- 跟踪已访问的 Web 页面和已访问的 Webflow。
- 将每个对象关联到栈中栈数据结构中的一个元素。
对于第一个问题,假设您有一个 Java factory,则可以像清单 13 那样使用它。
NotifyFactory.notifyPage(request); |
在此,request
是一个 HttpServletRequest
。
如果有一个可用的 J2EE 1.4 环境,那么可以使用这个 factory 和一个简单的过滤器来跟踪已访问的页面和 Webflow。
如果 Web 页面在一个 /jsp 目录中(相对于文档根目录),那么可以在 web.xml 文件中配置过滤器,如清单 15 所示。
清单 15. PageNotifierFilter XML 配置
<filter> <filter-name>wfnmPageNotifierFilter</filter-name> <filter-class>net.sf.wfnm.Web.PageNotifierFilter</filter-class> </filter> <filter-mapping> <filter-name>wfnmPageNotifierFilter</filter-name> <url-pattern>/jsp/*</url-pattern> <dispatcher>FORWARD</dispatcher> <dispatcher>REQUEST</dispatcher> </filter-mapping> |
由于过滤器同时处理 request
和 forward
的功能在 J2EE 1.4 中才引入,因此更早版本的 J2EE 需要一个不同的解决方案。对于这些版本,需要在每个 JSP 页面的末尾添加一个标记(或者,如果使用了像 Tiles 这样的模板机制,也可以将标记添加到一个公共的模板中)。可以将清单 16 中的标记应用到每个页面。
<wfnm:notify/> |
该标记的实现可以使用前面的 NotifyFactory
,如清单 17 所示。
public class NotifyTag extends TagSupport { ... public int doStartTag() throws JspException { NotifyFactory.notifyPage((HttpServletRequest) pageContext.getRequest()); return SKIP_BODY; } } |
过滤器还可以解决第二个问题 —— 将每个对象关联到栈中栈数据结构中的一个元素。假设有一个用于 HTTP 会话的包装器 HttpSessionWrapper
。这个包装器通过将方法托管给一个初始的会话(该会话可传入到构造函数中),实现了 HttpSession
接口。setAttribute(String,Object)
和 removeAttribute(String,Object)
方法的实现还可以通知框架,一个属性已经添加到 HTTP 会话中或已被从中删除。假设 HttpSessionWrapper
含有如清单 18 所示的静态方法。
public static HttpSession wrapItIfNecessary(HttpSession session) { if (session instanceof HttpSessionWrapper) { return session; } else { return new HttpSessionWrapper(session); } } |
可以使用上面的类来定义一个定制的扩展 javax.servlet.http.HttpServletRequestWrapper
的 HttpServletRequestWrapper
。
通过用这个包装器替代原始请求,当调用一个 getSession(...)
方法时,返回的类能够在向初始 HTTP 会话添加或从中删除对象时通知框架。
为了理解自动清理 HTTP 会话的重要性,使用前面的例子运行一个性能测试。
对于每个前进导航步骤,假设一个大小为 1 KB 的 bean 被存储到 HTTP 会话中。重复该测试两次,一次使用自动会话清理,另一次则不使用。
测试导航由 4 个回路组成:
回路 | 导航路径 |
1 | a1->a2->b1->b2->b4->d1->d3->a2 |
2 | b1->b3->b4->d1->d3->a2 |
3 | c1->c2->c4->d2->d3->a2 |
4 | c1->c3->c4->d2->d3->a2->a1 |
性能结果如图 7 所示:红色虚线是不使用自动会话清理时会话的大小;蓝色实线是使用自动会话清理时会话的大小。
开始学习 WFNM 时,最好在项目的 Web 站点(参见 参考资料)上查看和框架本身一起提供的示例:它就是本文描述的完整的场景。这个示例非常简明,可以作为开发其他 Web 应用程序的模板。该示例包含前面展示的两种不同版本的完整场景:第一个场景是使用 WFNM 和 Struts 实现的,第二个场景是使用 WFNM 和 JavaServer Faces 实现的。
查看了 Web 站点上的 demo 后,下载 wfnm-sample-<version>.zip,解压该文件,将 build 目录中的文件 wfnm-sample.war 部署到 J2EE 1.4 Web 容器中。现在,可以体验 WFNM 示例 Web 应用程序了。
查看 src 目录下的 Java 源文件,特别是 net.sf.wfnm.sample.struts.StrutsDynamicFactory
类和 net.sf.wfnm.sample.jsf.FacesDynamicFactory
类。这两个类使用 WFNM 框架的 DynamicFactory
类。现在,看看 Web 动作如何使用前面的 factory 在 Web 应用程序中后退导航。此外,您需要研究 net.sf.wfnm.sample.jsf.HomeBean
类和 net.sf.wfnm.sample.struts.HomeAction
类如何设置 WFNM 框架的默认配置。
接下来,解压缩 wfnm-sample.war,并查看 WEB-INF 目录中的 web.xml。您将看到 WFNM 框架需要的过滤器。查看 wfnm-sample.war 的 jsp 目录中的 JSP 页面,看看如何用 WFNM 标记定义 Webflow。
如果您打算用 Struts 或 JavaServer Faces 开发一个 Web 应用程序,那么可以用这个 WFNM 示例作为模板。web.xml 包含用于这两种框架的配置元素。无论是使用其中一种框架,还是采用自己的方式,都需要删除不需要的元素。
显然,除了 WEB-INF/lib 目录中的 wfnm.tld 文件和 wfnm.jar 外,您需要从示例中删除不需要的资源。如果使用 Struts 或 JavaServer Faces,需要将适当的类(分别为 StrutsDynamicFactory
或 FacesDynamicFactory
)移动到 Web 应用程序 Java 包中。如果您打算开发一个只有 servlet 和 JSP 页面的 Web 应用程序,或使用 Spring 或 WebWork 等其他 Web 框架,只需查看 StrutsDynamicFactory
和 FacesDynamicFactory
类如何使用 WFNM 框架的 DynamicFactory
类。
例如,如果您计划只使用 servlet 和 JSP 页面,并且需要让用户回到前一个页面,那么可以使用清单 21 中的代码:
public void doGet(HttpServletRequest req, HttpServletResponse res) throws ... { String url = DynamicFactory.getPreviousPage(req.getSession()); getContext().getServletDispatcher(url).forward(req,res); } |
通过类似的方式,可以使用 DynamicFactory
的其他方法在 Web 应用程序中后退导航。
自动会话清理不需要特别的配置,但有时需要更改默认策略。例如,如果返回前一个 Webflow B 时,要在 HTTP 会话中保留在 Webflow D 期间放入到 HTTP 会话中的对象,则要记得在定义 Webflow D 时指定这个策略,如清单 22 所示:
<wfnm:webflow name="D" owner="previous" /> |
通过这种方式,在 Webflow D 期间放入到 HTTP 会话中的对象由 Webflow B 所有(仍然可见),直到返回 Webflow A 时才被删除。
本文将 Webflow 的概念定义为 Web 页面的有名序列,并提供了一种使用 Web 标记的实现方式。通过考察一个示例场景,您看到了如何通过维护 Web 页面和 Webflow 的服务器端导航历史来改善后退导航,以及如何使用过滤器实现它。此外,还可以根据不同的策略使用服务器端导航历史自动清理 HTTP 会话,以及如何实现这样的系统。最后,还提到了自动会话清理如何显著减少 HTTP 会话的使用。
要获取这些好处,请将 WFNM 框架集成到 Web 应用程序。从 参考资料 下载 WFNM,开始您的体验!