第4版的第8章内容与第3版基本1致。
本章内容:
互联网的1个奇特的地方就在于它很容易让人迷失。有如此多的内容可以查看和浏览,而超链接是其强大魔力的核心所在。
有时候,web利用程序需要控制web冲浪者的导向,引导他们1步步地访问利用。比如电子商务网站的付款流程,从购物车开始,利用程序会引导你顺次经过配送详情、账单信息和终究的定单确认。
Spring Web Flow是1个web框架,它适用于元素规定流程运行的程序。本章中,我们将会探索它是如何用于Spring Web框架平台的。
其实我们可使用任何的Web框架编写流程化的利用程序,比如使用Struts构建特定的流程。但是这样没有办法将流程与实现分开,你会发现流程的定义分散在组成流程的各个元素中,没有特定的地方能够完全地描写全部流程。
Spring Web Flow是Spring MVC的扩大,它支持开发基于流程的利用程序,可以将流程的定义和实现流程行动的类和视图分离开来。
在介绍Spring Web Flow的时候,我们会暂且放下Spittr样例,而使用生产披萨定单的web程序。
使用的第1步是在项目中进行安装,那末就从安装开始吧。
Spring Web Flow是基于Spring MVC构建的,这就意味着所有的流程要求都需要经过Spring MVC的DispatcherServlet。我们需要在Spring利用上下文中配置1些Bean来处理流程要求并履行流程。
现在还没有支持使用Java来配置Spring Web Flow,所以没得选,只能在XML中进行配置。有1些Bean会使用Spring Web Flow的Spring配置文件命名空间来进行声明,因此我们需要在上下文定义XML文件中添加相应的命名空间:
声明了命名空间后,就能够准备装配Web Flow的Bean了。
顾名思义,流程履行器(flow executor )就是用来驱动流程的履行。当用户进入到1个流程时,流程履行器会为该用户创建并启动1个流程履行器的实例。当流程暂停时(例如为用户展现视图时),流程履行器会在用户履行操作后恢复流程。
在Spring中,元素可以创建1个流程履行器:
虽然流程履行器负责创建和履行流程,但它其实不负责加载流程定义。这个要由流程注册表(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匹配到流程上。例如,如果有1个ID为pizza的流程,FlowHandlerMapping就会知道如果要求的URL是/pizza的话,就会将其匹配到这个流程上。
但是,FlowHandlerMapping的工作仅仅是将流程要求定向到Spring Web Flow,响应要求的是FlowHandlerAdapter,它同等于Spring MVC的控制器,会对流程要求进行响应并处理。FlowHandlerAdapter可以像下面这样装配成1个Spring Bean:
这个处理适配器就是DispatcherServlet和Spring Web Flow之间的桥梁。它会处理流程要求并管理基于这些要求的流程。在这里,它装配了流程履行器的援用,而后者是为要求履行流程的。
现在已配置了Spring Web Flow所需的Bean和组件,下面所需的就是真实的定义流程了。首先了解下流程的组成元素。
在Spring Web Flow中,流程是由3个主要元素组成的:状态(state)、转移(transition)和流程数据(flow data)。状态是流程中事件产生的地点。如果将流程想象成公路旅行,那末状态就是路途上的城镇、路边饭店和风景点等。流程中的状态是业务逻辑履行、做出决策或将页面展现给用户的地方,而不是在公路旅行中买薯片或可乐这些行动。
如果说流程状态是公路上停下来的地点,那末转移就是连接这些点的公路。在流程上,需要通过转移从1个状态到达另外一个状态。
在城镇间旅行的时候,可能需要购买1些记念品、留下1下回想。类似的,在流程处理进程中,它要搜集1些数据:流程当前状态等。或许你很想将其称为流程的状态,但是我们定义的状态已有了另外的含义。
Spring Web Flow定义了5种不同的状态,以下表所示。通过选择Spring Web Flow的状态几近可以把任意的安排功能构造成会话式的Web利用程序。虽然其实不是所有的流程都需要下表中的状态,但终究你可能会常常使用其中几个。
状态类型 | 作用 |
---|---|
行动(Action) | 流程逻辑产生的地方 |
决策(Decision) | 决策状态将流程分为两个方向,它会基于流程数据的评估结果肯定流程方向 |
结束(End) | 结束状态是流程的最后1站,进入End状态,流程就会终止 |
子流程(Subflow) | 子流程状态会在当前正在运行的流程上下文中启动1个新的流程 |
视图(View) | 视图状态会暂停流程并约请用户参与流程 |
首先了解下这些流程元素在Spring Web Flow定义中是如何表现的。
视图状态用来为用户展现信息并使用户在流程中发挥作用。实际的视图实现可以是Spring支持的任意视图类型,但通常是用JSP来实现的。
在流程定义文件中,用来定义视图状态:
在这个简单的示例中,id属性有两个含义。其1,它定义了流程中的状态。其2,由于这里没有其他地方指定视图,那末它就指定了流程到达这个状态时要展现的逻辑视图名称为welcome。
如果要显示地指定另外1个视图名称,那末就能够使用view属性:
如果流程为用户展现了1个表单,你希望指定表单所绑定的对象,可使用model属性:
这里指定了takePayment视图将绑定流程范围内的paymentDetails对象。
视图状态包括流程利用的用户,而行动状态则是利用程序本身在履行任务。行动状态1般会触发Spring所管理Bean的1些方法,并跟你讲方法调用的履行结果转移到另外一个状态。
在流程定义文件中,行动状态使用元夙来声明:
虽然没有严格要求,但是元素1般都有1个子元素,该元素给出了行动状态要做的事情,expression属性指定了进入这个状态时要评估的表达式。本例中,给出的是SpEL表达式,这表明它将会找到ID为pizzaFlowActions的Bean,并调用其saveOrder()方法。
流程有可能会依照线性履行下去,从1个状态到另外一个状态,没有其他的替换线路。但是更常见的是流程在某1个点根据流程当前情况进入不同的分支。
决策状态能够使得在流程履行时产生两个分支,它会评估1个Boolean表达式,根据结果是true还是false在两个状态转移当选择1个。在流程定义文件中,使用元夙来定义决策状态:
其实不是单独工作的,元素是其核心,它是进行表达式评估的地方,如果表达式结果为true,流程会转向then属性指定的状态,为false会转向else指定的状态中。
或许你不会将利用程序的所有逻辑都写在1个方法里,而是将其分散到多个类、方法1起其他结构中。
一样的,将流程分成独立的部份也是个不错的主张。元素允许在1个正在履行的流程中调用另外一个流程:
这里,元素作为子流程的输入被用于传递定单对象。如果子流程结束的状态ID为orderCreated,那末本流程就会转移到ID为payment的状态。
最后,所有的流程都要结束。这就是流程转移到结束状态时所做的。元素指定了流程的结束:
当流程到达时,流程就会结束。接下来产生甚么要取决于以下几个因素:
- 如果结束的流程是个子流程,那末调用它的流程将会从处继续履行。的ID将会用作时间触发从开始的转移。
- 如果设置了view属性,那末就会渲染指定的视图。视图可以是相对流程的路径,也能够是流程模板,使用externalRedirect:前缀的会重定向到流程外部的页面,而使用flowRedirect:前缀的则会重定向到另外1个流程。
- 如果结束的流程不是子流程也没有配置view属性,那末这个流程就会结束。阅读器最后将会加载流程的基本URL地址,同时,由于没有活动的流程,所以会开始1个新的流程实例。
需要注意的是1个流程可能有多个结束状态。由于子流程的结束状态ID肯定了激活的事件,所以或许你会希望以多种结束状态来结束子流程,从而能够在调用流程中触发不同的事件,即便不是在子流程中,也有可能在结束流程后,根据流程的履行情况有多个显示页面供选择。
下面看1下流程是如何在状态间迁移的,如何在流程中通过定义转移来完成道路铺设。
如前文所述,转移连接了流程中的状态。流程中除结束状态外的每一个状态,最少需要1个转移,这样就知道在状态完成时的走向。1个状态或许有多个转移,分别表示当前状态结束时可以履行的不同路径。
转移是通过元夙来定义的,作为其他状态元素(、和)的子元素。最简单的情势就是元素在流程中指定下1个状态:
属性to用于指定流程中的下1个状态。如果元素只使用了to属性,那末这个转移就会是当前状态的默许转移选项,如果没有其他可用转移的话,就会使用它。
更加常见的转移定义是基于事件的触发来进行的。在视图状态,事件通常会是用户采取的动作。在行动状态,事件是评估表达式得到的结果。而在子流程状态,事件取决于子流程结束状态的ID。在任意事件中,你可使用on属性来指定触发转移的事件:
在示例中,如果触发了phoneEntered事件流程,就会进入lookupCustomer状态。
在抛出异常时,流程也可能进入另外一种状态。例如,如果没有找到顾客的记录,你可能希望流程转移到1个显示注册表单的视图状态,以下面:
属性on-exception和属性on10分类似,它是指定了要产生转移的异常而不是1个事件。
在创建完流程后,或许你会发现有些状态使用了1些通用的转移。例如在全部流程中到处都有以下转移:
与其在多个流程状态中重复通用的转移,不如将其作为的子元素,从而作为全局转移。
定义完全局转移,流程中所有的状态都会默许具有这个cancel转移。
当流程从1个状态到达另外一个状态时,它会带走1些数据。有时这些数据很快就会被使用,比如直接展现给用户,有时这些数据需要在全部流程中传递并在流程结束时使用。
流程数据是保存在变量中的,而变量可以在流程的任意位置进行援用,并且可以以多种方式进行创建。其中最简单的方式就是使用元素:
这里创建了1个新的Customer实例并将其放在customer变量中,这个变量可以在流程的任意状态下进行访问使用。
作为行动状态的1部份或说作为视图状态的入口,也能够使用元夙来创建变量:
这里元素计算了1个SpEL表达式,并将结果放到toppingsList变量中,这个变量是视图作用域的。
类似的,元素也能够设置变量的值:
元素与元素类似,都是讲变量设置为表达式计算的结果。这里我们设置了1个流程范围的pizza变量,它的值为Pizza对象的新实例。
流程中所携带的数据都有其各自的生命周期,这取决于保存数据的变量本身的作用域,以下表:
范围 | 生命周期 |
---|---|
Conversation | 最高层级的流程开始时创建,在最高层级的流程结束时烧毁。由最高层级的流程和其所有的子流程所同享 |
Flow | 当流程开始时创建,在流程结束时烧毁。只在创建它的流程中是可见的 |
Request | 当1个要求进入流程时创建,流程返回时烧毁 |
Flash | 流程开始时创建,流程结束时烧毁。在视图状态解析后,才会被清除 |
View | 进入视图状态时创建,退出这个状态时烧毁,只在视图状态内可见 |
当使用元素声明变量时,变量始终是流程作用域的,也就是在流程作用域内定义变量。当使用或时,作用域通过name或result属性的前缀指定。例如,将1个值赋给流程作用域的theAnswer变量:
到目前为止,我们已看到了Web流程的所有原材料,下面要将其进行整合了,完成1个完全的流程。
首先从构建1个高层次的流程开始,它定义了订购披萨的整体流程,然后将其拆分为多个子流程。
当顾客访问Spizza网站时,他们需要进行用户辨认、选择1个或多个披萨添加到定单、提供支付信息,然后提交定单,等待披萨上来,以下图:
下面展现Spring Web Flow的XML流程定义来实现披萨定单的整体流程:
流程定义中的第1件事就是声明order变量。每次流程开始的时候都会创建1个Order实例。Order类会包括关于定单的所有信息、顾客信息、订购的披萨和支付信息等。
流程定义的主要组成部份是流程的状态,默许情况下,流程定义文件中的第1个状态会是流程访问的第1个状态。本例中就是identifyCustomer状态(1个子流程)。也能够通过元素的start-state属性来指定任意状态为开始状态:
辨认顾客、构建披萨定单和支付这样的活动比较复杂,其实不合适将其直接放在1个状态,而是以元素展现的。
流程变量order将在前3个状态中进行填充并在第4个状态中进行保存。identifyCustomer子流程使用了元夙来填充order的customer属性,将其设置为调用顾客子流程收到的输出。buildOrder和takePayment状态使用了不同的方式,它们使用将order流程变量作为输入,这些子流程就可以在其内部填充order对象。
在定单得到顾客、披萨和支付信息后,就能够对其进行保存。saveOrder是处理这个任务的行动状态。它使用来调用ID为pizzaFlowActions的Bean的saveOrder()方法,并将保存的定单对象传递进来。定单完成保存后会转移到thankCustomer。
thankCustomer状态是1个简单的视图状态,后台使用了/WEB-INF/flows/pizza/thankCustomer.jsp文件进行展现:
该页面提供了1个完成流程的链接,它展现了用户与流程交互的唯1办法。
Spring Web Flow为视图的用户提供了1个flowExecutionUrl变量,它包括了流程的URL。结束链接将1个_eventId参数关联到URL上,以便返回到Web流程时触发finished事件。这个事件将会使流程到达结束状态。
流程将会在结束状态完成。由于在流程结束后没有下1步做甚么具体信息,流程将会重新从identifyCustomer状态开始,以准备接受下1个定单。
下面还要定义identifyCustomer、buildOrder、takePayment这些子流程。
对1个顾客,需要搜集其电话、住址等信息,以下面的流程图:
这个流程不再是线性的,而是有了分支。例如在查找顾客后,流程可能结束,也可能转到注册表单。一样的,在checkDeliveryArea状态,顾客可能会被告警,也多是不被告警。
程序清单:
下面将这个流程定义分解成1个个的状态。
welcome状态是1个很简单的视图状态,它欢迎访问Spizza网站的顾客并要求输入电话。它有两个转移:如果从视图触发phoneEntered事件,就会定向到lookupCustomer,另外1个就是在全局转移中定义用来响应cancel事件的cancel转移。
页面代码:
这个简单的表单用来让用户输入电话号码,有两个特殊的部份,首先是隐藏的_flowExecutionKey输入。当进入视图状态时,流程暂停并等待用户采取1些行动。当用户提交表单时,流程履行键会在_flowExecutionKey输入域中返回,并在流程暂停的位置进行恢复。
还需要注意提交按钮的名称_eventId_部份是Spring Web Flow的1个线索,它表明了接下来要触发事件。当点击这个按钮提交表单时,就会触发phoneEntered事件,进而转移到lookupCustomer。
当欢迎顾客的表单提交后,顾客的电话号码将包括在要求参数中,并用于查询顾客。lookupCustomer状态的元素是查找产生的位置。它将电话号码从要求参数中抽取出来,并传递到pizzaFlowActions Bean的lookupCustomer()方法中。该方法要末返回Customer对象,要末抛出CustomerNotFoundException异常。
在前1种情况下,Customer对象会被设置到customer变量中(通过result属性)并默许的转移将流程带到customerReady状态。如果没有查到顾客,那末会抛出异常,流程会转移到registrationForm状态。
registrationForm要求用户填写配送地址:
该表单绑定到了Order.customer对象上。
顾客提供了地址后,需要确认住址是不是在配送范围内,因此使用了决策状态。
决策状态checkDeliveryArea有1个元素,它将顾客的邮编传递到pizzaFlowActions Bean的checkDeliveryArea()方法中,该方法会返回1个Boolean值。
如果顾客在配送范围内,那末流程将转移到addCustomer状态,否则进入deliveryWarning视图状态。deliveryWarnin视图:
其中有两个链接,允许用户继续定单或取消定单。通过使用与welcome状态相同的flowExecutionUrl变量,这些链接分别触发流程中的accept和cancel事件。如果发送的是accept事件,那末流程会转移到addCustomer状态。否则,子流程会转移到cancel状态。
addCustomer有1个元素,它会调用pizzaFlowActions.addCustomer()方法,将order.customer流程参数传递进去。
1旦这个流程完成,就会履行默许转移,流程会转移到ID为customerReady的结束状态。
当customer流程完成所有的路径后,会到达customerReady的结束状态。当调用它的披萨流程恢复时,它会接收到1个customerReady事件,这个事件将使得流程转移到buildOrder状态。
注意,customerReady结束状态包括了1个元素。在流程中,它同等于Java的return语句。它会从子流程中传递1些数据到调用流程。例如,元素返回customer变量,这样披萨流程中的identifyCustomer子流程状态就能够将其指定给定单。
另外,如果用户在任意地方触发了cancel事件,将会通过cancel状态结束流程,这也会在披萨流程中触发cancel事件并致使转移到披萨流程的结束状态。
下面就是肯定顾客想要甚么样的披萨,提示用户创建披萨并将其放入定单,如图:
可以看到,showOrder状态位于定单子流程的中心位置。这是用户进入这个流程时的状态,也是用户添加披萨定单后转移的目标状态。它展现了定单确当前状态,并允许用户添加其他的披萨到定单中。
添加披萨定单时,会转移到createPizza状态。这是1个视图状态,允许用户对披萨进行选择。
在showOrder状态,用户可以提交定单,也能够取消。