# 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将导致使用 IDhotels/booking的流的注册表查询。如果找到带有该 ID 的流,则该流将处理该请求。如果没有找到流,那么将查询 Dispatcher 的有序链中的下一个处理程序映射,或者返回一个“nohandlerfound”响应。

# 11.3.3.流程处理工作流程

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

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

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

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

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

# 11.4.实现自定义 Flowhandlers

FlowHandler是可用于自定义在 HTTP Servlet 环境中如何执行流的扩展点。aFlowHandlerFlowHandlerAdapter使用,并负责:

  • 返回要执行的流定义的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 按钮发送事件信号

下面的示例显示了相同窗体上的两个按钮,当单击时分别表示proceedcancel事件。

<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 请求”解释了如何在 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>