上下文
问题
影响因素
解决方案
变体
示例
结果上下文
相关模式
致谢
上下文
对于任何一个曾经从头建立 Web 应用程序的人来说,他们都会有这样的体会:这项任务所需要的独立完成的工作量要比建立内部客户端 - 服务器应用程序多一些。首先,必须处理 HTTP 及其所有相关问题,比如 HTTP 头、多部分形式、HTTP 的无状态性、字符集编码方案、多用途 Internet 邮件扩展 (MIME) 类型和 URL 重写。在此之上,必须处理安全措施,比如安全套接字层 (SSL) 和用户身份验证。在许多情况下,列表中还要包括诸如客户端浏览器检测或用户活动情况日志记录之类的项目。
Web 应用程序服务器框架可以自动执行这其中的许多任务,但是,有时您需要有更多的控制权,或者需要在应用程序处理 Web 页面请求之前或之后插入您自己的处理步骤。
问题
如何围绕 Web 页面请求来实现公共的预处理和后处理步骤?
影响因素
有许多方法可以解决此问题,因此您需要考虑这会涉及哪些影响因素和权衡因素:
- 常见的做法是,将较低级别的功能(如处理 HTTP 头、Cookie 或字符编码)与应用程序逻辑分离。这样,您就可以在不能使用 Web 客户端的其他环境中测试和重用应用程序逻辑。
- 预处理和后处理功能发生更改的幅度可能不同于应用程序功能。在使字符集编码模块正常工作之后,更改它的可能性要小于更改用来生成 HTML 页面的代码的可能性。因此,应当区分所关注的事项,这样有助于限制更改的扩散。
- 许多预处理和后处理任务是所有 Web 页面公共的。因此,您应该努力在中心位置实现这些功能,以避免代码重复。
- 许多较低级别的功能不相互依赖。例如,浏览器检测和字符编码检测是两项独立的功能。要最大限度地利用重用性,应该将这些功能封装在一组可组合模块中。这样,就能添加或删除模块,而不会影响现有模块。
- 在许多情况下,能够在部署时而不是编译时添加或删除模块是非常有好处的。例如,您可能仅在软件的国际部署中部署字符编码检测模块,但不会在本地部署中这样做。或者,您可能有供匿名用户免费访问的 Web 站点,您要向该站点添加一个要求用户注册的身份验证模块。这种在部署时添加或删除模块而不必更改代码的能力,通常称为"部署时可组合性"。
- 因为较低级别的功能是针对每个单独的页面请求而执行的,所以性能至关重要。这意味着两件事情:执行尽可能少的操作和高效执行。您不希望由于不必要的功能或决策点而导致这些常见功能过载,但确实希望最大限度地减少对速度较慢的外部资源(如数据库)的访问。因此,您应该使每个处理步骤尽可能紧凑和高效。
- 您甚至可能考虑用不同的编程语言实现其中的某些功能,例如使用对字符流的处理效率非常高的语言(如 C++)。另一方面,使用不同的语言可能使您无法使用应用程序框架所提供的一些有用的功能(例如,自动内存管理和对象池)。无论哪种方法,只要能够将预处理与主应用程序分离开来,以便可以根据需要选择使用不同的编程语言,就是有益的。
- 在创建这些预处理和后处理功能之后,您希望能够在其他 Web 应用程序中重用它们。您希望对它们进行结构化处理,以便可以在另一个环境中重用一个模块,而不依赖于其他模块。您还希望能够将现有模块与新模块组合在一起,而不必进行任何代码更改。
解决方案
创建一串可组合的筛选器,以便在 Web 页面请求期间实现公共的预处理和后处理任务。
筛选器构成了一系列独立模块,在将页面请求传递到控制器对象之前,这些模块可以链接在一起以执行一组公共的处理步骤。因为各个筛选器实现的是完全相同的接口,所以它们彼此之间没有显式依赖性。因此,可以在不会影响现有筛选器的情况下添加新的筛选器。您甚至可以在部署时添加筛选器,方法是基于配置文件动态地对它们进行实例化。
对各个筛选器的设计应该尽可能让它们不对是否存在其他筛选器作出任何假设。这样可以维护可组合性,即添加、删除或重新排列筛选器的能力。此外,某些实现 Intercepting Filter 模式的框架不会保证筛选器的执行顺序。如果发现多个筛选器之间存在很强的相互依赖性,最好采取调用帮助器类的常规方法,因为这样可以保证保留筛选器序列中的约束信息。
在某些上下文中,术语 Intercepting Filter 与使用 Decorator 模式 [Gamma95] 的特定实现相关联。本文描述的解决方案采用了略微抽象的视图,并考虑了 Intercepting Filter 概念的不同实现选项。
筛选器链
Intercepting Filter 的直接实现方式是一个筛选器链,这个筛选器链可以用来遍历一个由所有筛选器组成的列表。Web 请求处理程序在将控制权传递到应用程序逻辑之前将首先执行筛选器链(见图 2)。
当 Web 服务器收到页面请求时,请求处理程序首先将控制权传递给 FilterChain(筛选器链)对象。此对象维护着一个包含所有筛选器的列表,并按顺序调用每个筛选器。FilterChain 可以从配置文件中读取筛选器顺序,以实现部署时的可组合性。每个筛选器都有机会修改传入请求。例如,它可以修改 URL,或添加应用程序要使用的头字段。执行完所有筛选器之后,"请求处理程序"将控制权传递给控制器,后者将执行应用程序功能(见图 3)。
此设计的主要优点之一是,筛选器是独立的组件,不直接依赖于其他筛选器或控制器,因为 FilterChain 会调用每个筛选器。因此,筛选器不必包含对下一个筛选器的引用。处理程序将筛选器所操作的上下文传递到每个筛选器中。筛选器可以操作上下文,例如,通过添加信息或重定向请求。
Decorator
Intercepting Filter 模式的一个有趣的替代实现是使用 Decorator 模式来包装 Front Controller。Decorator 可以包装一个对象,使其提供与原始对象相同的接口。因此,对于引用原始对象的任何其他对象来说,此包装是透明的。因为原始对象的接口和包装是完全相同的,所以可以添加用来包装该包装器的其他包装器,以创建与筛选器链很类似的包装器链。在每个包装器内,可以执行预处理和后处理功能。
图 4 和图 5 显示如何使用此概念实现 Intercepting Filter。每个筛选器都实现了 Controller 接口。它还包含对实现了 Controller 接口的下一个对象的引用,该对象可能是实际的控制器 (concreteController) 或另一个筛选器。即时筛选器可以相互调用,筛选器之间也没有直接的依赖性,因为每个筛选器仅引用 Controller 接口,而不是下一个筛选器类。
在筛选器将控制权传递给下一个筛选器之前,它有机会执行预处理任务。同样,在筛选器链中的其余筛选器处理完请求之后,筛选器也有机会执行后处理任务。
使用 Decorator 方法,就可以不需要用来遍历筛选器的 FilterChain 类。此外,请求处理程序现在完全不知道筛选器是否存在。就请求处理程序而言,它只需使用 Controller 接口调用控制器即可。在坚定的面向对象开发人员看来,通常此方法更好,但是若要通过查看代码来了解发生的事件会有点困难。Decorator 方法与筛选器链方法的关系,非常类似于链接列表与具有迭代器的数组之间的关系。
即使对象实例具有彼此的引用,您仍然可以在运行时组装筛选器链。您可以沿着对链中下一个筛选器对象的 Controller 接口的引用进行传递,从而对每个筛选器进行实例化。这样,您就可以从后向前动态构建筛选器链。
事件驱动的筛选器
在理想世界中,在设计各个筛选器时将使它们与其执行顺序无关;但在现实世界中这是很少见的。即使您设法独立地设计了筛选器,这些筛选器的许多功能最终也会重复。例如,需要对 HTTP 头(例如,执行浏览检测和提取 Cookie)进行分析的每个筛选器都必须分析头信息、提取头元素名称并对其执行某个操作。如果框架可以完成该工作的一部分,并遍历由所有头元素组成的集合(按元素名验证和编制索引),那么这会容易得多。这将使筛选器开发更容易、更不易出错,但是此后所有筛选器都将依赖于这一公共的头分析功能。除非筛选器必须在发生任何头分析(可能是因为您要操作或重新排列某些头信息)之前访问 HTTP 请求流,否则这将不是一个问题。
如果要提供其他基本功能,但仍然允许将筛选器插入到请求流中,则必须定义多个筛选器链。此后,将在由框架完成处理步骤之前或之后执行每个筛选器链。例如,您可以具有一个在发生任何头分析之前执行的筛选器链,还具有一个在分析头之后执行的筛选器链(见图 6)。如果您得出此概念的逻辑结论,就可以定义完整的一系列事件。可以让筛选器根据它执行的功能和它需要从框架获得的服务来决定它要附加到哪个事件上。
此模型与 Observer模式中所述的事件模型有一些类似之处。在这两种情况下,对象都可以在原始对象不依赖于观察器的情况下"预订"事件。对象不依赖于任何特定观察器,因为它通过抽象接口调用观察器。Intercepting Filter 和 Observer 的主要不同是,观察器通常不修改源对象,只是被动地"观察"源对象中发生的事件。另一方面,Intercepting Filter 的用途是截取和修改在调用它时所处的上下文。图 6 还很好地说明了每个筛选器如何截取 Web 服务器框架内的事件序列(Intercepting Filter 由此得名)。
变体
在大多数情况下,由于筛选器虽然操作了上下文但不会影响执行流,在这个意义上,筛选器是被动的。但是,如果筛选器截取了 Web 请求,通常必须把筛选器设计为将请求重定向到其他页面。例如,如果验证失败,则验证筛选器可能将请求重定向到错误页面或登录页面。
为了演示这些筛选器如何影响 Web 请求的流程,图 7 显示了一个典型的筛选器方案序列,其中的截取筛选器不会干涉消息流。
图 8 显示一个替代序列,其中 Filter One 根据请求的类型将消息流重定向到其他页面。
在此方案中,未呈现任何页面,但是产生了一个重定向头(HTTP 响应 302),并返回给客户端。此头信息导致客户端向在重定向头中所指定的 URL 发出新的请求。因为此类型的重定向需要来自客户端浏览器的另一个请求,所以它通常称为客户端重定向。主要的缺点是,客户端浏览器必须发出两个请求才能检索页面。这样,就减慢了页面显示速度,还会导致书签的复杂化,因为客户端将为重定向的 URL 创建书签,而这通常是不好的。
另一方面,服务器端重定向将把请求转发到其他页面,而不要求到客户端的往返行程。它们通过将控制权返回给 httpRunTime 对象做到这一点,而httpRunTime 对象将沿请求上下文直接调用其他Page Controller。此传递发生在服务器内部,不会涉及客户端。因此,您不必对请求重复执行任何公共预处理任务。
服务器端重定向在两个常见情况下使用:可以在 Intercepting Filter 中使用 URL 操作,以允许客户端使用虚拟 URL 将参数传递给应用程序。例如,筛选器可以将 http://example.com/clientabc 转换为 URL:http://www.example.com/start.aspx?Client=clientabc。此操作提供了一种间接机制,使客户端可以创建一个不受应用程序内部更改(例如,从 .asp 文件迁移到 .aspx 文件)影响的虚拟 URL 书签。使用服务器端重定向的另一常见方法是使用 Front Controller。Front Controller 在一个中心组件中处理所有页面请求,然后将控制权传递给适当的命令。如果 Web 应用程序具有可动态配置的导航路径,则 Front Controller 是很有用的。
示例
因为在处理 Web 请求时通常需要有截取筛选器,所以大多数 Web 框架都为应用程序开发人员提供了将截取筛选器挂靠到请求-响应过程中的机制。
Microsoft® Windows® 平台提供了两种截然不同的机制:
- 由运行 Internet 信息服务 (IIS) 的服务器提供 ISAPI 筛选器。ISAPI 筛选器是在执行任何其他处理之前调用的低级结构。因此,ISAPI 筛选器对请求的处理具有高度控制能力。ISAPI 筛选器非常适合于低级功能(如 URL 操作)。可惜的是,ISAPI 筛选器应该用 C++ 语言编写,并且无法访问合并到 Microsoft® .NET Framework 中的任何功能。
- .NET Framework 提供了 HTTPModule 接口。使用配置文件,可以将实现此接口的筛选器附加到由框架定义的一系列事件中。有关详细信息,请参阅在 ASP.NET 中使用 HTTP 模块实现 Intercepting Filter。
结果上下文
Intercepting Filter 模式具有下列优缺点:
优点
- 分隔任务。筛选器中包含的逻辑与应用逻辑相互分隔。因此,在低级功能发生改变(例如,如果从 HTTP 迁移到 HTTPS,或者将会话管理从 URL 重写迁移到隐藏的窗体域)时,应用程序代码不受影响。
- 灵活性。各个筛选器是相互独立的。因此,可以将任何筛选器的组合链接在一起,而不必对任何筛选器进行代码更改。
- 中心配置。由于筛选器具有可组合性,因此您可以使用单个配置文件来加载筛选器链。您可以修改单个配置文件以确定要插入到请求处理中的筛选器列表,而不需要使用许多源代码。
- 部署时可组合性。在运行时,可以根据配置文件构造 Intercepting Filter 链。因此,在部署过程中可以更改筛选器的顺序,而不必修改代码。
- 重用性。由于筛选器不依赖于其操作环境(它们所操作的上下文除外),因此可以在其他 Web 应用程序中重用各个筛选器。
缺点
- 顺序依赖性。截取筛选器不显式依赖于任何其他筛选器。但是,筛选器可能对传递给它们的上下文相关信息进行假定。例如,一些筛选器可能期望在调用它们之前已发生某个处理。在配置筛选器链时,请考虑这些隐式依赖性。一些框架可能不会保证筛选器之间的执行顺序。如果程序要求严格的顺序,则硬编码的方法调用可能比动态筛选器链更好。
- 共享状态。除了操作上下文以外,筛选器没有用于彼此共享状态信息的显式机制。对于将信息从筛选器传递到另一个筛选器,这也是正确的。例如,如果筛选器基于头字段的值来分析浏览器类型,则无法简单地将此信息传递到应用程序控制器。最常用的方法是将虚设的头字段添加到包含筛选器输出的请求上下文中。然后,控制器可以提取虚设头字段,并根据其值作出决定。遗憾的是,您失去了筛选器和控制器之间的任何编译时检查或类型安全性。这是松耦合的不利方面。
截取筛选器与控制器
因为截取筛选器是正好在控制器之前和之后执行的,所以有时可能很难确定是在截取筛选器内、还是在控制器内实现功能。下列准则为作出此决定提供了一些指导:
- 筛选器更适合于处理与传输有关的低级功能,如字符集解码、解压缩、会话验证、客户端浏览器类型识别和流量日志记录。这些类型的操作往往是封装良好的、高效的、无状态的。因此,将这些操作链接在一起是很容易的,一个操作不必将状态信息传递给另一操作。
- 与模型进行交互的真实应用程序功能最好在控制器或控制器的帮助器内实现。这些类型的功能通常不拥有筛选器所要求的那种可组合性。
- 在大多数情况下,筛选器内部的处理不依赖于应用程序的状态;无论如何,都会执行它。即使页面控制器可能包含常见功能,最好也要保留逐个覆盖行为的机会。控制器比筛选器链更适合于此任务。
- 许多筛选器实现(例如,IIS ISAPI 筛选器)在应用程序服务器内的较低层上执行。这为筛选器提供了很大控制(在调用筛选器之前发生的事件不多),但会阻止它们访问应用程序层提供的许多功能(如会话管理)。
- 因为筛选器是针对每个 Web 页面请求而执行的,所以性能是至关重要的。因此,框架可能限制实现语言的选择。例如,大多数 ISAPI 筛选器是使用已编译语言(如 C++)实现的。如果可以方便地使用 Microsoft Visual Basic® 开发系统或 Microsoft Visual C#® 开发工具来编写这些代码,并能完整地访问 .NET Framework,您就不必使用 C++ 编写复杂应用程序逻辑的代码。
相关模式
有关详细信息,请参考下列相关模式:
- Intercepting Filter 通常与Front Controller联合使用。[Alur01] 和 [Fowler03] 详细描述了这两种模式之间的关系。
- Decorator [Gamma95]。可以将截取筛选器视为包装了前端控制器的装饰器。
致谢
[Alur01] Alur, Crupi, and Malks. Core J2EE Patterns: Best Practices and Design Strategies. Prentice-Hall, 2001.
[Fowler03] Fowler, Martin. Patterns of Enterprise Application Architecture. Addison-Wesley, 2003.
[Gamma95] Gamma, Helm, Johnson, and Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley, 1995.
[Buschmann96] Buschmann, Frank, et al. Pattern-Oriented Software Architecture, Vol 1. Wiley & Sons, 1996.
[Schmidt00] Schmidt, et al. Pattern-Oriented Software Architecture, Vol 2. Wiley & Sons, 2000.