SpringWebFlow

2,052 阅读15分钟

Jack ma

图片与内容无关
个人博客: aaatao66.github.io/

What is Spring-Web-Flow?

Spring Web Flow builds on Spring MVC and allows implementing the "flows" of a web application. A flow encapsulates a sequence of steps that guide a user through the execution of some business task. It spans multiple HTTP requests, has state, deals with transactional data, is reusable, and may be dynamic and long-running in nature..

机翻:

Spring Web Flow构建于Spring MVC之上,允许实现Web应用程序的“流程”。流程封装了一系列步骤,指导用户执行某些业务任务。它跨越多个HTTP请求,具有状态,处理事务数据,可重用,并且可能是动态的,并且本质上是长期运行的。

Spring Web Flow的Web应用程序,例如办理登机手续,申请贷款,购物车结帐,甚至向表单添加确认步骤。这些场景的共同点是以下一个或多个特征:

  • 有一个明确的开始和结束点。
  • 用户必须按特定顺序浏览一组屏幕。
  • 直到最后一步,更改才会完成。
  • 一旦完成,就不可能意外地重复交易

这是官方给出的 maven 依赖:

<dependencies>
    <dependency>
        <groupId>org.springframework.webflow</groupId>
        <artifactId>spring-webflow</artifactId>
        <version>2.5.0.RELEASE</version>
    </dependency>
</dependencies>

和 Gradle:

dependencies {
    compile 'org.springframework.webflow:spring-webflow:2.5.0.RELEASE'
}

以上, 来自 : projects.spring.io/spring-webf…

官方API 2.5.0 版本: docs.spring.io/spring-webf…


有时候,Web应用程序需要控制网络冲浪者的方向,引导他们一步步地访问应用。比较典型的例子就是电子商务站点的结账流程,从购物车开始,应用程序会引导你依次经过派送详情、账单信息以及最终的订单确认流程。


在Spring中配置Web Flow

设置好 webflow 的命名空间 :

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:flow="http://www.springframework.org/schema/webflow-config"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/webflow-config
       http://www.springframework.org/schema/webflow-config/spring-webflow-config.xsd
 ">

装配流程执行器 (flow executor)

流程执行器会为用户创建并启动一个流程执行实例。当流程暂停的时候(如为用户展示视图时),流程执行器会在用户执行操作后恢复流程。

就像汽车,你踩油门才会前进,当你停下来看风景的时候,它会停下,当你再次踩油门,它会恢复启动

<flow:flow-executor id="flowExecutor"/>
这个标签就是流程控制器

配置流程注册表 (flow registry)

流程执行器负责创建和执行流程,但它并不负责加载流程定义。所以我们需要注册表。

通过下面的标签(稍后会说id的事情)

<flow:flow-registry id="flowRegistry" base-path="/WEB-INF/flows">
        <flow:flow-location-pattern value="/**/*-flow.xml"/>
</flow:flow-registry>
  • base-path属性指明流程注册表会在“/WEB-INF/flows”目录下查找流程定义

  • <flow:flowlocation-pattern>元素的值表示:任何文件名以“-flow.xml”结尾的XML文件都将视为流程定义。

当然, 这个路径可以写死, 看你需要什么了 示例:

<flow:flow-registry id="flowRegistry">
        <flow:flow-location path="/WEB-INF/flows/carson.xml"/>
    <!-- 这个id是通过文件名, 也就是说 carson 就是id -->
</flow:flow-registry>
		 <!-- 也可以指定id👇 通过<flow:flow-location>元素的id属性来进行设置 -->
<flow:flow-location id="haha" path="/WEB-INF/flows/carson.xml"/>

注意中间的 标签 , 他们俩是不一样的

所有的流程都是通过其ID来进行引用的。流程的ID就是相对于base-path的路径——或者双星号所代表的路径 :

/WEB-INF/flows/order/order-flow.xml
这个路径里 order 就是我们的id

我们来解析一下:

  • /WEB-INF/flows:是基本路径
  • order: 流程 ID
  • order-flow.xml: 流程定义

处理流程请求

DispatcherServlet一般将请求分发给控制器。但是对于流程而言,我们需要一个lowHandlerMapping来帮助DispatcherServlet将流程请求发送给Spring Web Flow。在Spring应用上下文中,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。FlowHandlerAdapter等同于Spring MVC的控制器,它会响应发送的流程请求并对其进行处理。FlowHandlerAdapter可以像下面这样装配成一个Spring bean,如下所示:

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

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

流程的组件

流程是由三个主要元素定义的:状态、转移和流程数据。

  • 状态(State): 是流程中事件发生的地点。如果你将流程想象成公路旅行,那状态就是路途上的城镇、路边饭店以及风景点。流程中的状态是业务逻辑执行、做出决策或将页面展现给用户的地方,而不是在公路旅行中买乐事薯片和可口可乐的所在。

  • 转移 (transition): 如果流程状态就像公路旅行中停下来的地点,那转移(transition)就是连接这些点的公路。在流程中,你通过转移的方式从北京到上海,也就是从一个状态到另一个 状态。

  • 流程数据: 当你行走在伊拉克的公路上,你可能会捡到一把AK-47,你通常要把它带上,以便于防身,这就是流程数据。

通俗一点:

比如PUBG吃鸡类游戏, 状态就是你跟人对枪打架, 转移就是开车跑圈, 而流程数据就是你身上的M4+kar98k+手榴弹+平底锅......

状态

Spring Web Flow定义了五种不同类型的状态,通过选择Spring Web Flow的状态几乎可以把任意的安排功能构造成会话式的Web应用。尽管并不是所有的流程都需要下面的表所描述的状态,但最终你可能会经常使用它们中的大多数。

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

稍后我们将会看到如何将这些不同类型的状态组合起来形成一个完整的流程。但首先,让我们了解一下这些流程元素在Spring Web Flow定义中是如何表现的。

视图状态

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

在流程定义的XML文件中,<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的一些方法并根据方法调用的执行结果转移到另一个状态。 在流程定义XML中,行为状态使用<action-state>元素来声明。这里是一个例子:

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

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

Spring Web Flow与表达式语言

在1.0版本的时候,Spring Web Flow使用的是对象图导航语言(Object-Graph Navigation Language ,OGNL)。

随后的2.0版本又换成了统一表达式语言(Unified Expression Language ,Unified EL)。在2.1版本中,Spring Web Flow使用的是SpEL。 尽管可以使用上述的任意表达式语言来配置Spring Web Flow,但SpEL是默认和推荐使用的表达式语言。因此,当定义流程的时候,我们会选择使用SpEL,忽略掉其他的可选方案

决策状态

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

决策状态能够在流程执行时产生两个分支。决策状态将评估一个Boolean类型的表达式,然后在两个状态转移中选择一个,这要取决于表达式会计算出true还是false。在XML流程定义中,决策状态通过<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>可以这样声明:

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

<input>元素用于传递订单对象作为子流程的输入。如果子流程结束的<end-state>状态ID为orderCreated,那么流程将会转移到名为payment的状态。 在这里,我有点超出进度了,我们还没有讨论到<end-state>元素和转移。我们很快就会下面介绍转移。对于结束状态,这正是接下来要介绍的

结束状态

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

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

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

转移

转移连接了流程中的状态。流程中除结束状态之外的每个状态,至少都需要一个转移,这样就能够知道一旦这个状态完成时流程要去向哪里。状态可以有多个转移,分别对应于当前状态结束时可以执行的不同的路径。

转移使用<transition>元素来进行定义;

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

<transition to="customerReady"/>

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

如果触发了phoneEntered事件,流程将会进入lookupCustomer状态:

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

**在抛出异常时,**流程也可以进入另一个状态。例如,如果顾客的账号未存在,你可能希望流程转移到一个展现注册表单的视图状态。以下的 代码片段显示了这种类型的转移

<transition 
        on-exception="com.carson.taobao.service.CustomerNotFoundException"
        to="registrationForm" />

属性on-exception类似于on属性,只不过它指定了要发生转移的异常而不是一个事件。在本示例 中,CustomerNotFoundException异常将导致流程转移到registrationForm状态。

全局转移

在进入购买流程的时候,无论你有多少的子流程,都避免不了取消订单这个转移;

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

与其在多个状态中都重复通用的转移,我们可以将<transition>元素作为<global-transitions>的子元素,把它们定义为全局转移。 例如:

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

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

流程数据

再举个例子:

当你PUBG或者APEX的时候,转移就是跑圈,而你捡到的装备,都是属于流程数据;

在很多方面,流程与这些游戏是很类似的。当流程从一个状态进行到另一个状态时,它会带走一些数据。有时候,这些数据只需要很短的时间(可能只要展现页面给用户)。有时候,这些数据会在整个流程中传递并在流程结束的时候使用。

声明变量

流程数据保存在变量中,而变量可以在流程的各个地方进行引用。它能够以多种方式创建。在流程中创建变量的最简单形式是使用<var>元素:

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

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

另外,

也可以使用 <evaluate>标签创建变量

还有<set>标签也可以

定义流程数据的作用域

流程中携带的数据会拥有不同的生命作用域和可见性,这取决于保存数据的变量本身的作用域。Spring Web Flow定义了五种不同作用域,如:

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

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

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