Spring实战4之Spring Web Flow篇

来源:互联网 发布:apache显示目录列表 编辑:程序博客网 时间:2024/06/05 10:41

一、披萨流程
首先从构建一个高层次的流程开始,它定义了订购披萨的整体流程,然后将其拆分为多个子流程。
1.定义基本流程
当顾客访问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/webflowhttp://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状态(一个子流程)。也可以通过元素的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>
识别顾客、构建披萨订单和支付这样的活动比较复杂,并不适合将其直接放在一个状态,而是以元素展现的。流程变量order将在前3个状态中进行填充并在第4个状态中进行保存。identifyCustomer子流程使用了元素来填充order的customer属性,将其设置为调用顾客子流程收到的输出。buildOrder和takePayment状态使用了不同的方式,它们使用将order流程变量作为输入,这些子流程就能在其内部填充order对象。在订单得到顾客、披萨以及支付信息后,就可以对其进行保存。saveOrder是处理这个任务的行为状态。它使用来调用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这些子流程。

2.收集顾客信息
对于一个顾客,需要收集其电话、住址等信息,如下面的流程图:
这里写图片描述
识别顾客流程
这个流程不再是线性的,而是有了分支。例如在查找顾客后,流程可能结束,也可能转到注册表单。同样的,在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>

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

(1)询问电话号码
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。

(2)查找顾客
当欢迎顾客的表单提交后,顾客的电话号码将包含在请求参数中,并用于查询顾客。lookupCustomer状态的元素是查找发生的位置。它将电话号码从请求参数中抽取出来,并传递到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对象上。

(3)检查配送区域
顾客提供了地址后,需要确认住址是否在配送范围内,因此使用了决策状态。
决策状态checkDeliveryArea有一个元素,它将顾客的邮编传递到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状态。(4)存储顾客数据addCustomer有一个元素,它会调用pizzaFlowActions.addCustomer()方法,将order.customer流程参数传递进去。一旦这个流程完成,就会执行默认转移,流程会转移到ID为customerReady的结束状态。结束流程当customer流程完成所有的路径后,会到达customerReady的结束状态。当调用它的披萨流程恢复时,它会接收到一个customerReady事件,这个事件将使得流程转移到buildOrder状态。注意,customerReady结束状态包含了一个元素。在流程中,它等同于Java的return语句。它会从子流程中传递一些数据到调用流程。例如,元素返回customer变量,这样披萨流程中的identifyCustomer子流程状态就可以将其指定给订单。另外,如果用户在任意地方触发了cancel事件,将会通过cancel状态结束流程,这也会在披萨流程中触发cancel事件并导致转移到披萨流程的结束状态。3.构建订单下面就是确定顾客想要什么样的披萨,提示用户创建披萨并将其放入订单,如图:通过订单子流程添加披萨可以看到,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对象,在这里我们使用元素来将Order对象传递进流程。
接下来会看到showOrder状态,它是一个基本的视图状态,具有3个不同的转移,分别用于创建披萨、提交订单和取消订单。
createPizza的视图是一个表单,这个表单可以添加新的Pizza对象到订单。元素添加了一个新的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转移。与这个转移关联的元素表明在转移到showOrder状态之前,流程作用域内的Pizza对象会传递给订单的addPizza()方法中。
有两种方法可以结束流程,用户可以点击showOrder视图中的Cancel按钮或者Checkout按钮。这两种操作都会使流程转移到一个。但是选择的结束状态ID决定了退出这个流程时触发事件,进而最终确定主流程的下一个行为。主流程要么基于cancel要么基于orderCreated事件进行状态转移。在前者情况下,外边的流程会结束;后者,会转移到takePayment子流程。

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

支付子流程
支付子流程也是使用元素接收一个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视图时,元素将构建一个支付表单并使用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的结束。就像其他的子流程一样,不论哪种都会结束子流程并将控制交给主流程。但是所采用的id将决定主流程接下来的转移。
目前我们已经依次介绍了披萨流程及其子流程,下面快速了解下如何对流程及其状态的访问增加安全保护。
保护Web流程
Spring Web Flow中的状态、转移甚至整个流程都可以借助元素实现安全性,该元素会作为这些元素的子元素。例如,为了保护对一个视图状态的访问:

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

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