《Spring實(shí)戰(zhàn)》學(xué)習(xí)筆記-第八章:使用Spring Web Flow

第四版的第八章內(nèi)容與第三版基本一致。

本章內(nèi)容:

  • 創(chuàng)建會話式web應(yīng)用程序
  • 定義流程狀態(tài)和行為
  • 保護(hù)web流程

互聯(lián)網(wǎng)的一個奇特之處就在于它很容易讓人迷失。有如此多的內(nèi)容可以查看和閱讀,而超鏈接是其強(qiáng)大魔力的核心所在。

有時候,web應(yīng)用程序需要控制web沖浪者的導(dǎo)向,引導(dǎo)他們一步步地訪問應(yīng)用。比如電子商務(wù)網(wǎng)站的付款流程,從購物車開始,應(yīng)用程序會引導(dǎo)你依次經(jīng)過配送詳情、賬單信息以及最終的訂單確認(rèn)。

Spring Web Flow是一個web框架,它適用于元素規(guī)定流程運(yùn)行的程序。本章中,我們將會探索它是如何用于Spring Web框架平臺的。

其實(shí)我們可以使用任何的Web框架編寫流程化的應(yīng)用程序,比如使用Struts構(gòu)建特定的流程。但是這樣沒有辦法將流程與實(shí)現(xiàn)分開,你會發(fā)現(xiàn)流程的定義分散在組成流程的各個元素中,沒有特定的地方能夠完整地描述整個流程。

Spring Web Flow是Spring MVC的擴(kuò)展,它支持開發(fā)基于流程的應(yīng)用程序,可以將流程的定義和實(shí)現(xiàn)流程行為的類和視圖分離開來。

在介紹Spring Web Flow的時候,我們會暫且放下Spittr樣例,而使用生產(chǎn)披薩訂單的web程序。

使用的第一步是在項(xiàng)目中進(jìn)行安裝,那么就從安裝開始吧。

在Spring中配置Spring Web Flow

Spring Web Flow是基于Spring MVC構(gòu)建的,這就意味著所有的流程請求都需要經(jīng)過Spring MVC的DispatcherServlet。我們需要在Spring應(yīng)用上下文中配置一些Bean來處理流程請求并執(zhí)行流程。

現(xiàn)在還沒有支持使用Java來配置Spring Web Flow,所以沒得選,只能在XML中進(jìn)行配置。有一些Bean會使用Spring Web Flow的Spring配置文件命名空間來進(jìn)行聲明,因此我們需要在上下文定義XML文件中添加相應(yīng)的命名空間:

<?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">

聲明了命名空間后,就可以準(zhǔn)備裝配Web Flow的Bean了。

編寫流程執(zhí)行器

顧名思義,流程執(zhí)行器(flow executor )就是用來驅(qū)動流程的執(zhí)行。當(dāng)用戶進(jìn)入到一個流程時,流程執(zhí)行器會為該用戶創(chuàng)建并啟動一個流程執(zhí)行器的實(shí)例。當(dāng)流程暫停時(例如為用戶展示視圖時),流程執(zhí)行器會在用戶執(zhí)行操作后恢復(fù)流程。

在Spring中,<flow:flow-executor>元素可以創(chuàng)建一個流程執(zhí)行器:
<flow:flow-executor id="flowExecutor" />

盡管流程執(zhí)行器負(fù)責(zé)創(chuàng)建和執(zhí)行流程,但它并不負(fù)責(zé)加載流程定義。這個要由流程注冊表(flow registry)負(fù)責(zé),下面會創(chuàng)建它。

配置流程注冊表

流程注冊表的工作就是加載流程定義,并讓流程執(zhí)行器可以使用它們。可以在Spring中使用<flow:flow-registry>進(jìn)行配置:

<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屬性指明的。根據(jù)<flow:flow-location-pattern>元素,任何以-flow.xml結(jié)尾的XML文件都會被視為流程定義。

所有的流程都是通過其ID來進(jìn)行引用的。使用<flow:flow-location-pattern>元素,流程的ID就是相對于base-path的路徑,或者是雙星號所代表的路徑,如下圖展示了流程ID是如何計(jì)算的:

在使用流程定位模式時,流程定義文件相對于基本路徑的路徑將用作流程的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為流程定義文件。當(dāng)這樣定義時,流程的ID是從流程定義文件的文件名中獲取的,這就是springpizza

如果你希望更顯示地指定流程ID,那么可以通過<flow:flow-location>元素的id屬性來進(jìn)行設(shè)置。例如,要設(shè)定pizza作為流程ID,可以這樣進(jìn)行配置:

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

處理流程請求

正如前面的章節(jié)中提到的,DispatcherServlet會將請求分發(fā)給控制器,但是對于流程而言,你需要FlowHandlerMapping來幫助DispatcherServlet將流程請求發(fā)送給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,響應(yīng)請求的是FlowHandlerAdapter,它等同于Spring MVC的控制器,會對流程請求進(jìn)行響應(yīng)并處理。FlowHandlerAdapter可以像下面這樣裝配成一個Spring Bean:

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

這個處理適配器就是DispatcherServlet和Spring Web Flow之間的橋梁。它會處理流程請求并管理基于這些請求的流程。在這里,它裝配了流程執(zhí)行器的引用,而后者是為請求執(zhí)行流程的。

現(xiàn)在已經(jīng)配置了Spring Web Flow所需的Bean和組件,下面所需的就是真正的定義流程了。首先了解下流程的組成元素。

流程組件

在Spring Web Flow中,流程是由3個主要元素組成的:狀態(tài)(state)、轉(zhuǎn)移(transition)和流程數(shù)據(jù)(flow data)。狀態(tài)是流程中事件發(fā)生的地點(diǎn)。如果將流程想象成公路旅行,那么狀態(tài)就是路途上的城鎮(zhèn)、路邊飯店以及風(fēng)景點(diǎn)等。流程中的狀態(tài)是業(yè)務(wù)邏輯執(zhí)行、做出決策或?qū)㈨撁嬲故窘o用戶的地方,而不是在公路旅行中買薯片或者可樂這些行為。

如果說流程狀態(tài)是公路上停下來的地點(diǎn),那么轉(zhuǎn)移就是連接這些點(diǎn)的公路。在流程上,需要通過轉(zhuǎn)移從一個狀態(tài)到達(dá)另一個狀態(tài)。

在城鎮(zhèn)間旅行的時候,可能需要購買一些紀(jì)念品、留下一下回憶。類似的,在流程處理過程中,它要收集一些數(shù)據(jù):流程當(dāng)前狀況等。也許你很想將其稱為流程的狀態(tài),但是我們定義的狀態(tài)已經(jīng)有了另外的含義。

狀態(tài)

Spring Web Flow定義了5種不同的狀態(tài),如下表所示。通過選擇Spring Web Flow的狀態(tài)幾乎可以把任意的安排功能構(gòu)造成會話式的Web應(yīng)用程序。盡管并不是所有的流程都需要下表中的狀態(tài),但最終你可能會經(jīng)常使用其中幾個。

狀態(tài)類型 作用
行為(Action) 流程邏輯發(fā)生的地方
決策(Decision) 決策狀態(tài)將流程分為兩個方向,它會基于流程數(shù)據(jù)的評估結(jié)果確定流程方向
結(jié)束(End) 結(jié)束狀態(tài)是流程的最后一站,進(jìn)入End狀態(tài),流程就會終止
子流程(Subflow) 子流程狀態(tài)會在當(dāng)前正在運(yùn)行的流程上下文中啟動一個新的流程
視圖(View) 視圖狀態(tài)會暫停流程并邀請用戶參與流程

首先了解下這些流程元素在Spring Web Flow定義中是如何表現(xiàn)的。

視圖狀態(tài)

視圖狀態(tài)用來為用戶展現(xiàn)信息并使用戶在流程中發(fā)揮作用。實(shí)際的視圖實(shí)現(xiàn)可以是Spring支持的任意視圖類型,但通常是用JSP來實(shí)現(xiàn)的。

在流程定義文件中,<view-state>用來定義視圖狀態(tài):

<view-state id="welcome" />

在這個簡單的示例中,id屬性有兩個含義。其一,它定義了流程中的狀態(tài)。其二,因?yàn)檫@里沒有其他地方指定視圖,那么它就指定了流程到達(dá)這個狀態(tài)時要展現(xiàn)的邏輯視圖名稱為welcome。

如果要顯示地指定另外一個視圖名稱,那么就可以使用view屬性:

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

如果流程為用戶展現(xiàn)了一個表單,你希望指定表單所綁定的對象,可以使用model屬性:

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

這里指定了takePayment視圖將綁定流程范圍內(nèi)的paymentDetails對象。

行為狀態(tài)

視圖狀態(tài)包括流程應(yīng)用的用戶,而行為狀態(tài)則是應(yīng)用程序自身在執(zhí)行任務(wù)。行為狀態(tài)一般會觸發(fā)Spring所管理Bean的一些方法,并跟你講方法調(diào)用的執(zhí)行結(jié)果轉(zhuǎn)移到另一個狀態(tài)。

在流程定義文件中,行為狀態(tài)使用<action-state>元素來聲明:

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

盡管沒有嚴(yán)格要求,但是<action-state>元素一般都有一個<evaluate>子元素,該元素給出了行為狀態(tài)要做的事情,expression屬性指定了進(jìn)入這個狀態(tài)時要評估的表達(dá)式。本例中,給出的是SpEL表達(dá)式,這表明它將會找到ID為pizzaFlowActions的Bean,并調(diào)用其saveOrder()方法。

決策狀態(tài)

流程有可能會按照線性執(zhí)行下去,從一個狀態(tài)到另一個狀態(tài),沒有其他的替代路線。但是更常見的是流程在某一個點(diǎn)根據(jù)流程當(dāng)前情況進(jìn)入不同的分支。

決策狀態(tài)能夠使得在流程執(zhí)行時產(chǎn)生兩個分支,它會評估一個Boolean表達(dá)式,根據(jù)結(jié)果是true還是false在兩個狀態(tài)轉(zhuǎn)移中選擇一個。在流程定義文件中,使用<decision-state>元素來定義決策狀態(tài):

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

<decision-state>并不是單獨(dú)工作的,<if>元素是其核心,它是進(jìn)行表達(dá)式評估的地方,如果表達(dá)式結(jié)果為true,流程會轉(zhuǎn)向then屬性指定的狀態(tài),為false會轉(zhuǎn)向else指定的狀態(tài)中。

子流程狀態(tài)

也許你不會將應(yīng)用程序的所有邏輯都寫在一個方法里,而是將其分散到多個類、方法一起其他結(jié)構(gòu)中。

同樣的,將流程分成獨(dú)立的部分也是個不錯的主意。<subflow-state>元素允許在一個正在執(zhí)行的流程中調(diào)用另一個流程:

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

這里,<input>元素作為子流程的輸入被用于傳遞訂單對象。如果子流程結(jié)束的<end-state>狀態(tài)ID為orderCreated,那么本流程就會轉(zhuǎn)移到ID為payment的狀態(tài)。

結(jié)束狀態(tài)

最后,所有的流程都要結(jié)束。這就是流程轉(zhuǎn)移到結(jié)束狀態(tài)時所做的。<end-state>元素指定了流程的結(jié)束:

<end-state id="customerReady" />

當(dāng)流程到達(dá)<end-state>時,流程就會結(jié)束。接下來發(fā)生什么要取決于以下幾個因素:

  • 如果結(jié)束的流程是個子流程,那么調(diào)用它的流程將會從<subflow-state>處繼續(xù)執(zhí)行。<end-state>的ID將會用作時間觸發(fā)從<subflow-state>開始的轉(zhuǎn)移。
  • 如果<end-state>設(shè)置了view屬性,那么就會渲染指定的視圖。視圖可以是相對于流程的路徑,也可以是流程模板,使用externalRedirect:前綴的會重定向到流程外部的頁面,而使用flowRedirect:前綴的則會重定向到另外一個流程。
  • 如果結(jié)束的流程不是子流程也沒有配置view屬性,那么這個流程就會結(jié)束。瀏覽器最后將會加載流程的基本URL地址,同時,因?yàn)闆]有活動的流程,所以會開始一個新的流程實(shí)例。

需要注意的是一個流程可能有多個結(jié)束狀態(tài)。因?yàn)樽恿鞒痰慕Y(jié)束狀態(tài)ID確定了激活的事件,所以也許你會希望以多種結(jié)束狀態(tài)來結(jié)束子流程,從而能夠在調(diào)用流程中觸發(fā)不同的事件,即使不是在子流程中,也有可能在結(jié)束流程后,根據(jù)流程的執(zhí)行情況有多個顯示頁面供選擇。

下面看一下流程是如何在狀態(tài)間遷移的,如何在流程中通過定義轉(zhuǎn)移來完成道路鋪設(shè)。

轉(zhuǎn)移

如前文所述,轉(zhuǎn)移連接了流程中的狀態(tài)。流程中除結(jié)束狀態(tài)外的每個狀態(tài),至少需要一個轉(zhuǎn)移,這樣就知道在狀態(tài)完成時的走向。一個狀態(tài)也許有多個轉(zhuǎn)移,分別表示當(dāng)前狀態(tài)結(jié)束時可以執(zhí)行的不同路徑。

轉(zhuǎn)移是通過<transition>元素來定義的,作為其他狀態(tài)元素(<action-state><view-state><subflow-state>)的子元素。最簡單的形式就是<transition>元素在流程中指定下一個狀態(tài):

<transition to="customerReady" />

屬性to用于指定流程中的下一個狀態(tài)。如果<transition>元素只使用了to屬性,那么這個轉(zhuǎn)移就會是當(dāng)前狀態(tài)的默認(rèn)轉(zhuǎn)移選項(xiàng),如果沒有其他可用轉(zhuǎn)移的話,就會使用它。

更為常見的轉(zhuǎn)移定義是基于事件的觸發(fā)來進(jìn)行的。在視圖狀態(tài),事件通常會是用戶采取的動作。在行為狀態(tài),事件是評估表達(dá)式得到的結(jié)果。而在子流程狀態(tài),事件取決于子流程結(jié)束狀態(tài)的ID。在任意事件中,你可以使用on屬性來指定觸發(fā)轉(zhuǎn)移的事件:

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

在示例中,如果觸發(fā)了phoneEntered事件流程,就會進(jìn)入lookupCustomer狀態(tài)。

在拋出異常時,流程也可能進(jìn)入另一種狀態(tài)。例如,如果沒有找到顧客的記錄,你可能希望流程轉(zhuǎn)移到一個顯示注冊表單的視圖狀態(tài),如下面:

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

屬性on-exception和屬性on十分類似,它是指定了要發(fā)生轉(zhuǎn)移的異常而不是一個事件。

全局轉(zhuǎn)移

在創(chuàng)建完流程后,也許你會發(fā)現(xiàn)有些狀態(tài)使用了一些通用的轉(zhuǎn)移。例如在整個流程中到處都有如下轉(zhuǎn)移:

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

與其在多個流程狀態(tài)中重復(fù)通用的轉(zhuǎn)移,不如將其作為<globaltransitions>的子元素,從而作為全局轉(zhuǎn)移

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

定義完全局轉(zhuǎn)移,流程中所有的狀態(tài)都會默認(rèn)擁有這個cancel轉(zhuǎn)移。

流程數(shù)據(jù)

當(dāng)流程從一個狀態(tài)到達(dá)另一個狀態(tài)時,它會帶走一些數(shù)據(jù)。有時這些數(shù)據(jù)很快就會被使用,比如直接展示給用戶,有時這些數(shù)據(jù)需要在整個流程中傳遞并在流程結(jié)束時使用。

聲明變量

流程數(shù)據(jù)是保存在變量中的,而變量可以在流程的任意位置進(jìn)行引用,并且可以以多種方式進(jìn)行創(chuàng)建。其中最簡單的方式就是使用<var>元素:

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

這里創(chuàng)建了一個新的Customer實(shí)例并將其放在customer變量中,這個變量可以在流程的任意狀態(tài)下進(jìn)行訪問使用。

作為行為狀態(tài)的一部分或者說作為視圖狀態(tài)的入口,也可以使用<evaluate>元素來創(chuàng)建變量:

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

這里<evaluate>元素計(jì)算了一個SpEL表達(dá)式,并將結(jié)果放到toppingsList變量中,這個變量是視圖作用域的。

類似的,<set>元素也可以設(shè)置變量的值:

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

<set>元素與<evaluate>元素類似,都是講變量設(shè)置為表達(dá)式計(jì)算的結(jié)果。這里我們設(shè)置了一個流程范圍的pizza變量,它的值為Pizza對象的新實(shí)例。

流程數(shù)據(jù)的作用域

流程中所攜帶的數(shù)據(jù)都有其各自的生命周期,這取決于保存數(shù)據(jù)的變量本身的作用域,如下表:

范圍 生命周期
Conversation 最高層級的流程開始時創(chuàng)建,在最高層級的流程結(jié)束時銷毀。由最高層級的流程和其所有的子流程所共享
Flow 當(dāng)流程開始時創(chuàng)建,在流程結(jié)束時銷毀。只在創(chuàng)建它的流程中是可見的
Request 當(dāng)一個請求進(jìn)入流程時創(chuàng)建,流程返回時銷毀
Flash 流程開始時創(chuàng)建,流程結(jié)束時銷毀。在視圖狀態(tài)解析后,才會被清除
View 進(jìn)入視圖狀態(tài)時創(chuàng)建,退出這個狀態(tài)時銷毀,只在視圖狀態(tài)內(nèi)可見

當(dāng)使用<var>元素聲明變量時,變量始終是流程作用域的,也就是在流程作用域內(nèi)定義變量。當(dāng)使用<set><evaluate>時,作用域通過name或result屬性的前綴指定。例如,將一個值賦給流程作用域的theAnswer變量:

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

到目前為止,我們已經(jīng)看到了Web流程的所有原材料,下面要將其進(jìn)行整合了,完成一個完整的流程。

組合起來:披薩流程

首先從構(gòu)建一個高層次的流程開始,它定義了訂購披薩的整體流程,然后將其拆分為多個子流程。

定義基本流程

當(dāng)顧客訪問Spizza網(wǎng)站時,他們需要進(jìn)行用戶識別、選擇一個或多個披薩添加到訂單、提供支付信息,然后提交訂單,等待披薩上來,如下圖:


網(wǎng)上購買披薩的流程
網(wǎng)上購買披薩的流程

下面展示Spring Web Flow的XML流程定義來實(shí)現(xiàn)披薩訂單的整體流程:

<?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" />
    <!-- 調(diào)用顧客子流程 -->
    <subflow-state id="identifyCustomer" subflow="pizza/customer">
        <output name="customer" value="order.customer" />
        <transition on="customerReady" to="buildOrder" />
    </subflow-state>
    <!-- 調(diào)用訂單子流程 -->
    <subflow-state id="buildOrder" subflow="pizza/order">
        <input name="order" value="order" />
        <transition on="orderCreated" to="takePayment" />
    </subflow-state>
    <!-- 調(diào)用支付子流程 -->
    <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" />
    <!-- 全局取消轉(zhuǎn)移 -->
    <global-transitions>
        <transition on="cancel" to="endState" />
    </global-transitions>
</flow>

流程定義中的第一件事就是聲明order變量。每次流程開始的時候都會創(chuàng)建一個Order實(shí)例。Order類會包含關(guān)于訂單的所有信息、顧客信息、訂購的披薩以及支付信息等。

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
}   

流程定義的主要組成部分是流程的狀態(tài),默認(rèn)情況下,流程定義文件中的第一個狀態(tài)會是流程訪問的第一個狀態(tài)。本例中就是identifyCustomer狀態(tài)(一個子流程)。也可以通過<flow>元素的start-state屬性來指定任意狀態(tài)為開始狀態(tài):

<?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>

識別顧客、構(gòu)建披薩訂單和支付這樣的活動比較復(fù)雜,并不適合將其直接放在一個狀態(tài),而是以<subflow-state>元素展現(xiàn)的。

流程變量order將在前3個狀態(tài)中進(jìn)行填充并在第4個狀態(tài)中進(jìn)行保存。identifyCustomer子流程使用了<output>元素來填充order的customer屬性,將其設(shè)置為調(diào)用顧客子流程收到的輸出。buildOrder和takePayment狀態(tài)使用了不同的方式,它們使用<input>將order流程變量作為輸入,這些子流程就能在其內(nèi)部填充order對象。

在訂單得到顧客、披薩以及支付信息后,就可以對其進(jìn)行保存。saveOrder是處理這個任務(wù)的行為狀態(tài)。它使用<evaluate>來調(diào)用ID為pizzaFlowActions的Bean的saveOrder()方法,并將保存的訂單對象傳遞進(jìn)來。訂單完成保存后會轉(zhuǎn)移到thankCustomer。

thankCustomer狀態(tài)是一個簡單的視圖狀態(tài),后臺使用了/WEB-INF/flows/pizza/thankCustomer.jsp文件進(jìn)行展示:

<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。結(jié)束鏈接將一個_eventId參數(shù)關(guān)聯(lián)到URL上,以便返回到Web流程時觸發(fā)finished事件。這個事件將會使流程到達(dá)結(jié)束狀態(tài)。

流程將會在結(jié)束狀態(tài)完成。由于在流程結(jié)束后沒有下一步做什么具體信息,流程將會重新從identifyCustomer狀態(tài)開始,以準(zhǔn)備接受下一個訂單。

下面還要定義identifyCustomer、buildOrder、takePayment這些子流程。

收集顧客信息

對于一個顧客,需要收集其電話、住址等信息,如下面的流程圖:


識別顧客流程
識別顧客流程

這個流程不再是線性的,而是有了分支。例如在查找顧客后,流程可能結(jié)束,也可能轉(zhuǎn)到注冊表單。同樣的,在checkDeliveryArea狀態(tài),顧客可能會被告警,也可能是不被告警。

程序清單:

<?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>

下面將這個流程定義分解成一個個的狀態(tài)。

詢問電話號碼

welcome狀態(tài)是一個很簡單的視圖狀態(tài),它歡迎訪問Spizza網(wǎng)站的顧客并要求輸入電話。它有兩個轉(zhuǎn)移:如果從視圖觸發(fā)phoneEntered事件,就會定向到lookupCustomer,另外一個就是在全局轉(zhuǎn)移中定義用來響應(yīng)cancel事件的cancel轉(zhuǎn)移。

頁面代碼:

<%@ 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輸入。當(dāng)進(jìn)入視圖狀態(tài)時,流程暫停并等待用戶采取一些行為。當(dāng)用戶提交表單時,流程執(zhí)行鍵會在_flowExecutionKey輸入域中返回,并在流程暫停的位置進(jìn)行恢復(fù)。

還需要注意提交按鈕的名稱_eventId_部分是Spring Web Flow的一個線索,它表明了接下來要觸發(fā)事件。當(dāng)點(diǎn)擊這個按鈕提交表單時,就會觸發(fā)phoneEntered事件,進(jìn)而轉(zhuǎn)移到lookupCustomer。

查找顧客

當(dāng)歡迎顧客的表單提交后,顧客的電話號碼將包含在請求參數(shù)中,并用于查詢顧客。lookupCustomer狀態(tài)的<evaluate>元素是查找發(fā)生的位置。它將電話號碼從請求參數(shù)中抽取出來,并傳遞到pizzaFlowActions Bean的lookupCustomer()方法中。該方法要么返回Customer對象,要么拋出CustomerNotFoundException異常。

在前一種情況下,Customer對象會被設(shè)置到customer變量中(通過result屬性)并默認(rèn)的轉(zhuǎn)移將流程帶到customerReady狀態(tài)。如果沒有查到顧客,那么會拋出異常,流程會轉(zhuǎn)移到registrationForm狀態(tài)。

注冊新顧客

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對象上。

檢查配送區(qū)域

顧客提供了地址后,需要確認(rèn)住址是否在配送范圍內(nèi),因此使用了決策狀態(tài)。

決策狀態(tài)checkDeliveryArea有一個<if>元素,它將顧客的郵編傳遞到pizzaFlowActions Bean的checkDeliveryArea()方法中,該方法會返回一個Boolean值。

如果顧客在配送范圍內(nèi),那么流程將轉(zhuǎn)移到addCustomer狀態(tài),否則進(jìn)入deliveryWarning視圖狀態(tài)。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>

其中有兩個鏈接,允許用戶繼續(xù)訂單或者取消訂單。通過使用與welcome狀態(tài)相同的flowExecutionUrl變量,這些鏈接分別觸發(fā)流程中的accept和cancel事件。如果發(fā)送的是accept事件,那么流程會轉(zhuǎn)移到addCustomer狀態(tài)。否則,子流程會轉(zhuǎn)移到cancel狀態(tài)。

存儲顧客數(shù)據(jù)

addCustomer有一個<evaluate>元素,它會調(diào)用pizzaFlowActions.addCustomer()方法,將order.customer流程參數(shù)傳遞進(jìn)去。

一旦這個流程完成,就會執(zhí)行默認(rèn)轉(zhuǎn)移,流程會轉(zhuǎn)移到ID為customerReady的結(jié)束狀態(tài)。

結(jié)束流程

當(dāng)customer流程完成所有的路徑后,會到達(dá)customerReady的結(jié)束狀態(tài)。當(dāng)調(diào)用它的披薩流程恢復(fù)時,它會接收到一個customerReady事件,這個事件將使得流程轉(zhuǎn)移到buildOrder狀態(tài)。

注意,customerReady結(jié)束狀態(tài)包含了一個<output>元素。在流程中,它等同于Java的return語句。它會從子流程中傳遞一些數(shù)據(jù)到調(diào)用流程。例如,<output>元素返回customer變量,這樣披薩流程中的identifyCustomer子流程狀態(tài)就可以將其指定給訂單。

另外,如果用戶在任意地方觸發(fā)了cancel事件,將會通過cancel狀態(tài)結(jié)束流程,這也會在披薩流程中觸發(fā)cancel事件并導(dǎo)致轉(zhuǎn)移到披薩流程的結(jié)束狀態(tài)。

構(gòu)建訂單

下面就是確定顧客想要什么樣的披薩,提示用戶創(chuàng)建披薩并將其放入訂單,如圖:


通過訂單子流程添加披薩
通過訂單子流程添加披薩

可以看到,showOrder狀態(tài)位于訂單子流程的中心位置。這是用戶進(jìn)入這個流程時的狀態(tài),也是用戶添加披薩訂單后轉(zhuǎn)移的目標(biāo)狀態(tài)。它展現(xiàn)了訂單的當(dāng)前狀態(tài),并允許用戶添加其他的披薩到訂單中。

添加披薩訂單時,會轉(zhuǎn)移到createPizza狀態(tài)。這是一個視圖狀態(tài),允許用戶對披薩進(jìn)行選擇。

在showOrder狀態(tài),用戶可以提交訂單,也可以取消。

<?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>

這個子流程實(shí)際上回操作主流程創(chuàng)建的Order對象,在這里我們使用<input>元素來將Order對象傳遞進(jìn)流程。

接下來會看到showOrder狀態(tài),它是一個基本的視圖狀態(tài),具有3個不同的轉(zhuǎn)移,分別用于創(chuàng)建披薩、提交訂單和取消訂單。

createPizza的視圖是一個表單,這個表單可以添加新的Pizza對象到訂單。<on-entry>元素添加了一個新的Pizza對象到流程作用域內(nèi),當(dāng)表單提交時它將填充進(jìn)訂單。值得注意的是,這個視圖狀態(tài)引用的model是流程作用域同一個Pizza對象。Pizza對象將綁定到創(chuàng)建披薩的表單中:

<%@ 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>

當(dāng)通過Continue按鈕提交訂單時,尺寸和配料選擇會綁定到Pizza對象中,并且觸發(fā)addPizza轉(zhuǎn)移。與這個轉(zhuǎn)移關(guān)聯(lián)的<evaluate>元素表明在轉(zhuǎn)移到showOrder狀態(tài)之前,流程作用域內(nèi)的Pizza對象會傳遞給訂單的addPizza()方法中。

有兩種方法可以結(jié)束流程,用戶可以點(diǎn)擊showOrder視圖中的Cancel按鈕或者Checkout按鈕。這兩種操作都會使流程轉(zhuǎn)移到一個<end-state>。但是選擇的結(jié)束狀態(tài)ID決定了退出這個流程時觸發(fā)事件,進(jìn)而最終確定主流程的下一個行為。主流程要么基于cancel要么基于orderCreated事件進(jìn)行狀態(tài)轉(zhuǎn)移。在前者情況下,外邊的流程會結(jié)束;后者,會轉(zhuǎn)移到takePayment子流程。

支付

在披薩流程要結(jié)束的時候,最后的子流程提示用戶輸入他們的支付信息,如下圖:

支付子流程
支付子流程

支付子流程也是使用<input>元素接收一個Order對象作為輸入。

可以看到,進(jìn)入支付子流程的時候,用戶會到達(dá)takePayment狀態(tài)。這是一個視圖狀態(tài),在這里用戶可以選擇信用卡、支票或者現(xiàn)金進(jìn)行支付。提示支付信息后,進(jìn)入verifyPayment狀態(tài),這是一個行為狀態(tài),會校驗(yàn)支付信息是否可以接受。

<?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>

在流程進(jìn)入takePayment視圖時,<on-entry>元素將構(gòu)建一個支付表單并使用SpEL表達(dá)式在流程范圍內(nèi)創(chuàng)建PaymentDetails實(shí)例,該實(shí)例實(shí)際上是表單背后的對象。它也會創(chuàng)建視圖作用域的paymentDetails變量,這個變量是一個包含了PaymentType enum的值的列表。在這里,SpEL的T()作用于PaymentType類,這樣就可以調(diào)用靜態(tài)的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('_', ' '));
    }
}

在面對支付表單的時候,用戶可能提交支付,也可能會取消。根據(jù)做出的選擇,支付子流程將名為paymentTaken或cancel的<end-state>結(jié)束。就像其他的子流程一樣,不論哪種<end-state>都會結(jié)束子流程并將控制交給主流程。但是所采用的id將決定主流程接下來的轉(zhuǎn)移。

目前我們已經(jīng)依次介紹了披薩流程及其子流程,下面快速了解下如何對流程及其狀態(tài)的訪問增加安全保護(hù)。

保護(hù)Web流程

Spring Web Flow中的狀態(tài)、轉(zhuǎn)移甚至整個流程都可以借助<secured>元素實(shí)現(xiàn)安全性,該元素會作為這些元素的子元素。例如,為了保護(hù)對一個視圖狀態(tài)的訪問:

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

按照這里的配置,只有授權(quán)ROLE_ADMIN訪問權(quán)限(借助attributes屬性)的用戶才能訪問這個視圖狀態(tài)。attributes屬性使用逗號分隔的權(quán)限列表來表明用戶要訪問指定狀態(tài)、轉(zhuǎn)移或流程所需要的權(quán)限。match屬性可以設(shè)置為any或all。如果是any,那么用戶至上具備一個attributes屬性所列的權(quán)限。如果的all,那么用戶必須具有所有權(quán)限。具體見下一章。



如果覺得有用,歡迎關(guān)注我的微信,有問題可以直接交流:

你的關(guān)注是對我最大的鼓勵!
你的關(guān)注是對我最大的鼓勵!
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,570評論 3 418
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,505評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,786評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,219評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,438評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,971評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,796評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,995評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,230評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,697評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,785評論 18 139
  • Spring Web Flow是Spring MVC的擴(kuò)展,它支持開發(fā)基于流程的應(yīng)用程序。比如在網(wǎng)上商城購買時,需...
    yjaal閱讀 1,815評論 0 2
  • 三、組合起來:披薩流程 這里我們通過訂購披薩的過程對流程進(jìn)行說明。我們首先從構(gòu)建一個高層次的流程開始,它定義了訂購...
    yjaal閱讀 1,249評論 0 1
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 46,898評論 6 342
  • 如今做Java尤其是web幾乎是避免不了和Spring打交道了,但是Spring是這樣的大而全,新鮮名詞不斷產(chǎn)生,...
    MageekChiu閱讀 1,629評論 0 26