文章摘要(AI生成)
介绍计算的本质可能是对于任何预期的输入(A),我们返回预期的输出(B)。挑战是从(A)到(B)。对于一个简单的程序,(A)到(B)可能是一个单一的转换。比如说,将字符代码移动 32 位,使“a”变为“A”。在复杂的应用程序中,A 到 B 可能是一条漫长而曲折的道路。我们可能需要确认用户有权从 (A)
介绍
计算的本质可能是对于任何预期的输入(A),我们返回预期的输出(B)。挑战是从(A)到(B)。对于一个简单的程序,(A)到(B)可能是一个单一的转换。比如说,将字符代码移动 32 位,使“a”变为“A”。在复杂的应用程序中,A 到 B 可能是一条漫长而曲折的道路。
我们可能需要确认用户有权从 (A) 创建 (B)。我们可能需要发现 (A) 是 (B) 的有效输入。我们可能需要从另一个字符集转换 (A)。我们可能需要在写 (B) 之前插入一个序言。在创建 (B) 之前,我们可能需要将另一个资源与 (A) 合并。同时,如果在处理过程中出现任何问题,则必须对错误进行处理,甚至记录。某些任务可能会在出现非致命错误后继续执行,或者,如果错误是致命错误,则可能需要停止所有处理。
程序员在应用程序中组织处理逻辑的方式有很多种。通常,优雅的架构和混乱的泥球之间的区别在于控制如何从一个进程流向另一个进程。为了实现和保持优雅,我们必须组织复杂的、多步骤的过程,以便它们易于发现和更改。
将“业务”逻辑与“表示”逻辑分开
问题:您希望在不使应用程序设计复杂化的情况下清晰地分离执行层和表示层。
解决方案:使用责任链和命令模式,以便表示层可以执行命令或命令链,而无需知道命令是如何实现的。
讨论:为了有用,大多数应用程序需要运行一个进程,然后告诉客户端发生了什么。在实践中,我们发现将“运行”和“讲述”混合在一起会产生难以测试和维护的代码。如果我们可以让一个组件运行(或执行)流程,而另一个组件报告(或呈现)结果,那么我们可以分别测试、创建和维护每个组件。但是,我们如何才能在不使应用程序设计复杂化的情况下将执行层和表示层清晰地分开呢?
大多数应用程序框架,尤其是 Web 应用程序框架,都依赖于命令模式。传入的 HTTP 请求被映射到某种类型的“命令”对象。命令对象使用 HTTP 请求中传递的信息执行所需的任何操作。
在实践中,命令中通常有命令。 Web 应用程序中的 Command 对象通常看起来像三明治。首先,它为表示层做一些事情,然后执行业务逻辑,然后再做一些表示层的事情。许多开发人员面临的问题是如何将 Web 命令中间的业务逻辑与请求/响应事务中的其他必要任务清晰地分开。
责任链包将命令模式与经典的责任链模式相结合,可以轻松地将业务命令作为更大应用程序命令的一部分进行调用。 (有关模式的更多信息,请参阅设计模式:可重用面向对象软件的元素 [ISBN 0-201-63361-2])。
为了实现这些模式,Chain 包定义了五个关键接口:
- context
- command
- chain
- filter
- catalog
context:上下文表示应用程序的状态。在 Chain 包中,Context 是 java.util.Map 的标记接口。上下文是一个信封,包含完成交易所需的属性。换句话说,上下文是具有成员值的有状态对象。
command:一个命令代表一个工作单元。一个命令有一个单一的入口方法:公共布尔执行(上下文上下文)。 Command 作用于通过上下文对象传递给它的状态,但不保留它自己的状态。命令可以组装成一个链,以便可以从离散的工作单元创建复杂的事务。如果命令返回 true,则不应执行链中的其他命令。如果命令返回 false,则链中的其他命令(如果有)可以执行。
Chain:Chain 实现了 Command 接口,因此 Chain 可以与 Command 互换使用。应用程序不需要知道它是调用链还是命令,因此您可以从一个重构到另一个。一个链可以根据需要嵌套其他链。这个性质被称为 Liskov 替换原则。(参考:https://commons.apache.org/proper/commons-chain/apidocs/index.html)
filter:理想情况下,每个命令都是一个岛。在现实生活中,我们有时需要分配资源并确保无论发生什么资源都会被释放。过滤器是一个专门的命令,它添加了一个 postProcess 方法。链应在返回之前调用链中任何过滤器的 postProcess 方法。实现 Filter 的命令可以安全地释放它通过 postProcess 方法分配的任何资源,即使这些资源与其他命令共享。
catalog:目录是命名命令(或链)的集合,可用于检索应基于符号标识符执行的命令集。 目录的使用是可选的,但在有多个可能的链可以根据环境条件选择和执行时很方便。
本章的其余部分提供了一些技巧,可帮助您将责任链包用于您自己的应用程序。
测试命令
问题:您想开始在应用程序中使用 Command 对象。
解决方案:使用测试驱动开发为一个命令创建一个测试,并让测试告诉你如何编写命令。当测试通过时,您将有一个工作命令集成到您的应用程序中。
讨论:假设我们正在开发一个为每个客户端维护一个“profile”对象的应用程序。我们需要在客户端与应用程序的“会话”期间更改 Profile 的状态,这可能跨越多个请求。不同的应用程序环境可能以不同的方式保存 Profile。 Web 应用程序可以将 Profile 存储为 HttpSession 的属性或客户端“cookie”。 EJB 应用程序可以维护一个概要文件作为客户端环境的一个属性。无论如何,您需要一个命令来检查客户端是否有 Profile 对象,如果没有,则创建一个。命令不知道应用程序如何存储配置文件,甚至不知道它是否被存储。
我们使用命令的原因之一是因为它们易于测试。在这个秘籍中,让我们为我们的命令编写一个测试。在另一个秘籍中,我们将创建相应的命令。这种方法称为测试驱动开发。
要测试我们的命令,我们可以简单地
- 创建具有已知状态的上下文
- 创建一个 Command 实例进行测试
- 执行命令,传递我们的上下文
- 确认我们的 Context 现在包含预期的状态
对于 Context,我们可以使用作为 Chain 包的一部分提供的 ContextBase 类。 ProfileCheck 命令和 Profile 对象显示在下一个章节中。 我们的 TestProfileCheck TestCase 的其余代码如示例 1 所示。
测试是否创建了 Profile 对象
package org.apache.commons.mailreader;
import junit.framework.TestCase;
import org.apache.commons.chain.Command;
import org.apache.commons.chain.Context;
import org.apache.commons.chain.mailreader.commands.ProfileCheck;
import org.apache.commons.chain.mailreader.commands.Profile;
import org.apache.commons.chain.impl.ContextBase;
public class ProfileCheckTest extends TestCase {
public void testProfileCheckNeed() {
Context context = new ContextBase();
Command command = new ProfileCheck();
try {
command.execute(context);
} catch (Exception e) {
fail(e.getMessage());
}
Profile profile = (Profile) context.get(Profile.PROFILE_KEY);
assertNotNull("Missing Profile", profile);
}
因为我们使用的是测试优先的方法,所以我们(还)不能运行甚至编译这个类。 但是我们可以使用测试类来告诉我们还需要写哪些其他类。 下一个秘籍展示了如何创建命令。
创建一个命令
问题:您需要为您的应用程序创建一个命令,这样命令的测试才会成功。
解决方案:通过测试告诉你什么代码会实现Command的API合约。
讨论:使用命令和命令链的一个关键原因是可测试性。由于命令被设计为作用于它们接收到的任何上下文,我们可以创建一个具有已知状态的上下文来测试我们的命令。在前面的秘籍中,我们为 ProfileCheck 命令创建了一个测试。让我们实现这个命令,让它通过我们的测试。
要通过 ProfileCheck 测试,我们需要
-
使用 Profile.PROFILE_KEY 作为属性名称,从上下文中检索配置文件。
-
如果 Profile 为 NULL,则创建一个 Profile 并将其存储在 Context 中。
-
向调用者返回 false 或 true。
-
在第 3 步是否返回 false 或 true 是可选的。您可以选择返回 true,因为此命令确实检查了配置文件。或者,您可以决定返回 false,以便命令可以用作链的一部分。返回值控制链是终止还是继续。 True 迫使链条结束。 False 允许链继续。现在,我们将只返回 false,以便我们的命令可以用作更大的命令链的一部分。
实现我们的 ProfileCheck 命令的代码如示例 2 所示。
package org.apache.commons.chain.mailreader.commands;
import org.apache.commons.chain.Command;
import org.apache.commons.chain.Context;
public class ProfileCheck implements Command {
public Profile newProfile(Context context) { return new Profile(); }
public boolean execute(Context context) throws Exception {
Object profile = context.get(Profile.PROFILE_KEY);
if (null == profile) {
profile = newProfile(context);
context.put(Profile.PROFILE_KEY, profile);
}
return false;
}
}
可能工作的最简单的 Profile 类。
package org.apache.commons.chain.mailreader.commands;
public class Profile {
public static String PROFILE_KEY = "profile";
}
请注意,我们使用了一个单独的方法来创建 Profile 对象。 如果我们在 Execute 方法中隐藏了对“new Profile()”的调用,我们就不能重用我们的 CheckProfile 命令来创建专门的配置文件。 使用辅助方法创建对象称为工厂模式。
我们现在应该能够编译所有三个类并运行我们的测试。
创建一个上下文
问题:您需要一个类型安全、封装或可与需要 JavaBean 属性的组件互操作的 Context。
解决方案:从 ContextBase 扩展您的 Context 类,并添加您需要的任何 JavaBean 属性。
讨论:许多组件已经使用“上下文”。各种 Java Servlet“范围”中的每一个都有一个上下文对象。 Apache Velocity 产品依赖于上下文对象。大多数操作系统都有一个简单的“环境”设置列表,即“上下文”。这些示例都使用“地图”或“字典”样式上下文。这些上下文是一个简单的条目列表,其中每个条目是一个键和一个值。
其他组件也使用相当于上下文的内容,但将条目预定义为对象属性。 Apache Struts 框架就是一个例子。开发人员可以定义一个 JavaBean(或“ActionForm”)作为请求的上下文。一些组件混合了这两种方法。 Servlet 请求和会话对象公开了一个 Map 样式的上下文以及几个预定义的属性。 Struts 支持使用 Map 的 ActionForm 变体。
架构师通常会选择 Map 样式的上下文,因为它们易于实现且非常易于扩展。通常,开发人员可以随意将自己的条目添加到 Map 样式的上下文中。当然,每个工程决策都是一种权衡。 Maps 以类型安全和封装换取灵活性和可扩展性。其他时候,架构师会决定用灵活性换取类型安全。或者,我们可能决定用可扩展性换取封装。通常,这些决策是由与可能需要 Map 或 JavaBean 的其他组件进行互操作的需要驱动的。
Apache Commons Chain of Command 架构师已选择 Map 样式的上下文作为默认设置。 Chain Context 只是标准 Java Map 接口的“标记接口”。
Context接口是一个扩展Map的“标记”接口。
public interface Context extends Map {
}
但是,为了向开发人员提供类型安全、封装和互操作性,Chain 提供了一个复杂的 ContextBase 类,该类也支持 JavaBean 属性。
如果开发人员在 ContextBase 的子类上声明了 JavaBean 属性,则 Map 方法会自动使用此属性。 ContextBase 的 Map get 和 put 方法自省子类。如果他们找到一个以键参数命名的 JavaBean 属性,则改为调用 getter 或 setter 方法。
这种魔法对任何声明的属性强制执行类型安全,但开发人员仍然可以像使用普通 Map 一样使用上下文。如果所有需要的属性都定义为属性,那么 ContextBase 可以与期望 Map 的组件以及期望 JavaBean 的组件互操作——所有这些都同时进行。一切都是透明的,对调用者没有特殊要求。
让我们为 ContextBase 子类创建一个测试,以证明 JavaBean 属性和 Map 方法是可互操作的和类型安全的。
为了测试上下文的互操作性,我们需要做四件事:
-
使用 JavaBean setter 为类型化属性赋值
-
使用 Map get 方法检索相同的值
-
使用 Map set 方法分配另一个值
-
使用 JavaBean setter 检索更新值
为了测试上下文的类型安全,我们还需要
- 使用 Map get 方法将 String 分配给类型化属性
- 确认分配引发“类型不匹配”异常
为了编写这些测试,让我们为名为“MailReader”的应用程序创建一个带有 Locale 属性的上下文。 我们的 LocaleValueTest 的代码如下所示。LocaleValueTest 证明我们的上下文是可互操作的和类型安全的。
package org.apache.commons.mailreader;
import junit.framework.TestCase;
import junit.framework.Assert;
import org.apache.commons.chain.mailreader.MailReader;
import java.util.Locale;
public class LocaleValueTest extends TestCase {
MailReader context;
public void setUp() {
context = new MailReader();
}
public void testLocaleSetPropertyGetMap() {
Locale expected = Locale.CANADA_FRENCH;
context.setLocale(expected);
Locale locale = (Locale) context.get(MailReader.LOCALE_KEY);
Assert.assertNotNull(locale);
Assert.assertEquals(expected, locale);
}
public void testLocalePutMapGetProperty() {
Locale expected = Locale.ITALIAN;
context.put(MailReader.LOCALE_KEY, expected);
Locale locale = context.getLocale();
Assert.assertNotNull(locale);
Assert.assertEquals(expected, locale);
}
public void testLocaleSetTypedWithStringException() {
String localeString = Locale.US.toString();
try {
context.put(MailReader.LOCALE_KEY, localeString);
fail("Expected 'argument type mismatch' error");
} catch (UnsupportedOperationException expected) {
;
}
}
}
一个通过 LocaleValueTest 的 MailReader Context 对象如下所示。将通过 LocalValueTest 的最简单的 MailReader 对象。
package org.apache.commons.chain.mailreader;
import org.apache.commons.chain.impl.ContextBase;
import java.util.Locale;
public class MailReader extends ContextBase {Prop
public static String LOCALE_KEY = "locale";
private Locale locale;
public Locale getLocale() {
return locale;
}
public void setLocale(Locale locale) {
this.locale = locale;
}
}
上面的 MailReader 对象显示了 ContextBase 类中内置了多少实用程序。 我们所要做的就是定义属性。 基类负责其余的工作。 当然,没有免费的午餐。 ContextBase 必须通过自省来判断属性是否具有属性。 ContextBase 代码的编写效率很高,但如果您的应用程序只能使用 Map 样式的上下文,则可以使用更精简的 MailReader 上下文版本,如下所示。更简单的 MailReader 上下文(但这会使 LocalValueTest 失败)。
package org.apache.commons.chain.mailreader;
import org.apache.commons.chain.Context;
import java.util.Hashmap;
public class MailReader extends Hashmap implements Context {
public static String LOCALE_KEY = "locale";
}
通过扩展现有的 ContextBase 子类,或使用 HashMap 滚动您自己的类,您可以使用最适合您自己的架构的任何类型的上下文。
创建一个目录
问题:您希望对应用程序进行分层而不创建对存在于不同层中的 Command 对象的依赖关系。
解决方案:为每个命令分配一个逻辑名称,以便可以从“目录”中调用它。目录将依赖关系移至逻辑名称并远离 Java 类名或类名。调用者依赖于目录,但不依赖于实际的 Command 类。
讨论:上下文和命令对象通常用于将应用程序的层连接在一起。一层如何在另一层调用命令而不在两层之间创建新的依赖关系?
层间依赖是企业应用程序中的一个常见难题。我们希望对我们的应用程序进行分层,使其变得健壮和有凝聚力,但我们还需要一种方法让不同的层相互交互。 Commons Chain 包提供了一个 Catalog 对象,以帮助解决层之间以及同一层上的组件之间的依赖关系问题。
目录可以通过元数据(XML 文档)进行配置,并在应用程序启动时进行实例化。客户端可以在运行时从目录中检索他们需要的任何命令。如果需要重构命令,可以在元数据中引用新的类名,而无需更改应用程序代码。
让我们看一些使用目录的代码。下面显示的是从存储在 Web 应用程序的 servlet 上下文中的目录执行命令的方法。目录存储应用程序可以查找和执行的命令。
boolean executeCatalogCommand(Context context,
String name, HttpServletRequest request)
throws Exception {
ServletContext servletContext =
request.getSession().getServletContext();
Catalog catalog =
(Catalog) servletContext.getAttribute("catalog");
Command command = catalog.getCommand(name);
boolean stop = command.execute(context);
return stop;
}
请注意,我们只将命令的名称传递给方法。 另请注意,我们在不知道任一对象的精确类型的情况下检索命令并将其传递给上下文。 所有参考均指向标准接口。
下面显示的是一个可用于创建目录的 XML 文档,就像上面示例中调用的那样。目录可以使用元数据(XML 文档)进行配置。
<?xml version="1.0" ?>
<catalog>
<command
name="LocaleChange"
className="org.apache.commons.chain.mailreader.commands.LocaleChange"/>
<command
name="LogonUser"
className="org.apache.commons.chain.mailreader.commands.LogonUser"/>
</catalog>
应用程序需要知道我们要执行的命令的名称,但它不需要知道命令的类名。 命令也可以是命令链。 我们可以重构目录中的命令并对应用程序进行零更改。 例如,我们可能决定在更改用户的区域设置之前检查用户配置文件。 如果我们想让运行 CheckProfile 命令成为“LocaleChange”的一部分,我们可以更改目录以使“LocaleChange”成为链。 以下示例显示了目录元数据,其中“LocaleChange”是一个链。 可以通过对应用程序代码进行零更改来重构目录。
<catalog>
<chain name="LocaleChange">
<command
className="org.apache.commons.chain.mailreader.commands.ProfileCheck"/>
<command
className="org.apache.commons.chain.mailreader.commands.LocaleChange"/>
</chain>
<command
name="LogonUser"
className="org.apache.commons.chain.mailreader.commands.LogonUser"/>
</catalog>
在“创建命令”章节中,我们使用工厂方法创建“Profile”对象。 如果我们将该命令子类化以创建一个专门的配置文件,我们可以在目录中引用新的类名,而对应用程序的其余部分进行零更改。
能够对应用程序进行快速轻松的更改会对利润产生重大影响。 应用程序的经常性年度维护成本介于初始开发成本的 25% 到 50% 之间(Gartner Group,2002 年 5 月)。
从 Web 应用程序加载目录
问题:您希望在 Web 应用程序启动时自动加载目录。
解决方案:利用与 Commons Chain of Responsibility 包捆绑的 ChainListener。
讨论:可以使用传统的 Java 语句或通过将目录成员指定为元数据(XML 文档)以编程方式创建目录。 对于测试,以编程方式创建目录可能是最简单的。 对于部署,目录作为元数据更容易维护。 使用元数据的缺点是需要对其进行解析,以便可以创建指定的对象。 令人高兴的是,Commons Chain of Responsibility 包捆绑了一个 Listener,它可以读取 Catalog 元数据文件并创建相应的对象图。
要在 Web 应用程序中使用 ChainListener,只需添加对应用程序 web.xml(另一个元数据文档)的引用。 一个这样的参考如下所示。 通过 web.xml 加载 ChainListener
<!-- Commons Chain listener to load catalogs -->
<context-param>
<param-name>org.apache.commons.chain.CONFIG_CLASS_RESOURCE</param-name>
<param-value>resources/catalog.xml</param-value>
</context-param>
<listener>
<listener-class>org.apache.commons.chain.web.ChainListener</listener-class>
</listener>
此示例中的元素期望有一个“catalog.xml”文件存储在名为“resources”的目录下的应用程序类路径中。 通常,这意味着在“WEB-INF/classes”下有一个“resources”目录。 如果您使用 Maven 构建应用程序,Maven 可以自动将元数据文件从源代码树复制到 Web 基础结构树。 许多团队对自定义 Ant 构建文件做同样的事情。 下面显示的是 Maven 属性文件的片段,该文件将 catalog.xml 从“src/resources/chain”下的目录复制到 Web 部署目录下的“/WEB-INF/classpath/resources”。 在 Maven 属性文件中管理资源
<!-- ... -->
<build>
<sourceDirectory>src/java</sourceDirectory>
<resources>
<resource>
<directory>${basedir}/src/resources/chain</directory>
<targetPath>resources</targetPath>
<includes>
<include>catalog.xml</include>
</includes>
</resource>
</resources>
</build>
<!-- ... -->
您还可以将 ChainListener 配置为从系统路径或 JAR 读取文件。 有关所有配置详细信息,请参阅 JavaDoc。 如果您使用的是 Servlet 2.2 平台,还有一个 ChainServlet。
使用默认属性,并给定一个 HttpServletRequest 实例,您可以通过编码访问目录:
Catalog catalog = (Catalog) request.getSession()
.getServletContext().getAttribute("catalog");
给定目录,您可以执行命令并将上下文传递给它,如下所示:
Command command = catalog.getCommand(commandName);
boolean stop = command.execute(context);
当然,困难的部分是填充上下文并确定我们需要为给定请求运行哪个命令。 这项工作通常留给前端控制器,就像 Apache Struts 实现的那样。 因此,我们在本章中包含了“从 Struts 调用命令”秘籍。 如果您喜欢控制器,但不喜欢 Struts,那么还有“创建控制器”和“从 Servlet 调用命令”章节。
从 Struts 调用命令
问题:您想从 Struts 应用程序中调用命令。
解决方案:使用 CommandAction 调用以您的 ActionForm 命名的命令。
讨论:作为前端控制器,Apache Struts Web 应用程序框架具有三个主要职责。
-
验证用户请求
-
处理用户请求
-
创建对请求的响应
第三项通常委托给服务器页面。 Struts 提供了框架感知组件,如 JSP 标记库,以鼓励开发人员使用其他资源来创建响应。这样,Struts 只需要选择资源。实际的响应创建在其他地方处理。
Struts 还捆绑了一个组件来帮助验证用户请求。 Struts 验证器利用元数据来审查请求值并在验证失败时创建用户提示。
为了履行其“处理用户请求”的职责,Struts 提供了一个称为“Action”类的扩展点。 Struts Action 是一张白纸,开发人员可以在其中做任何必要的事情来处理请求。一些开发人员甚至从 Actions 进行 JDBC 调用,但不鼓励这种做法。 Struts 的最佳实践是让 Actions 将业务和系统逻辑调用委托给另一个组件,例如业务外观。 Struts Action 将适当的值传递给外观上的一个或多个方法。结果用于确定适当的响应。通常,动作的结果被描述为“成功”或“失败”。
除了空白的 Action,Struts 还分发了几个“标准”Action,例如 DispatchAction。标准操作旨在在应用程序中以不同方式多次使用。为了允许重用Action,Struts 提供了一个称为ActionMapping 的Decorator 类。可以通过 ActionMappings 指定运行时详细信息,因此标准 Action 的每次使用可能会略有不同。
为了解决Struts调用Command的问题,我们可以使用标准的Action来检索Catalog并调用Command。我们可以在 ActionMapping 中指定运行时详细信息。我们的详细信息包括要通过哪组验证以及要运行哪个命令。
在实践中,我们需要通过的验证集和我们需要运行的命令是紧密耦合的。事实上,为每个命令创建一组不同的验证可能是一种很好的做法。如果一个命令改变了,那么它的验证可以随之改变,而不影响其他命令。
在 Struts 中,验证集与 ActionForm 名称相关联。 ActionForm 名称是一个逻辑标识符,与 ActionForm 类名分开。当您使用 Struts Validator 时,Validations 的“表单”名称与 ActionMapping 指定的 ActionForm“名称”是相同的字符串。数据库专家将其称为 1:1 关系; Validator 表单名称和 ActionForm 名称是共享键。如果我们希望每个命令都有自己的一组验证,并且它是自己的 ActionMapping,那么我们应该始终使用相同的“键”。命令名称可以是 ActionForm 名称以及 Validator 表单名称。
以下示例显示名称如何在三个元数据文件(catalog.xml、validation.xml 和 struts-config.xml)中排列。将三个文件链接在一起的令牌或“密钥”是“LocaleChange”三个元数据文件的故事:catalog.xml、validation.xml 和 struts-config.xml
<!-- catalog.xml -->
<?xml version="1.0" ?>
<catalog>
<command
name="<em>LocaleChange</em>"
className="org.apache.commons.chain.mailreader.commands.LocaleChange" />
</catalog>
<!-- validation.xml -->
<?xml version="1.0" ?>
<!DOCTYPE form-validation PUBLIC
"-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.1//EN"
"http://jakarta.apache.org/commons/dtds/validator_1_1.dtd">
<form-validation>
<formset>
<form name="<em>LocaleChange</em>">
<field property="language" depends="required">
<arg0 key="prompt.language"/>
</field>
</form>
</formset>
</form-validation>
<!-- struts-config.xml -->
<?xml version="1.0" ?>
<!DOCTYPE struts-config PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 1.2//EN"
"http://jakarta.apache.org/struts/dtds/struts-config_1_2.dtd">
<struts-config>
<form-beans>
<form-bean
name="<em>LocaleChange</em>"
type="org.apache.struts.validator.DynaValidatorForm">
<form-property name="language" type="java.lang.String"/>
<form-property name="country" type="java.lang.String"/>
</form-bean>
</form-beans>
<action-mappings>
<action path="/LocaleChange"
name="<em>LocaleChange</em>"
type="org.apache.commons.chain.mailreader.struts.CommandAction">
<forward name="success" path="/Welcome.do" />
</action>
</action-mappings>
<struts-config>
在上面的示例中,我们使用“LocaleChange”作为命令名称、验证表单名称和操作表单 bean 名称。 要触发线程,我们需要做的就是定义一个通用 Action,它将使用 form-bean 名称作为命令名称。 下面的示例显示了我们的 CommandAction。 CommandAction 将表单 bean 名称与命令名称链接起来
package org.apache.commons.chain.mailreader.struts;
import org.apache.commons.chain.Catalog;
import org.apache.commons.chain.Command;
import org.apache.commons.chain.Context;
import org.apache.commons.chain.web.servlet.ServletWebContext;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;as the ActionForm name.
import org.apache.struts.action.ActionMapping;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class CommandAction extends Action {
protected Command getCommand(ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
throws Exception {
Catalog catalog = (Catalog) request.getSession()
.getServletContext().getAttribute("catalog");
String name = mapping.getName();
Command command = catalog.getCommand(name);
return command;
}
protected Context getContext(ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
throws Exception {
ServletContext application = request.getSession()
.getServletContext();
Context context = new ServletWebContext(
application, request, response);
return context;
}
protected static String SUCCESS = "success";
protected ActionForward findLocation(ActionMapping mapping,
boolean stop) {
if (stop) return mapping.getInputForward(); // Something failed
return mapping.findForward(SUCCESS);
}
public ActionForward execute(
ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
throws Exception {
Command command = getCommand(mapping, form, request, response);
Context context = getContext(mapping, form, request, response);
boolean stop = command.execute(context);
ActionForward location = findLocation(mapping, stop);
return location;
}
Action 类的入口点是 execute 方法。 我们的 execute 方法调用我们定义的 getCommand 和 getContext 方法,以从目录中获取 Command 并根据 Web 请求构建 Context。 为了简单起见,我们使用与 Commons Chain 捆绑的 ServletWebContext。 根据您的需要,您可能希望定义自己的专用上下文。 (请参阅“创建上下文”配方。)然后我们的 execute 方法调用命令的 execute 方法。 我们将 command.execute 的返回值传递给我们的 findLocation 方法,该方法确定“成功”或“失败”。
编写 CommandAction 的另一种方法是使用 ActionMapping “参数”属性来指示 Command name 。 为此,我们将修补 getCommand 以调用 mapping.getParameter() 而不是 getName(),如下所示:
- String name = mapping.getName();
+ String name = mapping.getParameter();
(减号表示删除或减去该行。加号表示插入或添加该行。Unix 补丁程序遵循这种格式。)
前面示例中的“参数”方法让我们可以独立于命令名称命名表单bean。 但是,结果是我们必须为每个 ActionMapping 指定 Command 名称。 (Bor-ring!)您还可以合并这两种方法并仅在使用时返回参数属性,如下所示:
String name = mapping.getParameter();
+ if ((null==name) || (name=="")) name = mapping.getName();
或者你可以混合和匹配这两种方法,当formbean名称和命令名称匹配时使用CommandAction,当它们不匹配时使用CommandParameterAction。 Struts 允许您使用任意数量的动作和标准动作,只要您喜欢。
请注意,我们的 Command 应该执行通常委托给 Action 的“自定义”工作。因此,我们不需要为每个任务创建一个 Action 子类。我们可以使用一两个标准动作并让它们调用适当的命令类。一组相关任务(或“故事”)可能共享一个 ActionForm 类和一个 Command 类,但大多数情况下,Action 可以是标准的、可重用的 Action。
关于上面的例子还有一点需要注意的是,我们使用“LocaleChange”标记作为路径属性。这意味着故事将通过打开(例如)“/LocaleChange.do”页面来触发。即便如此,路径并不是我们语义链的一部分。路径不是我们控制的完全逻辑名称。路径令牌与容器共享,容器可能对路径有自己的约束。 (例如,JAAS 模式匹配。)路径不能是我们的键链的一部分,因为它与容器的“业务逻辑”共享。
将“LocaleChange”用于其他所有内容后,将其用于路径标记似乎很自然。我们大多数人都会这样做。但是,路径可以根据需要而变化,而不会打乱语义链的其余部分。如果“路径”需要更改以适应 JAAS 配置的更改,则无需更改任何其他内容。
当然,还有其他几种方法可以从 Struts 动作中调用命令。由于将请求传递给 Action,因此很容易获得存储在应用程序范围内的 Catalog。一旦您可以访问目录,剩下的就很容易了。
其他框架,如 WebWorks 和 Maverick,具有类似于 Struts Actions 的组件。这些组件中的任何一个都可用于创建上下文、访问目录和执行命令。
创建一个controller
问题:您希望将应用程序的控制器组件基于 Commons Chain of Command 包。
解决方案:为控制器包创建一组接口,这些接口可以使用命令链包中的基类来实现。
警告:由于我们正在创建一个基础包,因此这个配方比大多数配方都要长。每个单独的组件都很简单,但有几个组件需要涵盖。由于这些组件是相互关联的,单独覆盖它们会令人困惑。所以,坐下来,放松一下腰带,尽情享受吧,而我们会准备一顿“七道菜”。
讨论:许多应用程序使用控制器模式的实现来处理用户请求。核心 J2EE 模式:最佳实践和设计策略 [ISBN 0-13-142246-4] 将控制器描述为“与客户端交互,控制和管理每个请求的处理”的组件。有几种类型的控制器,包括应用程序控制器和前端控制器。许多 Web 应用程序框架,如 Apache Struts,都使用前端控制器。
通常,控制器模式的实现会反过来使用命令模式或命令链模式。我们如何使用 Commons Chain of Command 包来实现控制器?
按照 Core J2EE Patterns 的一般描述,让我们首先定义一个测试,该测试将请求传递给控制器并确认返回了适当的响应。
要编写我们的测试,我们需要:
-
创建一个控制器。
-
为我们的请求添加一个处理程序到控制器。
-
创建一个请求并将其传递给控制器。
-
确认请求返回预期的响应。
为了简化测试的编写,让我们做出一些执行决策:
- Request 和 Response 对象具有“名称”属性。
- 响应的名称与其请求的名称(共享密钥)相匹配。
- 测试将基于接口;实现的类将扩展公共链成员。
- 控制器扩展目录。
- 请求和响应扩展上下文。
- 请求处理程序扩展命令。
- 没有特别的原因,我们将我们的控制器包称为“敏捷”。
下面的例子展示了一个 ProcessingTest 类和我们的 testRequestResponseNames 方法。Test 断言我们的控制器可以处理一个请求并返回一个适当的响应
package org.apache.commons.agility;
import junit.framework.TestCase;
import org.apache.commons.agility.impl.ControllerCatalog;
import org.apache.commons.agility.impl.HandlerCommand;
import org.apache.commons.agility.impl.RequestContext;
public class ProcessingTest extends TestCase {
public void testRequestResponseName() {
String NAME = "TestProcessing";
Controller controller = new ControllerCatalog();
RequestHandler handler = new HandlerCommand(NAME);
controller.addHandler(handler);
Request request = new RequestContext(NAME);
controller.process(request);
Response response = request.getResponse();
assertNotNull(response);
assertEquals(NAME, response.getName());
}
}
要编译 ProcessingTest 类,我们需要 Controller、RequestHandler、Request 和 Response 的接口成员,以及 ControllerCatalog、HandlerCommand 和 RequestContext 的类成员。
实现ProcessingTest需要的四个接口
要编译 ProcessTest,我们需要定义四个接口。
// Controller.java
package org.apache.commons.agility;
public interface Controller {
void addHandler(RequestHandler handler);
RequestHandler getHandler(String name) throws ProcessException;
void process(Request request) throws ProcessException;
}
// Request.java
package org.apache.commons.agility;
public interface Request {
String getName();
Response getResponse();
void setResponse(Response response);
}
// Response.java
package org.apache.commons.agility;
public interface Response {
String getName();
}
// RequestHandler.java
package org.apache.commons.agility;
public interface RequestHandler {
String getName();
void handle(Request request) throws ProcessException;
}
// ProcessException.java
package org.apache.commons.agility;
public class ProcessException extends Exception {
public ProcessException(Throwable cause) {
super(cause);
}
}
有了接口,我们就可以转向我们需要实现的类。
这些类需要实现ProcessingTest。
如果我们创建类,并存根方法,我们可以获得要编译的代码。 测试将运行,但骨架类不会通过集合。 让我们实现每个类,从 HandlerCommand 开始,如下所示。
HandlerCommand 提供子类可以覆盖的默认行为
package org.apache.commons.agility.impl;
import org.apache.commons.agility.ProcessException;
import org.apache.commons.agility.Request;
import org.apache.commons.agility.RequestHandler;
import org.apache.commons.agility.Response;
import org.apache.commons.chain.Command;
import org.apache.commons.chain.Context;
public class HandlerCommand implements Command, RequestHandler {
String name = null;
public HandlerCommand(String name) {
this.name = name;
}
public String getName() {
return name;
}
public boolean execute(Context context) throws Exception {
handle((Request) context);
return true;
}
public void handle(Request request) throws ProcessException {
try {
String name = request.getName();
Response response = new ResponseContext(name);
request.setResponse(response);
} catch (Exception e) {
throw new ProcessException(e);
}
}
}
HandlerCommand 的handle(Request) 方法实现了这个类的主要职责:为请求创建一个响应。 execute(Context) 方法是一个委托给 handle 方法的适配器。现在我们可以调用 execute 或 handle 并获得相同的结果。构造函数为 HandlerCommand 的每个实例分配一个名称,以便它可以与请求匹配。
这里显示的 handle(Request) 方法不是很有用。但是,它将通过我们的测试并证明基础架构正在运行。子类可以覆盖 handle(Request) 来为给定的请求创建适当的响应。由于 HandlerCommands 仍然是 Commands,我们可以将 HandlerCommand 子类逐项列出为元数据(一个 XML 文档)。随着我们的应用程序的增长,这将使处理新请求变得容易。
HandlerCommand 类创建一个 ResponseContext 实例并将其设置为 Response。 ResponseContext 类如下所示。
ResponseContext 的许多其他实现是可能的。他们只需要实现 Response 并扩展 ContextBase。
package org.apache.commons.agility.impl;
import org.apache.commons.agility.Response;
import org.apache.commons.chain.impl.ContextBase;
public class ResponseContext extends ContextBase implements Response {
private String name;
public ResponseContext(String name) {
super();
this.name = name;
}
public String getName() {
return name;
}
}
由于我们只是在测试基础设施,我们的 ResponseContext 是初级的。 Web 应用程序框架的前端控制器可能会为响应定义多个属性,例如服务器页面的位置。 RequestHandler 可以创建可能需要的任何类型的 Response 对象。
无论我们需要什么 RequestHandlers,都可以作为元数据或以编程方式添加到目录中。 我们的测试以编程方式添加处理程序,因此我们需要实现 AddHandler 方法。 下面展示了我们对 CatalogController 的实现。 RequestHandlers 可以通过编程方式或通过元数据添加到 CatalogController
package org.apache.commons.agility.impl;
import org.apache.commons.agility.Controller;
import org.apache.commons.agility.ProcessException;
import org.apache.commons.agility.Request;
import org.apache.commons.agility.RequestHandler;
import org.apache.commons.chain.impl.CatalogBase;
import org.apache.commons.chain.Command;
public class ControllerCatalog extends CatalogBase implements Controller {
public RequestHandler getHandler(String name) {
return (RequestHandler) getCommand(request.getName());
}
public void addHandler(RequestHandler handler) {
this.addCommand(handler.getName(), (Command) handler);
}
public void process(Request request) throws ProcessException {
Handler handler = getHandler(request.getName());
if (handler != null) handler.handle(request);
}
}
我们控制器的主要入口点是 process(Request) 方法。这种方法可以承载大量的功能。我们甚至可以将 process 方法实现为一系列命令或命令链。然后,应用程序可以通过在元数据目录中指定不同的命令来微调请求处理。 Struts Web 应用程序框架将这种方法用于其请求处理器。
但是现在,我们只想通过我们的测试。所有流程方法需要做的就是找到RequestHandler并调用它的handle(Request)方法。我们只需在目录中查找请求的名称并检索匹配的请求处理程序(或命令)即可做到这一点。
addHandler(RequestHandler) 方法是另一个委托给继承方法的适配器。在这种情况下,addHandler 调用 addCommand(String,Command)。由于我们的 RequestHandlers 是命令,它们可以传递给超类方法。 getHandler(String) 方法是另一个适配器/委托。
最后但并非最不重要的是 RequestContext 类,如下所示。
package org.apache.commons.agility.impl;
import org.apache.commons.agility.Request;
import org.apache.commons.agility.Response;
import org.apache.commons.chain.impl.ContextBase;
public class RequestContext extends ContextBase implements Request {
private String name;
private Response response;
public RequestContext(String name) {
super();
this.name = name;
}
public String getName() {
return name;
}
public Response getResponse() {
return response;
}
public void setResponse(Response response) {
this.response = response;
}
}
与 ResponseContext 一样,应用程序可以向其 Request 类添加多个属性。 Web 应用程序可能会包装或传输来自 HttpServletRequest 的属性。 但是只要类实现了 Request 和 Context,它就会插入到我们的 Controller 实现中。
使用此处显示的接口和基类,您可以创建所需的任何控制器。
评论区