《Spring实战》学习笔记-第八章:使用Spring Web Flow

第四版的第八章内容与第三版基本一致。

本章内容:

  • 创建会话式web应用程序
  • 定义流程状态和行为
  • 保护web流程

互联网的一个奇特之处就在于它很容易让人迷失。有如此多的内容可以查看和阅读,而超链接是其强大魔力的核心所在。

有时候,web应用程序需要控制web冲浪者的导向,引导他们一步步地访问应用。比如电子商务网站的付款流程,从购物车开始,应用程序会引导你依次经过配送详情、账单信息以及最终的订单确认。

Spring Web Flow是一个web框架,它适用于元素规定流程运行的程序。本章中,我们将会探索它是如何用于Spring Web框架平台的。

其实我们可以使用任何的Web框架编写流程化的应用程序,比如使用Struts构建特定的流程。但是这样没有办法将流程与实现分开,你会发现流程的定义分散在组成流程的各个元素中,没有特定的地方能够完整地描述整个流程。

Spring Web Flow是Spring MVC的扩展,它支持开发基于流程的应用程序,可以将流程的定义和实现流程行为的类和视图分离开来。

在介绍Spring Web Flow的时候,我们会暂且放下Spittr样例,而使用生产披萨订单的web程序。

使用的第一步是在项目中进行安装,那么就从安装开始吧。

在Spring中配置Spring Web Flow

Spring Web Flow是基于Spring MVC构建的,这就意味着所有的流程请求都需要经过Spring MVC的DispatcherServlet。我们需要在Spring应用上下文中配置一些Bean来处理流程请求并执行流程。

现在还没有支持使用Java来配置Spring Web Flow,所以没得选,只能在XML中进行配置。有一些Bean会使用Spring Web Flow的Spring配置文件命名空间来进行声明,因此我们需要在上下文定义XML文件中添加相应的命名空间:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:flow="http://www.springframework.org/schema/webflow-config"
    xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/webflow-config 
   http://www.springframework.org/schema/webflow-config/spring-webflow-config-2.3.xsd
   http://www.springframework.org/schema/beans 
   http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
   http://www.springframework.org/schema/context 
   http://www.springframework.org/schema/context/spring-context-3.0.xsd">

声明了命名空间后,就可以准备装配Web Flow的Bean了。

编写流程执行器

顾名思义,流程执行器(flow executor )就是用来驱动流程的执行。当用户进入到一个流程时,流程执行器会为该用户创建并启动一个流程执行器的实例。当流程暂停时(例如为用户展示视图时),流程执行器会在用户执行操作后恢复流程。

在Spring中,<flow:flow-executor>元素可以创建一个流程执行器:
<flow:flow-executor id="flowExecutor" />

尽管流程执行器负责创建和执行流程,但它并不负责加载流程定义。这个要由流程注册表(flow registry)负责,下面会创建它。

配置流程注册表

流程注册表的工作就是加载流程定义,并让流程执行器可以使用它们。可以在Spring中使用<flow:flow-registry>进行配置:

<flow:flow-registry id="flowRegistry" base-path="/WEB-INF/flows">
    <flow:flow-location-pattern value="/**/*-flow.xml" />
</flow:flow-registry>

正如这里声明的,流程注册表会在/WEB-INF/flows目录下寻找流程定义,这个路径是由base-path属性指明的。根据<flow:flow-location-pattern>元素,任何以-flow.xml结尾的XML文件都会被视为流程定义。

所有的流程都是通过其ID来进行引用的。使用<flow:flow-location-pattern>元素,流程的ID就是相对于base-path的路径,或者是双星号所代表的路径,如下图展示了流程ID是如何计算的:

在使用流程定位模式时,流程定义文件相对于基本路径的路径将用作流程的id
在使用流程定位模式时,流程定义文件相对于基本路径的路径将用作流程的id

另外,你也可以不使用base-path属性,直接显式地声明流程定义文件的位置:

<flow:flow-registry id="flowRegistry">
    <flow:flow-location path="/WEB-INF/flows/springpizza.xml" />
</flow:flow-registry>

这里使用了<flow:flow-location>而不是<flow:flow-location-pattern>path属性直接指定了/WEB-INF/flows/springpizza.xml为流程定义文件。当这样定义时,流程的ID是从流程定义文件的文件名中获取的,这就是springpizza

如果你希望更显示地指定流程ID,那么可以通过<flow:flow-location>元素的id属性来进行设置。例如,要设定pizza作为流程ID,可以这样进行配置:

<flow:flow-registry id="flowRegistry">
    <flow:flow-location id="pizza"
        path="/WEB-INF/flows/springpizza.xml" />
</flow:flow-registry>

处理流程请求

正如前面的章节中提到的,DispatcherServlet会将请求分发给控制器,但是对于流程而言,你需要FlowHandlerMapping来帮助DispatcherServlet将流程请求发送给Spring Web Flow。FlowHandlerMapping的配置如下:

<bean class="org.springframework.webflow.mvc.servlet.FlowHandlerMapping">
    <property name="flowRegistry" ref="flowRegistry" />
</bean>

FlowHandlerMapping装配了注册表的引用,这样它就知道如何将请求的URL匹配到流程上。例如,如果有一个ID为pizza的流程,FlowHandlerMapping就会知道如果请求的URL是/pizza的话,就会将其匹配到这个流程上。

然而,FlowHandlerMapping的工作仅仅是将流程请求定向到Spring Web Flow,响应请求的是FlowHandlerAdapter,它等同于Spring MVC的控制器,会对流程请求进行响应并处理。FlowHandlerAdapter可以像下面这样装配成一个Spring Bean:

<bean class="org.springframework.webflow.mvc.servlet.FlowHandlerAdapter">
    <property name="flowExecutor" ref="flowExecutor" />
</bean>

这个处理适配器就是DispatcherServlet和Spring Web Flow之间的桥梁。它会处理流程请求并管理基于这些请求的流程。在这里,它装配了流程执行器的引用,而后者是为请求执行流程的。

现在已经配置了Spring Web Flow所需的Bean和组件,下面所需的就是真正的定义流程了。首先了解下流程的组成元素。

流程组件

在Spring Web Flow中,流程是由3个主要元素组成的:状态(state)、转移(transition)和流程数据(flow data)。状态是流程中事件发生的地点。如果将流程想象成公路旅行,那么状态就是路途上的城镇、路边饭店以及风景点等。流程中的状态是业务逻辑执行、做出决策或将页面展示给用户的地方,而不是在公路旅行中买薯片或者可乐这些行为。

如果说流程状态是公路上停下来的地点,那么转移就是连接这些点的公路。在流程上,需要通过转移从一个状态到达另一个状态。

在城镇间旅行的时候,可能需要购买一些纪念品、留下一下回忆。类似的,在流程处理过程中,它要收集一些数据:流程当前状况等。也许你很想将其称为流程的状态,但是我们定义的状态已经有了另外的含义。

状态

Spring Web Flow定义了5种不同的状态,如下表所示。通过选择Spring Web Flow的状态几乎可以把任意的安排功能构造成会话式的Web应用程序。尽管并不是所有的流程都需要下表中的状态,但最终你可能会经常使用其中几个。

状态类型 作用
行为(Action) 流程逻辑发生的地方
决策(Decision) 决策状态将流程分为两个方向,它会基于流程数据的评估结果确定流程方向
结束(End) 结束状态是流程的最后一站,进入End状态,流程就会终止
子流程(Subflow) 子流程状态会在当前正在运行的流程上下文中启动一个新的流程
视图(View) 视图状态会暂停流程并邀请用户参与流程

首先了解下这些流程元素在Spring Web Flow定义中是如何表现的。

视图状态

视图状态用来为用户展现信息并使用户在流程中发挥作用。实际的视图实现可以是Spring支持的任意视图类型,但通常是用JSP来实现的。

在流程定义文件中,<view-state>用来定义视图状态:

<view-state id="welcome" />

在这个简单的示例中,id属性有两个含义。其一,它定义了流程中的状态。其二,因为这里没有其他地方指定视图,那么它就指定了流程到达这个状态时要展现的逻辑视图名称为welcome。

如果要显示地指定另外一个视图名称,那么就可以使用view属性:

<view-state id="welcome" view="greeting" />

如果流程为用户展现了一个表单,你希望指定表单所绑定的对象,可以使用model属性:

<view-state id="takePayment" model="flowScope.paymentDetails"/>

这里指定了takePayment视图将绑定流程范围内的paymentDetails对象。

行为状态

视图状态包括流程应用的用户,而行为状态则是应用程序自身在执行任务。行为状态一般会触发Spring所管理Bean的一些方法,并跟你讲方法调用的执行结果转移到另一个状态。

在流程定义文件中,行为状态使用<action-state>元素来声明:

<action-state id="saveOrder">
    <evaluate expression="pizzaFlowActions.saveOrder(order)" />
    <transition to="thankYou" />
</action-state>

尽管没有严格要求,但是<action-state>元素一般都有一个<evaluate>子元素,该元素给出了行为状态要做的事情,expression属性指定了进入这个状态时要评估的表达式。本例中,给出的是SpEL表达式,这表明它将会找到ID为pizzaFlowActions的Bean,并调用其saveOrder()方法。

决策状态

流程有可能会按照线性执行下去,从一个状态到另一个状态,没有其他的替代路线。但是更常见的是流程在某一个点根据流程当前情况进入不同的分支。

决策状态能够使得在流程执行时产生两个分支,它会评估一个Boolean表达式,根据结果是true还是false在两个状态转移中选择一个。在流程定义文件中,使用<decision-state>元素来定义决策状态:

<decision-state id="checkDeliveryArea">
    <if test="pizzaFlowActions.checkDeliveryArea(customer.zipCode)"
        then="addCustomer"
        else="deliveryWarning" />
</decision-state>

<decision-state>并不是单独工作的,<if>元素是其核心,它是进行表达式评估的地方,如果表达式结果为true,流程会转向then属性指定的状态,为false会转向else指定的状态中。

子流程状态

也许你不会将应用程序的所有逻辑都写在一个方法里,而是将其分散到多个类、方法一起其他结构中。

同样的,将流程分成独立的部分也是个不错的主意。<subflow-state>元素允许在一个正在执行的流程中调用另一个流程:

<subflow-state id="order" subflow="pizza/order">
    <input name="order" value="order"/>
    <transition on="orderCreated" to="payment" />
</subflow-state>

这里,<input>元素作为子流程的输入被用于传递订单对象。如果子流程结束的<end-state>状态ID为orderCreated,那么本流程就会转移到ID为payment的状态。

结束状态

最后,所有的流程都要结束。这就是流程转移到结束状态时所做的。<end-state>元素指定了流程的结束:

<end-state id="customerReady" />

当流程到达<end-state>时,流程就会结束。接下来发生什么要取决于以下几个因素:

  • 如果结束的流程是个子流程,那么调用它的流程将会从<subflow-state>处继续执行。<end-state>的ID将会用作时间触发从<subflow-state>开始的转移。
  • 如果<end-state>设置了view属性,那么就会渲染指定的视图。视图可以是相对于流程的路径,也可以是流程模板,使用externalRedirect:前缀的会重定向到流程外部的页面,而使用flowRedirect:前缀的则会重定向到另外一个流程。
  • 如果结束的流程不是子流程也没有配置view属性,那么这个流程就会结束。浏览器最后将会加载流程的基本URL地址,同时,因为没有活动的流程,所以会开始一个新的流程实例。

需要注意的是一个流程可能有多个结束状态。因为子流程的结束状态ID确定了激活的事件,所以也许你会希望以多种结束状态来结束子流程,从而能够在调用流程中触发不同的事件,即使不是在子流程中,也有可能在结束流程后,根据流程的执行情况有多个显示页面供选择。

下面看一下流程是如何在状态间迁移的,如何在流程中通过定义转移来完成道路铺设。

转移

如前文所述,转移连接了流程中的状态。流程中除结束状态外的每个状态,至少需要一个转移,这样就知道在状态完成时的走向。一个状态也许有多个转移,分别表示当前状态结束时可以执行的不同路径。

转移是通过<transition>元素来定义的,作为其他状态元素(<action-state><view-state><subflow-state>)的子元素。最简单的形式就是<transition>元素在流程中指定下一个状态:

<transition to="customerReady" />

属性to用于指定流程中的下一个状态。如果<transition>元素只使用了to属性,那么这个转移就会是当前状态的默认转移选项,如果没有其他可用转移的话,就会使用它。

更为常见的转移定义是基于事件的触发来进行的。在视图状态,事件通常会是用户采取的动作。在行为状态,事件是评估表达式得到的结果。而在子流程状态,事件取决于子流程结束状态的ID。在任意事件中,你可以使用on属性来指定触发转移的事件:

<transition on="phoneEntered" to="lookupCustomer"/>

在示例中,如果触发了phoneEntered事件流程,就会进入lookupCustomer状态。

在抛出异常时,流程也可能进入另一种状态。例如,如果没有找到顾客的记录,你可能希望流程转移到一个显示注册表单的视图状态,如下面:

<transition on-exception="com.springinaction.pizza.service.CustomerNotFoundException"
    to="registrationForm" />

属性on-exception和属性on十分类似,它是指定了要发生转移的异常而不是一个事件。

全局转移

在创建完流程后,也许你会发现有些状态使用了一些通用的转移。例如在整个流程中到处都有如下转移:

<transition on="cancel" to="endState" />

与其在多个流程状态中重复通用的转移,不如将其作为<globaltransitions>的子元素,从而作为全局转移

<global-transitions>
    <transition on="cancel" to="endState" />
</global-transitions>

定义完全局转移,流程中所有的状态都会默认拥有这个cancel转移。

流程数据

当流程从一个状态到达另一个状态时,它会带走一些数据。有时这些数据很快就会被使用,比如直接展示给用户,有时这些数据需要在整个流程中传递并在流程结束时使用。

声明变量

流程数据是保存在变量中的,而变量可以在流程的任意位置进行引用,并且可以以多种方式进行创建。其中最简单的方式就是使用<var>元素:

<var name="customer" class="com.springinaction.pizza.domain.Customer"/>

这里创建了一个新的Customer实例并将其放在customer变量中,这个变量可以在流程的任意状态下进行访问使用。

作为行为状态的一部分或者说作为视图状态的入口,也可以使用<evaluate>元素来创建变量:

<evaluate result="viewScope.toppingsList"
    expression="T(com.springinaction.pizza.domain.Topping).asList()" />

这里<evaluate>元素计算了一个SpEL表达式,并将结果放到toppingsList变量中,这个变量是视图作用域的。

类似的,<set>元素也可以设置变量的值:

<set name="flowScope.pizza"
    value="new com.springinaction.pizza.domain.Pizza()" />

<set>元素与<evaluate>元素类似,都是讲变量设置为表达式计算的结果。这里我们设置了一个流程范围的pizza变量,它的值为Pizza对象的新实例。

流程数据的作用域

流程中所携带的数据都有其各自的生命周期,这取决于保存数据的变量本身的作用域,如下表:

范围 生命周期
Conversation 最高层级的流程开始时创建,在最高层级的流程结束时销毁。由最高层级的流程和其所有的子流程所共享
Flow 当流程开始时创建,在流程结束时销毁。只在创建它的流程中是可见的
Request 当一个请求进入流程时创建,流程返回时销毁
Flash 流程开始时创建,流程结束时销毁。在视图状态解析后,才会被清除
View 进入视图状态时创建,退出这个状态时销毁,只在视图状态内可见

当使用<var>元素声明变量时,变量始终是流程作用域的,也就是在流程作用域内定义变量。当使用<set><evaluate>时,作用域通过name或result属性的前缀指定。例如,将一个值赋给流程作用域的theAnswer变量:

<set name="flowScope.theAnswer" value="42"/>

到目前为止,我们已经看到了Web流程的所有原材料,下面要将其进行整合了,完成一个完整的流程。

组合起来:披萨流程

首先从构建一个高层次的流程开始,它定义了订购披萨的整体流程,然后将其拆分为多个子流程。

定义基本流程

当顾客访问Spizza网站时,他们需要进行用户识别、选择一个或多个披萨添加到订单、提供支付信息,然后提交订单,等待披萨上来,如下图:


网上购买披萨的流程
网上购买披萨的流程

下面展示Spring Web Flow的XML流程定义来实现披萨订单的整体流程:

<?xml version="1.0" encoding="UTF-8"?>
<flow xmlns="http://www.springframework.org/schema/webflow"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/webflow
http://www.springframework.org/schema/webflow/spring-webflow-2.3.xsd">
    <var name="order" class="com.springinaction.pizza.domain.Order" />
    <!-- 调用顾客子流程 -->
    <subflow-state id="identifyCustomer" subflow="pizza/customer">
        <output name="customer" value="order.customer" />
        <transition on="customerReady" to="buildOrder" />
    </subflow-state>
    <!-- 调用订单子流程 -->
    <subflow-state id="buildOrder" subflow="pizza/order">
        <input name="order" value="order" />
        <transition on="orderCreated" to="takePayment" />
    </subflow-state>
    <!-- 调用支付子流程 -->
    <subflow-state id="takePayment" subflow="pizza/payment">
        <input name="order" value="order" />
        <transition on="paymentTaken" to="saveOrder" />
    </subflow-state>
    <!-- 保存订单 -->
    <action-state id="saveOrder">
        <evaluate expression="pizzaFlowActions.saveOrder(order)" />
        <transition to="thankCustomer" />
    </action-state>
    <!-- 感谢顾客 -->
    <view-state id="thankCustomer">
        <transition to="endState" />
    </view-state>
    <end-state id="endState" />
    <!-- 全局取消转移 -->
    <global-transitions>
        <transition on="cancel" to="endState" />
    </global-transitions>
</flow>

流程定义中的第一件事就是声明order变量。每次流程开始的时候都会创建一个Order实例。Order类会包含关于订单的所有信息、顾客信息、订购的披萨以及支付信息等。

package com.springinaction.pizza.domain;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Configurable;

@Configurable("order")
public class Order implements Serializable {
   private static final long serialVersionUID = 1L;
   private Customer customer;
   private List<Pizza> pizzas;
   private Payment payment;

   public Order() {
      pizzas = new ArrayList<Pizza>();
      customer = new Customer();
   }
   
   //getters and setters
}   

流程定义的主要组成部分是流程的状态,默认情况下,流程定义文件中的第一个状态会是流程访问的第一个状态。本例中就是identifyCustomer状态(一个子流程)。也可以通过<flow>元素的start-state属性来指定任意状态为开始状态:

<?xml version="1.0" encoding="UTF-8"?>
<flow xmlns="http://www.springframework.org/schema/webflow"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/webflow
    http://www.springframework.org/schema/webflow/spring-webflow-2.3.xsd"
    start-state="identifyCustomer">
    ...
</flow>

识别顾客、构建披萨订单和支付这样的活动比较复杂,并不适合将其直接放在一个状态,而是以<subflow-state>元素展现的。

流程变量order将在前3个状态中进行填充并在第4个状态中进行保存。identifyCustomer子流程使用了<output>元素来填充order的customer属性,将其设置为调用顾客子流程收到的输出。buildOrder和takePayment状态使用了不同的方式,它们使用<input>将order流程变量作为输入,这些子流程就能在其内部填充order对象。

在订单得到顾客、披萨以及支付信息后,就可以对其进行保存。saveOrder是处理这个任务的行为状态。它使用<evaluate>来调用ID为pizzaFlowActions的Bean的saveOrder()方法,并将保存的订单对象传递进来。订单完成保存后会转移到thankCustomer。

thankCustomer状态是一个简单的视图状态,后台使用了/WEB-INF/flows/pizza/thankCustomer.jsp文件进行展示:

<html xmlns:jsp="http://java.sun.com/JSP/Page">
    <jsp:output omit-xml-declaration="yes" />
    <jsp:directive.page contentType="text/html;charset=UTF-8" />
    <head><title>Spizza</title></head>
    <body>
        <h2>Thank you for your order!</h2>
        <![CDATA[
        <a href='${flowExecutionUrl}&_eventId=finished'>Finish</a>
        ]]>
    </body>
</html>

该页面提供了一个完成流程的链接,它展示了用户与流程交互的唯一办法。

Spring Web Flow为视图的用户提供了一个flowExecutionUrl变量,它包含了流程的URL。结束链接将一个_eventId参数关联到URL上,以便返回到Web流程时触发finished事件。这个事件将会使流程到达结束状态。

流程将会在结束状态完成。由于在流程结束后没有下一步做什么具体信息,流程将会重新从identifyCustomer状态开始,以准备接受下一个订单。

下面还要定义identifyCustomer、buildOrder、takePayment这些子流程。

收集顾客信息

对于一个顾客,需要收集其电话、住址等信息,如下面的流程图:


识别顾客流程
识别顾客流程

这个流程不再是线性的,而是有了分支。例如在查找顾客后,流程可能结束,也可能转到注册表单。同样的,在checkDeliveryArea状态,顾客可能会被告警,也可能是不被告警。

程序清单:

<?xml version="1.0" encoding="UTF-8"?>
<flow xmlns="http://www.springframework.org/schema/webflow"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/webflow 
  http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd">

    <input name="order" required="true" />

    <!-- Customer -->
    <view-state id="welcome">
        <transition on="phoneEntered" to="lookupCustomer" />
        <transition on="cancel" to="cancel" />
    </view-state>

    <action-state id="lookupCustomer">
        <evaluate result="order.customer"
            expression="pizzaFlowActions.lookupCustomer(requestParameters.phoneNumber)" />
        <transition to="registrationForm"
            on-exception="com.springinaction.pizza.service.CustomerNotFoundException" />
        <transition to="customerReady" />
    </action-state>

    <view-state id="registrationForm" model="order" popup="true">
        <on-entry>
            <evaluate
                expression="order.customer.phoneNumber = requestParameters.phoneNumber" />
        </on-entry>
        <transition on="submit" to="checkDeliveryArea" />
        <transition on="cancel" to="cancel" />
    </view-state>

    <decision-state id="checkDeliveryArea">
        <if test="pizzaFlowActions.checkDeliveryArea(order.customer.zipCode)"
            then="addCustomer" else="deliveryWarning" />
    </decision-state>

    <view-state id="deliveryWarning">
        <transition on="accept" to="addCustomer" />
        <transition on="cancel" to="cancel" />
    </view-state>

    <action-state id="addCustomer">
        <evaluate expression="pizzaFlowActions.addCustomer(order.customer)" />
        <transition to="customerReady" />
    </action-state>

    <!-- End state -->
    <end-state id="cancel" />
    <end-state id="customerReady" />
</flow>

下面将这个流程定义分解成一个个的状态。

询问电话号码

welcome状态是一个很简单的视图状态,它欢迎访问Spizza网站的顾客并要求输入电话。它有两个转移:如果从视图触发phoneEntered事件,就会定向到lookupCustomer,另外一个就是在全局转移中定义用来响应cancel事件的cancel转移。

页面代码:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<html>

<head>
<title>Spring Pizza</title>
</head>

<body>
    <h2>Welcome to Spring Pizza!!!</h2>

    <form:form>
        <input type="hidden" name="_flowExecutionKey"
            value="${flowExecutionKey}" />
        <input type="text" name="phoneNumber" />
        <br />
        <input type="submit" name="_eventId_phoneEntered"
            value="Lookup Customer" />
    </form:form>
</body>
</html>

这个简单的表单用来让用户输入电话号码,有两个特殊的部分,首先是隐藏的_flowExecutionKey输入。当进入视图状态时,流程暂停并等待用户采取一些行为。当用户提交表单时,流程执行键会在_flowExecutionKey输入域中返回,并在流程暂停的位置进行恢复。

还需要注意提交按钮的名称_eventId_部分是Spring Web Flow的一个线索,它表明了接下来要触发事件。当点击这个按钮提交表单时,就会触发phoneEntered事件,进而转移到lookupCustomer。

查找顾客

当欢迎顾客的表单提交后,顾客的电话号码将包含在请求参数中,并用于查询顾客。lookupCustomer状态的<evaluate>元素是查找发生的位置。它将电话号码从请求参数中抽取出来,并传递到pizzaFlowActions Bean的lookupCustomer()方法中。该方法要么返回Customer对象,要么抛出CustomerNotFoundException异常。

在前一种情况下,Customer对象会被设置到customer变量中(通过result属性)并默认的转移将流程带到customerReady状态。如果没有查到顾客,那么会抛出异常,流程会转移到registrationForm状态。

注册新顾客

registrationForm要求用户填写配送地址:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<html>

  <head><title>Spring Pizza</title></head>

  <body>
    <h2>Customer Registration</h2>
    
    <form:form commandName="order">
      <input type="hidden" name="_flowExecutionKey" 
             value="${flowExecutionKey}"/>
      <b>Phone number: </b><form:input path="customer.phoneNumber"/><br/>
      <b>Name: </b><form:input path="customer.name"/><br/>
      <b>Address: </b><form:input path="customer.address"/><br/>
      <b>City: </b><form:input path="customer.city"/><br/>
      <b>State: </b><form:input path="customer.state"/><br/>
      <b>Zip Code: </b><form:input path="customer.zipCode"/><br/>
      <input type="submit" name="_eventId_submit" 
             value="Submit" />
      <input type="submit" name="_eventId_cancel" 
             value="Cancel" />
    </form:form>
    </body>
</html>

该表单绑定到了Order.customer对象上。

检查配送区域

顾客提供了地址后,需要确认住址是否在配送范围内,因此使用了决策状态。

决策状态checkDeliveryArea有一个<if>元素,它将顾客的邮编传递到pizzaFlowActions Bean的checkDeliveryArea()方法中,该方法会返回一个Boolean值。

如果顾客在配送范围内,那么流程将转移到addCustomer状态,否则进入deliveryWarning视图状态。deliveryWarnin视图:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
  <head><title>Spring Pizza</title></head>
  
  <body>
        <h2>Delivery Unavailable</h2>
        
        <p>The address is outside of our delivery area. The order
        may still be taken for carry-out.</p>
        
        <a href="${flowExecutionUrl}&_eventId=accept">Accept</a> | 
        <a href="${flowExecutionUrl}&_eventId=cancel">Cancel</a>
  </body>
</html>

其中有两个链接,允许用户继续订单或者取消订单。通过使用与welcome状态相同的flowExecutionUrl变量,这些链接分别触发流程中的accept和cancel事件。如果发送的是accept事件,那么流程会转移到addCustomer状态。否则,子流程会转移到cancel状态。

存储顾客数据

addCustomer有一个<evaluate>元素,它会调用pizzaFlowActions.addCustomer()方法,将order.customer流程参数传递进去。

一旦这个流程完成,就会执行默认转移,流程会转移到ID为customerReady的结束状态。

结束流程

当customer流程完成所有的路径后,会到达customerReady的结束状态。当调用它的披萨流程恢复时,它会接收到一个customerReady事件,这个事件将使得流程转移到buildOrder状态。

注意,customerReady结束状态包含了一个<output>元素。在流程中,它等同于Java的return语句。它会从子流程中传递一些数据到调用流程。例如,<output>元素返回customer变量,这样披萨流程中的identifyCustomer子流程状态就可以将其指定给订单。

另外,如果用户在任意地方触发了cancel事件,将会通过cancel状态结束流程,这也会在披萨流程中触发cancel事件并导致转移到披萨流程的结束状态。

构建订单

下面就是确定顾客想要什么样的披萨,提示用户创建披萨并将其放入订单,如图:


通过订单子流程添加披萨
通过订单子流程添加披萨

可以看到,showOrder状态位于订单子流程的中心位置。这是用户进入这个流程时的状态,也是用户添加披萨订单后转移的目标状态。它展现了订单的当前状态,并允许用户添加其他的披萨到订单中。

添加披萨订单时,会转移到createPizza状态。这是一个视图状态,允许用户对披萨进行选择。

在showOrder状态,用户可以提交订单,也可以取消。

<?xml version="1.0" encoding="UTF-8"?>
<flow xmlns="http://www.springframework.org/schema/webflow"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/webflow 
  http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd">

    <input name="order" required="true" />

    <!-- Order -->
    <view-state id="showOrder">
        <transition on="createPizza" to="createPizza" />
        <transition on="checkout" to="orderCreated" />
        <transition on="cancel" to="cancel" />
    </view-state>

    <view-state id="createPizza" model="flowScope.pizza">
        <on-entry>
            <set name="flowScope.pizza" value="new com.springinaction.pizza.domain.Pizza()" />

            <evaluate result="viewScope.toppingsList"
                expression="T(com.springinaction.pizza.domain.Topping).asList()" />
        </on-entry>
        <transition on="addPizza" to="showOrder">
            <evaluate expression="order.addPizza(flowScope.pizza)" />
        </transition>
        <transition on="cancel" to="showOrder" />
    </view-state>


    <!-- End state -->
    <end-state id="cancel" />
    <end-state id="orderCreated" />
</flow>

这个子流程实际上回操作主流程创建的Order对象,在这里我们使用<input>元素来将Order对象传递进流程。

接下来会看到showOrder状态,它是一个基本的视图状态,具有3个不同的转移,分别用于创建披萨、提交订单和取消订单。

createPizza的视图是一个表单,这个表单可以添加新的Pizza对象到订单。<on-entry>元素添加了一个新的Pizza对象到流程作用域内,当表单提交时它将填充进订单。值得注意的是,这个视图状态引用的model是流程作用域同一个Pizza对象。Pizza对象将绑定到创建披萨的表单中:

<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<div>

    <h2>Create Pizza</h2>
    <form:form commandName="pizza">
      <input type="hidden" name="_flowExecutionKey" 
          value="${flowExecutionKey}"/>
    
      <b>Size: </b><br/>
        <form:radiobutton path="size" label="Small (12-inch)" value="SMALL"/><br/>
        <form:radiobutton path="size" label="Medium (14-inch)" value="MEDIUM"/><br/>
        <form:radiobutton path="size" label="Large (16-inch)" value="LARGE"/><br/>
        <form:radiobutton path="size" label="Ginormous (20-inch)" value="GINORMOUS"/><br/>
      <br/>
      
      <b>Toppings: </b><br/>
      <form:checkboxes path="toppings" items="${toppingsList}" 
                       delimiter="<br/>"/><br/><br/>

          
      <input type="submit" class="button" 
          name="_eventId_addPizza" value="Continue"/>
      <input type="submit" class="button" 
          name="_eventId_cancel" value="Cancel"/>          
    </form:form>
</div>

当通过Continue按钮提交订单时,尺寸和配料选择会绑定到Pizza对象中,并且触发addPizza转移。与这个转移关联的<evaluate>元素表明在转移到showOrder状态之前,流程作用域内的Pizza对象会传递给订单的addPizza()方法中。

有两种方法可以结束流程,用户可以点击showOrder视图中的Cancel按钮或者Checkout按钮。这两种操作都会使流程转移到一个<end-state>。但是选择的结束状态ID决定了退出这个流程时触发事件,进而最终确定主流程的下一个行为。主流程要么基于cancel要么基于orderCreated事件进行状态转移。在前者情况下,外边的流程会结束;后者,会转移到takePayment子流程。

支付

在披萨流程要结束的时候,最后的子流程提示用户输入他们的支付信息,如下图:

支付子流程
支付子流程

支付子流程也是使用<input>元素接收一个Order对象作为输入。

可以看到,进入支付子流程的时候,用户会到达takePayment状态。这是一个视图状态,在这里用户可以选择信用卡、支票或者现金进行支付。提示支付信息后,进入verifyPayment状态,这是一个行为状态,会校验支付信息是否可以接受。

<?xml version="1.0" encoding="UTF-8"?>
<flow xmlns="http://www.springframework.org/schema/webflow"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.springframework.org/schema/webflow 
  http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd">

    <input name="order" required="true"/>
    
    <view-state id="takePayment" model="flowScope.paymentDetails">
        <on-entry>
          <set name="flowScope.paymentDetails" 
              value="new com.springinaction.pizza.domain.PaymentDetails()" />

          <evaluate result="viewScope.paymentTypeList" 
              expression="T(com.springinaction.pizza.domain.PaymentType).asList()" />
        </on-entry>
        <transition on="paymentSubmitted" to="verifyPayment" />
        <transition on="cancel" to="cancel" />
    </view-state>

    <action-state id="verifyPayment">
        <evaluate result="order.payment" expression=
            "pizzaFlowActions.verifyPayment(flowScope.paymentDetails)" />
        <transition to="paymentTaken" />
    </action-state>
            
    <!-- End state -->
    <end-state id="cancel" />
    <end-state id="paymentTaken" />
</flow>

在流程进入takePayment视图时,<on-entry>元素将构建一个支付表单并使用SpEL表达式在流程范围内创建PaymentDetails实例,该实例实际上是表单背后的对象。它也会创建视图作用域的paymentDetails变量,这个变量是一个包含了PaymentType enum的值的列表。在这里,SpEL的T()作用于PaymentType类,这样就可以调用静态的asList()方法。

package com.springinaction.pizza.domain;

import java.util.Arrays;
import java.util.List;

import org.apache.commons.lang3.text.WordUtils;

public enum PaymentType {
    CASH, CHECK, CREDIT_CARD;

    public static List<PaymentType> asList() {
        PaymentType[] all = PaymentType.values();
        return Arrays.asList(all);
    }

    @Override
    public String toString() {
        return WordUtils.capitalizeFully(name().replace('_', ' '));
    }
}

在面对支付表单的时候,用户可能提交支付,也可能会取消。根据做出的选择,支付子流程将名为paymentTaken或cancel的<end-state>结束。就像其他的子流程一样,不论哪种<end-state>都会结束子流程并将控制交给主流程。但是所采用的id将决定主流程接下来的转移。

目前我们已经依次介绍了披萨流程及其子流程,下面快速了解下如何对流程及其状态的访问增加安全保护。

保护Web流程

Spring Web Flow中的状态、转移甚至整个流程都可以借助<secured>元素实现安全性,该元素会作为这些元素的子元素。例如,为了保护对一个视图状态的访问:

<view-state id="restricted">
    <secured attributes="ROLE_ADMIN" match="all"/>
</view-state>

按照这里的配置,只有授权ROLE_ADMIN访问权限(借助attributes属性)的用户才能访问这个视图状态。attributes属性使用逗号分隔的权限列表来表明用户要访问指定状态、转移或流程所需要的权限。match属性可以设置为any或all。如果是any,那么用户至上具备一个attributes属性所列的权限。如果的all,那么用户必须具有所有权限。具体见下一章。



如果觉得有用,欢迎关注我的微信,有问题可以直接交流:

你的关注是对我最大的鼓励!
你的关注是对我最大的鼓励!
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容