# 11. Spring MVC 集成

## 11.1.导言

本章展示了如何将 Web 流集成到 Spring MVC Web 应用程序中。对于具有 Web 流的 Spring MVC，`booking-mvc`示例应用程序是一个很好的参考。这个应用程序是一个简化的旅游网站，允许用户搜索和预订酒店房间。

## 11.2.配置 web.xml

使用 Spring MVC 的第一步是在`web.xml`中配置`DispatcherServlet`。你通常在每个 Web 应用程序中执行一次此操作。

下面的示例将所有以`/spring/`开头的请求映射到 DispatcherServlet。`init-param`用于提供`contextConfigLocation`。这是 Web 应用程序的配置文件。

```
<servlet>
	<servlet-name>Spring MVC Dispatcher Servlet</servlet-name>
	<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
	<init-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>/WEB-INF/web-application-config.xml</param-value>
	</init-param>
</servlet>

<servlet-mapping>
	<servlet-name>Spring MVC Dispatcher Servlet</servlet-name>
	<url-pattern>/spring/*</url-pattern>
</servlet-mapping>
```

## 11.3.向流量调度

`DispatcherServlet`将对应用程序资源的请求映射到处理程序。流是处理程序的一种类型。

### 11.3.1.注册 FlowhandlerAdapter

将请求发送到流的第一步是在 Spring MVC 中启用流处理。为此，请安装`FlowHandlerAdapter`:

```
<!-- Enables FlowHandler URL mapping -->
<bean class="org.springframework.webflow.mvc.servlet.FlowHandlerAdapter">
	<property name="flowExecutor" ref="flowExecutor" />
</bean>
			
```

### 11.3.2.定义流映射

一旦启用了流处理，下一步就是将特定的应用程序资源映射到你的流。最简单的方法是定义`FlowHandlerMapping`:

```
<!-- Maps request paths to flows in the flowRegistry;
	e.g. a path of /hotels/booking looks for a flow with id "hotels/booking" -->
<bean class="org.springframework.webflow.mvc.servlet.FlowHandlerMapping">
	<property name="flowRegistry" ref="flowRegistry"/>
	<property name="order" value="0"/>
</bean>
			
```

配置此映射允许 Dispatcher 将应用程序资源路径映射到流注册中心中的流。例如，访问资源路径`/hotels/booking`将导致使用 ID`hotels/booking`的流的注册表查询。如果找到带有该 ID 的流，则该流将处理该请求。如果没有找到流，那么将查询 Dispatcher 的有序链中的下一个处理程序映射，或者返回一个“nohandlerfound”响应。

### 11.3.3.流程处理工作流程

当找到一个有效的流映射时，`FlowHandlerAdapter`会根据 HTTP 请求提供的信息，计算出是启动该流的新执行，还是恢复现有的执行。有许多与适配器使用的流执行的启动和恢复相关的默认值：

* HTTP 请求参数在所有初始流执行的输入映射中都是可用的。

* 当流执行结束而未发送最终响应时，默认处理程序将尝试在同一请求中启动新的执行。

* 未处理的异常将传播到 Dispatcher，除非该异常是 NosuchFlowExecutionException 异常。默认处理程序将尝试通过重新执行 NosuchFlowExecutionException 来恢复 NosuchFlowExecutionException。

有关更多信息，请参阅`FlowHandlerAdapter`的 API 文档。你可以通过子类化或实现你自己的 Flowhandler（将在下一节中讨论）来覆盖这些默认值。

## 11.4.实现自定义 Flowhandlers

`FlowHandler`是可用于自定义在 HTTP Servlet 环境中如何执行流的扩展点。a`FlowHandler`由`FlowHandlerAdapter`使用，并负责：

* 返回要执行的流定义的`id`

* 创建输入以在该流开始时传递新的执行。

* 处理通过执行该流返回的结果，并在其结束时将其执行

* 处理由该流的执行引发的任何异常

这些职责在`org.springframework.mvc.servlet.FlowHandler`接口的定义中得到了说明：

```
public interface FlowHandler {

	public String getFlowId();

	public MutableAttributeMap createExecutionInputMap(HttpServletRequest request);

	public String handleExecutionOutcome(FlowExecutionOutcome outcome,
		HttpServletRequest request, HttpServletResponse response);

	public String handleException(FlowException e,
		HttpServletRequest request, HttpServletResponse response);
}
		
```

要实现 Flowhandler，子类`AbstractFlowHandler`。所有这些操作都是可选的，如果没有实现，那么将应用默认值。你只需要覆盖你需要的方法。具体而言：

* 当你的流的 ID 不能直接从 HTTP 请求派生时，覆盖`getFlowId(HttpServletRequest)`。默认情况下，要执行的流的 ID 是从请求 URI 的 PathInfo 部分派生的。例如，在默认情况下，`http://localhost/app/hotels/booking?hotelId=1`会导致流 ID 为`hotels/booking`。

* 当需要对从 HttpServletRequest 提取流输入参数进行细粒度控制时，覆盖`createExecutionInputMap(HttpServletRequest)`。默认情况下，所有请求参数都被视为流输入参数。

* 当需要以自定义方式处理特定的流执行结果时，覆盖`handleExecutionOutcome`。默认行为发送重定向到结束的流的 URL，以重新启动该流的新执行。

* 当需要对未处理的流异常进行细粒度控制时，覆盖`handleException`。当客户端试图访问已结束或过期的流执行时，默认行为会尝试重启流。默认情况下，任何其他异常都会被重新抛出到 Spring MVC ExceptionResolver 基础结构中。

### 11.4.1.Flowhandler 示例

Spring MVC 和 Web 流之间的一种常见交互模式是，当流结束时，将其重定向到 @Controller。Flowhandler 允许在不将流定义本身与特定的控制器 URL 耦合的情况下完成此操作。下面显示了一个重定向到 Spring MVC 控制器的 Flowhandler 示例：

```
public class BookingFlowHandler extends AbstractFlowHandler {
	public String handleExecutionOutcome(FlowExecutionOutcome outcome,
										HttpServletRequest request, HttpServletResponse response) {
		if (outcome.getId().equals("bookingConfirmed")) {
			return "/booking/show?bookingId=" + outcome.getOutput().get("bookingId");
		} else {
			return "/hotels/index";
		}
	}
}
			
```

由于此处理程序只需要以自定义的方式处理流执行结果，因此不重写其他内容。`bookingConfirmed`结果将导致重定向，以显示新的预订。任何其他结果将重定向回酒店索引页面。

### 11.4.2.部署自定义 Flowhandler

要安装自定义 Flowhandler，只需将其部署为 Bean。 Bean 名称必须与处理程序应用到的流的 ID 匹配。

```
<bean name="hotels/booking" class="org.springframework.webflow.samples.booking.BookingFlowHandler" />
			
```

通过这种配置，访问资源`/hotels/booking`将使用自定义 BookingFlowhandler 启动`hotels/booking`流。当预订流程结束时，Flowhandler 将处理流程执行结果并将其重定向到适当的控制器。

### 11.4.3.Flowhandler 重定向

处理 FlowExecutionResult 或 FlowException 的 Flowhandler 返回`String`，以指示处理后要重定向到的资源。在前面的示例中，对于`BookingFlowHandler`结果，`booking/show`资源 URI 重定向到`bookingConfirmed`，对于所有其他结果，`hotels/index`资源 URI 重定向到`booking/show`。

默认情况下，返回的资源位置是相对于当前 Servlet 映射的。这允许流处理程序使用相对路径重定向到应用程序中的其他控制器。此外，对于需要更多控制的情况，支持显式重定向前缀。

支持的显式重定向前缀如下：

* `servletRelative:`-相对于当前资源重定向 Servlet

* `contextRelative:`-相对于当前 Web 应用程序上下文路径重定向到资源

* `serverRelative:`-重定向到相对于服务器根目录的资源

* `http://`或`https://`-重定向到完全限定的资源 URI

当结合视图状态或结束状态使用`externalRedirect:`指令时，流定义中也支持这些相同的重定向前缀；例如，`view="externalRedirect:http://springframework.org"`

## 11.5.视图分辨率

除非另有说明，否则 Web Flow2 将选定的视图标识符映射到位于流的工作目录中的文件。对于现有的 Spring MVC+Web 流应用程序，外部`ViewResolver`可能已经在为你处理此映射。因此，要继续使用该解析器，并避免更改打包现有流视图的方式，请按以下方式配置 Web 流：

```
<webflow:flow-registry id="flowRegistry" flow-builder-services="flowBuilderServices">
	<webflow:location path="/WEB-INF/hotels/booking/booking.xml" />
</webflow:flow-registry>

<webflow:flow-builder-services id="flowBuilderServices" view-factory-creator="mvcViewFactoryCreator"/>

<bean id="mvcViewFactoryCreator" class="org.springframework.webflow.mvc.builder.MvcViewFactoryCreator">
	<property name="viewResolvers" ref="myExistingViewResolverToUseForFlows"/>
</bean>
		
```

MVCViewFactoryCreator 是一个工厂，它允许你配置如何在 Spring Web 流中使用 Spring MVC 视图系统。使用它来配置现有的 viewresolver，以及其他服务，例如定制的 MessageCodesresolver。你还可以通过将`useSpringBinding`标志设置为 true 来启用使用 Spring MVC 的本机 BeanWrapper 的数据绑定功能。这是使用统一 EL 进行视图到模型数据绑定的一种替代方法。有关更多信息，请参见该类的 Javadoc API。

## 11.6.从视图发送事件信号

当流进入视图状态时，它会暂停，将用户重定向到其执行 URL，并等待用户事件恢复。事件通常通过激活按钮、链接或其他用户界面命令来发出信号。事件如何在服务器端进行解码取决于所使用的视图技术。本节展示了如何从基于 HTML 的视图中触发事件，这些视图是由 JSP、Velocity 或 Freemarker 等模板引擎生成的。

### 11.6.1.使用命名的 HTML 按钮发送事件信号

下面的示例显示了相同窗体上的两个按钮，当单击时分别表示`proceed`和`cancel`事件。

```
<input type="submit" name="_eventId_proceed" value="Proceed" />
<input type="submit" name="_eventId_cancel" value="Cancel" />
			
```

当按下按钮时，WebFlow 查找一个以`_eventId_`开头的请求参数名，并将其余的子字符串作为事件 ID。所以在这个例子中，提交`_eventId_proceed`变成了`proceed`。当有几个不同的事件可以从相同的形式发出信号时，应该考虑这种样式。

### 11.6.2.使用隐藏的 HTML 表单参数来表示事件

下面的示例显示了一个表单，该表单在提交时表示`proceed`事件：

```
<input type="submit" value="Proceed" />
<input type="hidden" name="_eventId" value="proceed" />
			
```

在这里，WebFlow 只检测特殊的`_eventId`参数，并将其值用作事件 ID。只有当窗体上有一个事件可以用信号表示时，才应该考虑这种样式。

### 11.6.3.使用 HTML 链接发出事件信号

下面的示例显示了一个链接，该链接在激活时发出`cancel`事件的信号：

```
<a href="${flowExecutionUrl}&_eventId=cancel">Cancel</a>
			
```

触发一个事件会导致一个 HTTP 请求被发送回服务器。在服务器端，流处理从其当前视图状态中解码事件。这个解码过程的工作方式是特定于视图实现的。回想一下 Spring MVC 视图实现只是查找一个名为`_eventId`的请求参数。如果没有找到`_eventId`参数，则视图将查找一个以`_eventId_`开头的参数，并将剩余的子字符串用作事件 ID。如果两种情况都不存在，则不会触发流事件。

## 11.7.在页面上嵌入流

默认情况下，当流进入视图状态时，它会在呈现视图之前执行客户端重定向。这种方法被称为 post-redirect-get。它的优点是将一个视图的表单处理与下一个视图的呈现分离开来。因此，浏览器的后退和刷新按钮可以无缝地工作，而不会引起任何浏览器警告。

通常情况下，从用户的角度来看，客户端重定向是透明的。然而，在有些情况下，后重定向可能不会带来同样的好处。例如，流可以嵌入在页面上并通过 Ajax 请求只刷新属于该流的页面的区域来驱动。在这种情况下，不仅没有必要使用客户端重定向，而且在保持页面周围内容不变方面也不是所需的行为。

[第 12.5 节，“处理 Ajax 请求”](spring-js.html#spring-js-ajax)解释了如何在 Ajax 请求期间执行部分呈现。本节的重点是解释如何在 Ajax 请求期间控制流执行重定向行为。要指示一个流应该以“页面嵌入”模式执行，你所需要做的就是在启动该流时附加一个额外的参数：

```
/hotels/booking?mode=embedded
```

当以“嵌入页面”模式启动时，流将不会在 Ajax 请求期间发出流执行重定向。Mode=Embedded 参数只需要在启动流时传递。你唯一关心的另一个问题是使用 Ajax 请求，并且只呈现更新显示该流的页面部分所需的内容。

### 11.7.1.嵌入式模式与默认重定向行为

默认情况下，WebFlow 在进入每个视图状态时都会进行客户端重定向。但是，如果在 Ajax 请求期间保持相同的视图状态（例如，没有“to”属性的转换），那么就不会有客户端重定向。这种行为对于 Spring Web Flow2 用户应该非常熟悉。它适用于支持浏览器后退按钮的顶级流，同时还可以利用 Ajax 和部分呈现，用于在表单验证、分页槽搜索结果等视图中保持不变的用例。但是，在转换到新的视图状态之后，总是会进行客户端重定向。这使得不可能在页面或模态对话框中嵌入流并执行多个视图状态，而不会导致整页刷新。因此，如果你的用例需要嵌入一个流，你可以在“嵌入”模式中启动它。

### 11.7.2.嵌入式流程示例

如果你想看到嵌入在页面和模态对话框中的流的示例，请参阅 WebFlow-Showcase 项目。你可以在本地签出源代码，将其构建为 Maven 项目，并将其导入到 Eclipse 中：

```
cd some-directory
svn co https://src.springframework.org/svn/spring-samples/webflow-showcase
cd webflow-showcase
mvn package
# import into Eclipse
```

## 11.8.将流输出保存到 MVC 闪存范围

当`end-state`执行内部重定向时，流输出可以自动保存到 MVC 闪存范围。当在流的末尾显示摘要屏幕时，这一点特别有用。对于向后兼容性，默认情况下禁用此功能，以便将`saveOutputToFlashScopeOnRedirect`上的`FlowHandlerAdapter`设置为`true`。

```
<!-- Enables FlowHandler URL mapping -->
<bean class="org.springframework.webflow.mvc.servlet.FlowHandlerAdapter">
	<property name="flowExecutor" ref="flowExecutor" />
	<property name="saveOutputToFlashScopeOnRedirect" value="true" />
</bean>
		
```

下面的示例将在重定向到`summary`屏幕之前将`confirmationNumber`添加到 MVC 闪存范围。

```
<end-state id="finish" view="externalRedirect:summary">
	<output name="confirmationNumber" value="booking.confirmationNumber" />
</end-state>
		
```