第四版的第八章内容与第三版基本一致。
本章内容:
- 创建会话式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文件中添加相应的命名空间:
声明了命名空间后,就可以准备装配Web Flow的Bean了。
编写流程执行器
顾名思义,流程执行器(flow executor )就是用来驱动流程的执行。当用户进入到一个流程时,流程执行器会为该用户创建并启动一个流程执行器的实例。当流程暂停时(例如为用户展示视图时),流程执行器会在用户执行操作后恢复流程。
在Spring中,
元素可以创建一个流程执行器:
尽管流程执行器负责创建和执行流程,但它并不负责加载流程定义。这个要由流程注册表(flow registry)负责,下面会创建它。
配置流程注册表
流程注册表的工作就是加载流程定义,并让流程执行器可以使用它们。可以在Spring中使用
进行配置:
正如这里声明的,流程注册表会在/WEB-INF/flows
目录下寻找流程定义,这个路径是由base-path
属性指明的。根据
元素,任何以-flow.xml
结尾的XML文件都会被视为流程定义。
所有的流程都是通过其ID来进行引用的。使用
元素,流程的ID就是相对于base-path
的路径,或者是双星号所代表的路径,如下图展示了流程ID是如何计算的:
另外,你也可以不使用base-path
属性,直接显式地声明流程定义文件的位置:
这里使用了
而不是
,path
属性直接指定了/WEB-INF/flows/springpizza.xml
为流程定义文件。当这样定义时,流程的ID是从流程定义文件的文件名中获取的,这就是springpizza
。
如果你希望更显示地指定流程ID,那么可以通过
元素的id属性来进行设置。例如,要设定pizza作为流程ID,可以这样进行配置:
处理流程请求
正如前面的章节中提到的,DispatcherServlet
会将请求分发给控制器,但是对于流程而言,你需要FlowHandlerMapping
来帮助DispatcherServlet
将流程请求发送给Spring Web Flow。FlowHandlerMapping
的配置如下:
FlowHandlerMapping
装配了注册表的引用,这样它就知道如何将请求的URL匹配到流程上。例如,如果有一个ID为pizza的流程,FlowHandlerMapping
就会知道如果请求的URL是/pizza
的话,就会将其匹配到这个流程上。
然而,FlowHandlerMapping
的工作仅仅是将流程请求定向到Spring Web Flow,响应请求的是FlowHandlerAdapter
,它等同于Spring MVC的控制器,会对流程请求进行响应并处理。FlowHandlerAdapter
可以像下面这样装配成一个Spring 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来实现的。
在流程定义文件中,
用来定义视图状态:
在这个简单的示例中,id属性有两个含义。其一,它定义了流程中的状态。其二,因为这里没有其他地方指定视图,那么它就指定了流程到达这个状态时要展现的逻辑视图名称为welcome。
如果要显示地指定另外一个视图名称,那么就可以使用view
属性:
如果流程为用户展现了一个表单,你希望指定表单所绑定的对象,可以使用model
属性:
这里指定了takePayment视图将绑定流程范围内的paymentDetails对象。
行为状态
视图状态包括流程应用的用户,而行为状态则是应用程序自身在执行任务。行为状态一般会触发Spring所管理Bean的一些方法,并跟你讲方法调用的执行结果转移到另一个状态。
在流程定义文件中,行为状态使用
元素来声明:
尽管没有严格要求,但是
元素一般都有一个
子元素,该元素给出了行为状态要做的事情,expression
属性指定了进入这个状态时要评估的表达式。本例中,给出的是SpEL表达式,这表明它将会找到ID为pizzaFlowActions的Bean,并调用其saveOrder()方法。
决策状态
流程有可能会按照线性执行下去,从一个状态到另一个状态,没有其他的替代路线。但是更常见的是流程在某一个点根据流程当前情况进入不同的分支。
决策状态能够使得在流程执行时产生两个分支,它会评估一个Boolean表达式,根据结果是true还是false在两个状态转移中选择一个。在流程定义文件中,使用
元素来定义决策状态:
并不是单独工作的,
元素是其核心,它是进行表达式评估的地方,如果表达式结果为true,流程会转向then
属性指定的状态,为false会转向else
指定的状态中。
子流程状态
也许你不会将应用程序的所有逻辑都写在一个方法里,而是将其分散到多个类、方法一起其他结构中。
同样的,将流程分成独立的部分也是个不错的主意。
元素允许在一个正在执行的流程中调用另一个流程:
这里,元素作为子流程的输入被用于传递订单对象。如果子流程结束的
状态ID为orderCreated,那么本流程就会转移到ID为payment的状态。
结束状态
最后,所有的流程都要结束。这就是流程转移到结束状态时所做的。
元素指定了流程的结束:
当流程到达
时,流程就会结束。接下来发生什么要取决于以下几个因素:
- 如果结束的流程是个子流程,那么调用它的流程将会从
处继续执行。
的ID将会用作时间触发从
开始的转移。 - 如果
设置了view属性,那么就会渲染指定的视图。视图可以是相对于流程的路径,也可以是流程模板,使用externalRedirect:
前缀的会重定向到流程外部的页面,而使用flowRedirect:
前缀的则会重定向到另外一个流程。 - 如果结束的流程不是子流程也没有配置view属性,那么这个流程就会结束。浏览器最后将会加载流程的基本URL地址,同时,因为没有活动的流程,所以会开始一个新的流程实例。
需要注意的是一个流程可能有多个结束状态。因为子流程的结束状态ID确定了激活的事件,所以也许你会希望以多种结束状态来结束子流程,从而能够在调用流程中触发不同的事件,即使不是在子流程中,也有可能在结束流程后,根据流程的执行情况有多个显示页面供选择。
下面看一下流程是如何在状态间迁移的,如何在流程中通过定义转移来完成道路铺设。
转移
如前文所述,转移连接了流程中的状态。流程中除结束状态外的每个状态,至少需要一个转移,这样就知道在状态完成时的走向。一个状态也许有多个转移,分别表示当前状态结束时可以执行的不同路径。
转移是通过
元素来定义的,作为其他状态元素(
、
和
)的子元素。最简单的形式就是
元素在流程中指定下一个状态:
属性to
用于指定流程中的下一个状态。如果
元素只使用了to
属性,那么这个转移就会是当前状态的默认转移选项,如果没有其他可用转移的话,就会使用它。
更为常见的转移定义是基于事件的触发来进行的。在视图状态,事件通常会是用户采取的动作。在行为状态,事件是评估表达式得到的结果。而在子流程状态,事件取决于子流程结束状态的ID。在任意事件中,你可以使用on
属性来指定触发转移的事件:
在示例中,如果触发了phoneEntered事件流程,就会进入lookupCustomer状态。
在抛出异常时,流程也可能进入另一种状态。例如,如果没有找到顾客的记录,你可能希望流程转移到一个显示注册表单的视图状态,如下面:
属性on-exception
和属性on
十分类似,它是指定了要发生转移的异常而不是一个事件。
全局转移
在创建完流程后,也许你会发现有些状态使用了一些通用的转移。例如在整个流程中到处都有如下转移:
与其在多个流程状态中重复通用的转移,不如将其作为
的子元素,从而作为全局转移。
定义完全局转移,流程中所有的状态都会默认拥有这个cancel转移。
流程数据
当流程从一个状态到达另一个状态时,它会带走一些数据。有时这些数据很快就会被使用,比如直接展示给用户,有时这些数据需要在整个流程中传递并在流程结束时使用。
声明变量
流程数据是保存在变量中的,而变量可以在流程的任意位置进行引用,并且可以以多种方式进行创建。其中最简单的方式就是使用元素:
这里创建了一个新的Customer实例并将其放在customer变量中,这个变量可以在流程的任意状态下进行访问使用。
作为行为状态的一部分或者说作为视图状态的入口,也可以使用
元素来创建变量:
这里
元素计算了一个SpEL表达式,并将结果放到toppingsList变量中,这个变量是视图作用域的。
类似的,
元素也可以设置变量的值:
元素与
元素类似,都是讲变量设置为表达式计算的结果。这里我们设置了一个流程范围的pizza变量,它的值为Pizza对象的新实例。
流程数据的作用域
流程中所携带的数据都有其各自的生命周期,这取决于保存数据的变量本身的作用域,如下表:
范围 | 生命周期 |
---|---|
Conversation | 最高层级的流程开始时创建,在最高层级的流程结束时销毁。由最高层级的流程和其所有的子流程所共享 |
Flow | 当流程开始时创建,在流程结束时销毁。只在创建它的流程中是可见的 |
Request | 当一个请求进入流程时创建,流程返回时销毁 |
Flash | 流程开始时创建,流程结束时销毁。在视图状态解析后,才会被清除 |
View | 进入视图状态时创建,退出这个状态时销毁,只在视图状态内可见 |
当使用元素声明变量时,变量始终是流程作用域的,也就是在流程作用域内定义变量。当使用
或
时,作用域通过name或result属性的前缀指定。例如,将一个值赋给流程作用域的theAnswer变量:
到目前为止,我们已经看到了Web流程的所有原材料,下面要将其进行整合了,完成一个完整的流程。
组合起来:披萨流程
首先从构建一个高层次的流程开始,它定义了订购披萨的整体流程,然后将其拆分为多个子流程。
定义基本流程
当顾客访问Spizza网站时,他们需要进行用户识别、选择一个或多个披萨添加到订单、提供支付信息,然后提交订单,等待披萨上来,如下图:
下面展示Spring Web Flow的XML流程定义来实现披萨订单的整体流程:
流程定义中的第一件事就是声明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 pizzas;
private Payment payment;
public Order() {
pizzas = new ArrayList();
customer = new Customer();
}
//getters and setters
}
流程定义的主要组成部分是流程的状态,默认情况下,流程定义文件中的第一个状态会是流程访问的第一个状态。本例中就是identifyCustomer状态(一个子流程)。也可以通过
元素的start-state
属性来指定任意状态为开始状态:
...
识别顾客、构建披萨订单和支付这样的活动比较复杂,并不适合将其直接放在一个状态,而是以
元素展现的。
流程变量order将在前3个状态中进行填充并在第4个状态中进行保存。identifyCustomer子流程使用了元素来填充order的customer属性,将其设置为调用顾客子流程收到的输出。buildOrder和takePayment状态使用了不同的方式,它们使用
将order流程变量作为输入,这些子流程就能在其内部填充order对象。
在订单得到顾客、披萨以及支付信息后,就可以对其进行保存。saveOrder是处理这个任务的行为状态。它使用
来调用ID为pizzaFlowActions的Bean的saveOrder()方法,并将保存的订单对象传递进来。订单完成保存后会转移到thankCustomer。
thankCustomer状态是一个简单的视图状态,后台使用了/WEB-INF/flows/pizza/thankCustomer.jsp
文件进行展示:
Spizza
Thank you for your order!
Finish
]]>
该页面提供了一个完成流程的链接,它展示了用户与流程交互的唯一办法。
Spring Web Flow为视图的用户提供了一个flowExecutionUrl变量,它包含了流程的URL。结束链接将一个_eventId参数关联到URL上,以便返回到Web流程时触发finished事件。这个事件将会使流程到达结束状态。
流程将会在结束状态完成。由于在流程结束后没有下一步做什么具体信息,流程将会重新从identifyCustomer状态开始,以准备接受下一个订单。
下面还要定义identifyCustomer、buildOrder、takePayment这些子流程。
收集顾客信息
对于一个顾客,需要收集其电话、住址等信息,如下面的流程图:
这个流程不再是线性的,而是有了分支。例如在查找顾客后,流程可能结束,也可能转到注册表单。同样的,在checkDeliveryArea状态,顾客可能会被告警,也可能是不被告警。
程序清单:
下面将这个流程定义分解成一个个的状态。
询问电话号码
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"%>
Spring Pizza
Welcome to Spring Pizza!!!
这个简单的表单用来让用户输入电话号码,有两个特殊的部分,首先是隐藏的_flowExecutionKey
输入。当进入视图状态时,流程暂停并等待用户采取一些行为。当用户提交表单时,流程执行键会在_flowExecutionKey输入域中返回,并在流程暂停的位置进行恢复。
还需要注意提交按钮的名称_eventId_
部分是Spring Web Flow的一个线索,它表明了接下来要触发事件。当点击这个按钮提交表单时,就会触发phoneEntered事件,进而转移到lookupCustomer。
查找顾客
当欢迎顾客的表单提交后,顾客的电话号码将包含在请求参数中,并用于查询顾客。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" %>
Spring Pizza
Customer Registration
Phone number:
Name:
Address:
City:
State:
Zip Code:
该表单绑定到了Order.customer对象上。
检查配送区域
顾客提供了地址后,需要确认住址是否在配送范围内,因此使用了决策状态。
决策状态checkDeliveryArea有一个
元素,它将顾客的邮编传递到pizzaFlowActions Bean的checkDeliveryArea()方法中,该方法会返回一个Boolean值。
如果顾客在配送范围内,那么流程将转移到addCustomer状态,否则进入deliveryWarning视图状态。deliveryWarnin视图:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
Spring Pizza
Delivery Unavailable
The address is outside of our delivery area. The order
may still be taken for carry-out.
Accept |
Cancel
其中有两个链接,允许用户继续订单或者取消订单。通过使用与welcome状态相同的flowExecutionUrl变量,这些链接分别触发流程中的accept和cancel事件。如果发送的是accept事件,那么流程会转移到addCustomer状态。否则,子流程会转移到cancel状态。
存储顾客数据
addCustomer有一个
元素,它会调用pizzaFlowActions.addCustomer()方法,将order.customer流程参数传递进去。
一旦这个流程完成,就会执行默认转移,流程会转移到ID为customerReady的结束状态。
结束流程
当customer流程完成所有的路径后,会到达customerReady的结束状态。当调用它的披萨流程恢复时,它会接收到一个customerReady事件,这个事件将使得流程转移到buildOrder状态。
注意,customerReady结束状态包含了一个元素。在流程中,它等同于Java的
return
语句。它会从子流程中传递一些数据到调用流程。例如,元素返回customer变量,这样披萨流程中的identifyCustomer子流程状态就可以将其指定给订单。
另外,如果用户在任意地方触发了cancel事件,将会通过cancel状态结束流程,这也会在披萨流程中触发cancel事件并导致转移到披萨流程的结束状态。
构建订单
下面就是确定顾客想要什么样的披萨,提示用户创建披萨并将其放入订单,如图:
可以看到,showOrder状态位于订单子流程的中心位置。这是用户进入这个流程时的状态,也是用户添加披萨订单后转移的目标状态。它展现了订单的当前状态,并允许用户添加其他的披萨到订单中。
添加披萨订单时,会转移到createPizza状态。这是一个视图状态,允许用户对披萨进行选择。
在showOrder状态,用户可以提交订单,也可以取消。
这个子流程实际上回操作主流程创建的Order对象,在这里我们使用元素来将Order对象传递进流程。
接下来会看到showOrder状态,它是一个基本的视图状态,具有3个不同的转移,分别用于创建披萨、提交订单和取消订单。
createPizza的视图是一个表单,这个表单可以添加新的Pizza对象到订单。
元素添加了一个新的Pizza对象到流程作用域内,当表单提交时它将填充进订单。值得注意的是,这个视图状态引用的model是流程作用域同一个Pizza对象。Pizza对象将绑定到创建披萨的表单中:
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
Create Pizza
Size:
Toppings:
当通过Continue按钮提交订单时,尺寸和配料选择会绑定到Pizza对象中,并且触发addPizza
转移。与这个转移关联的
元素表明在转移到showOrder状态之前,流程作用域内的Pizza对象会传递给订单的addPizza()方法中。
有两种方法可以结束流程,用户可以点击showOrder视图中的Cancel按钮或者Checkout按钮。这两种操作都会使流程转移到一个
。但是选择的结束状态ID决定了退出这个流程时触发事件,进而最终确定主流程的下一个行为。主流程要么基于cancel要么基于orderCreated事件进行状态转移。在前者情况下,外边的流程会结束;后者,会转移到takePayment子流程。
支付
在披萨流程要结束的时候,最后的子流程提示用户输入他们的支付信息,如下图:
支付子流程也是使用元素接收一个Order对象作为输入。
可以看到,进入支付子流程的时候,用户会到达takePayment状态。这是一个视图状态,在这里用户可以选择信用卡、支票或者现金进行支付。提示支付信息后,进入verifyPayment状态,这是一个行为状态,会校验支付信息是否可以接受。
在流程进入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 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中的状态、转移甚至整个流程都可以借助
元素实现安全性,该元素会作为这些元素的子元素。例如,为了保护对一个视图状态的访问:
按照这里的配置,只有授权ROLE_ADMIN
访问权限(借助attributes属性)的用户才能访问这个视图状态。attributes属性使用逗号分隔的权限列表来表明用户要访问指定状态、转移或流程所需要的权限。match属性可以设置为any或all。如果是any,那么用户至上具备一个attributes属性所列的权限。如果的all,那么用户必须具有所有权限。具体见下一章。
如果觉得有用,欢迎关注我的微信,有问题可以直接交流: