19. Spring MVC 集成
本章展示了如何将 Web Flow 集成到 Spring MVC 的 Web 应用程序中。 booking-mvc 示例应用程序是使用 Web Flow 的 Spring MVC 的良好参考。 该应用程序是一个简化的旅行网站,允许用户搜索和预订酒店房间。
19.1. 配置web.xml
使用 Spring MVC 的第一步是在 web.xml 中配置 DispatcherServlet。 通常每个 Web 应用程序只执行一次此操作。
The following example maps all requests that begin with /spring/ to DispatcherServlet. An init-param is used to provide the contextConfigLocation. This is the configuration file for the web application.
<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>
19.2. 分发到流程
DispatcherServlet 将应用程序资源的请求映射到处理程序。 一种流程就是一种类型的处理程序。
19.2.1. 注册FlowHandlerAdapter
将请求分发到流程的第一步是在 Spring MVC 中启用流程处理。 为此,请按如下方式安装 FlowHandlerAdapter:
<!-- Enables FlowHandler URL mapping -->
<bean class="org.springframework.webflow.mvc.servlet.FlowHandlerAdapter">
<property name="flowExecutor" ref="flowExecutor" />
</bean>
19.2.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的流,则该流将处理请求。如果未找到流,则会查询调度程序有序链中的下一个处理程序映射,或者返回noHandlerFound响应。
19.2.3. 流程处理工作流
当找到有效的流程映射时,FlowHandlerAdapter 会根据 HTTP 请求中存在的信息,确定是启动该流程的新执行还是恢复现有的执行。 适配器采用了一些与启动和恢复流程执行相关的默认设置:
-
HTTP 请求参数在所有启动流执行的输入映射中可用。
-
当一个流程执行结束且未发送最终响应时,默认处理程序会尝试在同一请求中启动新的执行。
-
未处理的异常会传播到
Dispatcher,除非该异常是NoSuchFlowExecutionException。默认处理器会尝试通过重新开始新的执行来从NoSuchFlowExecutionException中恢复。
See the API 文档 for FlowHandlerAdapter 以获取更多信息。 您可以通过子类化或实现自己的 FlowHandler 来覆盖这些默认值,如下一节中所讨论的。
19.3. 实现自定义流程处理器
FlowHandler 是可以用来定制在 HTTP servlet 环境中如何使用流的扩展点。 一个 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。 -
覆盖
createExecutionInputMap(HttpServletRequest)当你需要对从HttpServletRequest中提取流输入参数进行细粒度控制时。默认情况下,所有请求参数都被视为流输入参数。 -
覆盖
handleExecutionOutcome当你需要以自定义方式处理特定的流执行结果时。默认行为是发送重定向到已结束流的 URL,以重新启动该流的新执行。 -
覆盖
handleException当你需要对未处理的流异常进行细粒度控制时。默认行为是当客户端尝试访问已结束或过期的流执行时,尝试重新启动流。默认情况下,任何其他异常都会重新抛给 Spring MVC 的ExceptionResolver基础设施。
19.3.1. 示例 FlowHandler
Spring MVC 和 Web Flow 之间的一种常见交互模式是,当流程结束时,流程会重定向到一个 @Controller。 FlowHandler 实例可以在不将流程定义本身与特定的控制器 URL 耦合的情况下实现这一点。 以下示例 FlowHandler 重定向到一个 Spring MVC 控制器:
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 结果将重定向以显示新的预订。 任何其他结果都将重定向回酒店的索引页面。
19.3.2. 部署自定义 FlowHandler
要安装自定义的 FlowHandler,您需要将其部署为一个 bean。 bean 的名称必须与该处理器应适用的流程的 ID 相匹配。 以下示例创建了一个与 hotels/booking 流程相匹配的 bean:
<bean name="hotels/booking" class="org.springframework.webflow.samples.booking.BookingFlowHandler" />
通过此配置,访问资源 /hotels/booking 会通过自定义的 BookingFlowHandler 启动 hotels/booking 流程。 当预订流程结束时,FlowHandler 会处理流程执行结果并重定向到适当的控制器。
19.3.3. FlowHandler 重定向
一个处理FlowExecutionOutcome或FlowException的FlowHandler返回一个String,以指示处理后要重定向到的资源。 在上一节所示的示例中,BookingFlowHandler会针对bookingConfirmed结果重定向到booking/show资源URI,而针对所有其他结果则重定向到hotels/index资源URI。
默认情况下,返回的资源位置是相对于当前 servlet 映射的。 这允许流程处理器通过使用相对路径重定向到应用程序中的其他控制器。 此外,还支持显式的重定向前缀,以满足需要更多控制的情况。
支持的显式重定向前缀包括:
-
servletRelative:: 重定向到相对于当前 servlet 的资源 -
contextRelative:: 重定向到相对于当前Web应用程序上下文路径的资源 -
serverRelative:: 重定向到相对于服务器根目录的资源 -
http://或https://: 重定向到完全限定的资源 URI
在流定义中使用 externalRedirect: 指令结合 view-state 或 end-state 时,同样支持这些重定向前缀——例如,view="externalRedirect:https://springframework.org"。
19.4. 视图解析
除非另有说明,Web Flow 会将选定的视图标识符映射到流工作目录中的文件。 对于现有的 Spring MVC 和 Web Flow 应用程序,外部的 ViewResolver 可能已经为您处理了此映射。 因此,为了继续使用该解析器并避免更改现有流视图的打包方式,您可以按如下方式配置 Web Flow:
<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 MVC 视图系统如何在 Spring Web Flow 中使用。 你可以使用它来配置现有的 ViewResolver 实例以及其他服务,例如自定义的 MessageCodesResolver。 你还可以通过将 useSpringBinding 标志设置为 true 来让数据绑定使用 Spring MVC 的原生 BeanWrapper。 这是使用 Unified EL 进行视图到模型数据绑定的一种替代方案。 更多信息,请参阅 此类的 JavaDoc API。
19.5. 从视图中发出事件信号
当流程进入 view-state 时,它会暂停,将用户重定向到其执行 URL,并等待用户事件以恢复。 事件通常通过激活按钮、链接或其他用户界面命令来触发。 事件在服务器端的解码方式取决于所使用的视图技术。 本节展示了如何从由模板引擎(如 JSP、Velocity 或 Freemarker)生成的基于 HTML 的视图中触发事件。
19.5.1. 使用命名的HTML按钮来触发事件
以下示例显示了同一表单上的两个按钮,分别在点击时发出 proceed 和 cancel 事件信号:
<input type="submit" name="_eventId_proceed" value="Proceed" />
<input type="submit" name="_eventId_cancel" value="Cancel" />
当按下按钮时,Web Flow 会找到以 _eventId_ 开头的请求参数名称,并将剩余的子字符串视为事件 ID。 因此在这个例子中,提交 \_eventId_proceed 会变成 proceed。 当同一个表单可以发出多个不同的事件时,应考虑使用这种样式。
19.5.2. 使用隐藏的HTML表单参数来信号通知事件
以下示例显示了一个在提交时触发 proceed 事件的表单:
<input type="submit" value="Proceed" />
<input type="hidden" name="_eventId" value="proceed" />
在这里,Web Flow 检测到特殊的 \_eventId 参数,并将其值用作事件 ID。 这种样式仅在表单上可以触发一个事件时才应考虑。
19.5.3. 使用 HTML 链接来触发事件
以下示例显示了一个在激活时发出 cancel 事件的链接:
<a href="${flowExecutionUrl}&_eventId=cancel">Cancel</a>
触发事件会导致向服务器发送 HTTP 请求。 在服务器端,流程会在其当前的 view-state 中处理解码事件。 此解码过程的具体实现取决于视图的实现方式。 回想一下,Spring MVC 视图实现会查找名为 _eventId 的请求参数。 如果没有找到 \_eventId 参数,视图会查找以 \_eventId_ 开头的参数,并将剩余的子字符串作为事件 ID。 如果两种情况都不存在,则不会触发任何流程事件。
19.6. 在页面上嵌入流程
默认情况下,当流程进入视图状态时,它会在渲染视图之前执行客户端重定向。 这种方法被称为 POST-REDIRECT-GET。 它的优点是将一个视图的表单处理与下一个视图的渲染分离开来。 因此,浏览器的“后退”和“刷新”按钮可以无缝工作,而不会引发任何浏览器警告。
通常,客户端重定向对用户来说是透明的。 然而,在某些情况下,POST-REDIRECT-GET 可能不会带来相同的好处。 例如,某个流程可能嵌入在页面中,并由 Ajax 请求驱动,仅刷新属于该流程的页面区域。 在这种情况下,不仅没有必要使用客户端重定向,而且就保持页面周围内容的完整性而言,这也不是理想的行为。
处理 Ajax 请求 部分解释了如何在 Ajax 请求期间进行局部渲染。 本节的重点是解释如何在 Ajax 请求期间控制流执行的重定向行为。 要指示流应以“页面嵌入”模式运行,在启动流时附加一个额外的参数,如下所示:
/hotels/booking?mode=embedded
当以“页面嵌入”模式启动时,流不会在 Ajax 请求期间发出流执行重定向。 mode=embedded 参数仅在启动流时需要传递。 您唯一需要关注的其他事项是使用 Ajax 请求,并且仅渲染更新显示流的页面部分所需的内容。
19.7. 将流程输出保存到 MVC Flash 范围
当end-state执行内部重定向时,您可以自动将流输出保存到MVC闪存范围。 这在流结束时显示摘要屏幕时特别有用。 为了向后兼容,默认情况下此功能是禁用的。 要启用它,请按照如下方法在您的FlowHandlerAdapter上将saveOutputToFlashScopeOnRedirect设置为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>