spring statemachine-傳遞參數(shù)的message與持久化

1.傳遞參數(shù)的message

?? 在企業(yè)開發(fā)中,數(shù)據(jù)在不同的業(yè)務(wù)間傳輸是最常見的工作叠赐,所以雖然我們的主架構(gòu)是用的狀態(tài)機欲账,也就是從流程狀態(tài)的角度來看待這個項目,但在具體業(yè)務(wù)中芭概,每個狀態(tài)的轉(zhuǎn)變中會牽涉到各類業(yè)務(wù)赛不,這些業(yè)務(wù)有些需要收到狀態(tài)機變化的通知,需要把狀態(tài)值傳遞給業(yè)務(wù)類和業(yè)務(wù)方法谈山,同樣的俄删,在處理狀態(tài)變化是,也需要獲取業(yè)務(wù)數(shù)據(jù)奏路,方便不同的業(yè)務(wù)在同一個狀態(tài)變化環(huán)節(jié)做各自的業(yè)務(wù)畴椰,下面我們就講下這個數(shù)據(jù)在spring statemachine里面的傳遞。

?? 這次我們的順序變一下鸽粉,由外部傳入一個訂單號到controller開始:

@RequestMapping("/testOrderState")
    public void testOrderState(String orderId) throws Exception {

        StateMachine<OrderStates, OrderEvents> stateMachine = orderStateMachineBuilder.build(beanFactory);
        System.out.println(stateMachine.getId());

        // 創(chuàng)建流程
        stateMachine.start();

        // 觸發(fā)PAY事件
        stateMachine.sendEvent(OrderEvents.PAY);

        // 觸發(fā)RECEIVE事件
        Order order = new Order(orderId, "547568678", "廣東省深圳市", "13435465465", "RECEIVE");
        Message<OrderEvents> message = MessageBuilder.withPayload(OrderEvents.RECEIVE).setHeader("order", order).build();
        stateMachine.sendEvent(message);

        // 獲取最終狀態(tài)
        System.out.println("最終狀態(tài):" + stateMachine.getState().getId());
    }

?? controller收到request請求的參數(shù)斜脂,orderId,然后狀態(tài)機依次觸發(fā)事件触机,到觸發(fā)RECEIVE事件的時候帚戳,我們新建了一個Order玷或,并把orderId塞進去了,其實更多的情況應(yīng)該是我們拿到orderId片任,然后查詢數(shù)據(jù)庫偏友,得到order數(shù)據(jù)對象,這里為了簡化代碼对供,就新建一個啦位他。
?? 然后就是真正的主角登場了,Message产场。它其實不是spirng statemachine專屬的鹅髓,它是spring里面通用的一種消息工具,看它的源代碼:

package org.springframework.messaging;

public interface Message<T> {

    /**
     * Return the message payload.
     */
    T getPayload();

    /**
     * Return message headers for the message (never {@code null} but may be empty).
     */
    MessageHeaders getHeaders();

}

它由兩個部分組成京景,看圖就知道了窿冯,和代碼里面是一致的

image

?? 在spring statemachine里面,我們把狀態(tài)塞到message的payload里面确徙,然后把需要傳遞的業(yè)務(wù)數(shù)據(jù)(例子里面就是order對象)塞到header里面醒串。創(chuàng)建message用的是messagebuilder,看它的名字就知道是專門創(chuàng)建message的鄙皇。

Message<OrderEvents> message = MessageBuilder.withPayload(OrderEvents.RECEIVE).setHeader("order", order).build();
        stateMachine.sendEvent(message);

?? 創(chuàng)建了message后厦凤,狀態(tài)機sendEvent就可以不只是傳一個event,可以組合event(OrderEvents.RECEIVE)和數(shù)據(jù)內(nèi)容(order)一起發(fā)送給狀態(tài)機變化的處理類eventconfig了育苟。讓我們看eventConfig的處理:

/**
     * WAITING_FOR_RECEIVE->DONE 執(zhí)行的動作
     */
    @OnTransition(source = "WAITING_FOR_RECEIVE", target = "DONE")
    public void receive(Message<OrderEvents> message) {
        System.out.println("傳遞的參數(shù):" + message.getHeaders().get("order"));
        logger.info("---用戶已收貨,訂單完成---");
    }

首先椎木,receive方法的參數(shù)由之前的為空:

public void receive() {
        logger.info("---用戶已收貨违柏,訂單完成---");
}

?? 改成了Message<OrderEvents> message,這樣就能從message的getHeaders里面取到傳遞過來的數(shù)據(jù)對象了香椎。

?? 另外如果我們需要傳遞多個數(shù)據(jù)對象怎么辦呢漱竖,比如我們在實際業(yè)務(wù)中,除了傳訂單數(shù)據(jù)畜伐,可能還需要把商品數(shù)據(jù)馍惹,或者支付結(jié)果數(shù)據(jù)也傳過來,那么也容易玛界,我們還是從controller里面開始:

Message<OrderEvents> message = MessageBuilder.withPayload(OrderEvents.RECEIVE).setHeader("order", order).setHeader("otherobj", "otherobjvalue").build();

在后面繼續(xù)setHeader就好了万矾,然后到eventConfig里面:

System.out.println("傳遞的參數(shù):" + message.getHeaders().get("order"));
System.out.println("傳遞的參數(shù):" + message.getHeaders().get("otherObj"));

運行后看日志:

傳遞的參數(shù):Order [id=null, userId=547568678, address=廣東省深圳市, phoneNum=13435465465, state=RECEIVE]
傳遞的參數(shù):otherObjValue

?? 可知兩個的數(shù)據(jù)都傳遞到了eventConfig里面了,這個就實現(xiàn)了多個數(shù)據(jù)對象的同時傳遞慎框。
?? 到這里為止良狈,狀態(tài)機通過message對象就和其他的業(yè)務(wù)代碼做到了數(shù)據(jù)連接。其實這個很關(guān)鍵笨枯,只有做到和其他業(yè)務(wù)的數(shù)據(jù)傳遞薪丁,才能算的上真正的可用遇西。

2.持久化

?? 目前為止,我們都是從狀態(tài)流程的開始階段創(chuàng)建一個狀態(tài)機严嗜,然后一路走下去粱檀。但在實際業(yè)務(wù)中,狀態(tài)機可能需要在某個環(huán)節(jié)停留漫玄,等待其他業(yè)務(wù)的觸發(fā)茄蚯,然后再繼續(xù)下面的流程。比如訂單称近,可能在支付環(huán)節(jié)需要等待一個剁手的用戶隔天再下單第队,所以這里面涉及到一個創(chuàng)建的狀態(tài)機該何去何從的問題。在spring statemachine中刨秆,給出來的辦法就是保存起來凳谦,到需要的時候取出來用。

1衡未、持久化到本地內(nèi)存

import java.util.HashMap;
import java.util.Map;

import org.springframework.statemachine.StateMachineContext;
import org.springframework.statemachine.StateMachinePersist;
import org.springframework.stereotype.Component;

/**
 * 在內(nèi)存中持久化狀態(tài)機
 */
@Component
public class InMemoryStateMachinePersist implements StateMachinePersist<OrderStates, OrderEvents, String> {

    private Map<String, StateMachineContext<OrderStates, OrderEvents>> map = new HashMap<String, StateMachineContext<OrderStates,OrderEvents>>();
    
    @Override
    public void write(StateMachineContext<OrderStates, OrderEvents> context, String contextObj) throws Exception {
        map.put(contextObj, context);
    }

    @Override
    public StateMachineContext<OrderStates, OrderEvents> read(String contextObj) throws Exception {
        return map.get(contextObj);
    }

}

這個接口非常簡單尸执,就是write和read,但他保存的對象是StateMachineContext缓醋,不是StateMachine如失,所以我們還不能直接用它,需要配置一下送粱。

import org.springframework.statemachine.StateMachinePersist;
......
import org.springframework.statemachine.persist.StateMachinePersister;
@Configuration
public class PersistConfig {
    
    
    @Autowired
    private InMemoryStateMachinePersist inMemoryStateMachinePersist;
    
    /**
     * 注入StateMachinePersister對象
     * 
     * @return
     */
    @Bean(name="orderMemoryPersister")
    public StateMachinePersister<OrderStates, OrderEvents, String> getPersister() {
        return new DefaultStateMachinePersister<>(inMemoryStateMachinePersist);
    }

}

這里有個坑褪贵,InMemoryStateMachinePersist 實現(xiàn)的接口是

org.springframework.statemachine.StateMachinePersist

,但在PersistConfig 里面抗俄,getPersister()方法返回的值類型是StateMachinePersister類型脆丁,看著很像,但并不是上面的這個接口动雹,而是org.springframework.statemachine.persist.StateMachinePersister接口槽卫,為了表示這個坑對我的傷害,我要強調(diào)一下兩個接口:

package org.springframework.statemachine;
public interface StateMachinePersist<S, E, T> {
    void write(StateMachineContext<S, E> context, T contextObj) throws Exception;
    StateMachineContext<S, E> read(T contextObj) throws Exception;
}
package org.springframework.statemachine.persist;
import org.springframework.statemachine.StateMachine;
public interface StateMachinePersister<S, E, T> {
    void persist(StateMachine<S, E> stateMachine, T contextObj) throws Exception;
    StateMachine<S, E> restore(StateMachine<S, E> stateMachine, T contextObj) throws Exception;
}

這兩個接口名字很類似胰蝠,很容易搞混歼培,但下面的是有er的,包名也不同的茸塞。StateMachinePersister是可以直接保存StateMachine對象的躲庄,所以我們需要先實現(xiàn)上面的StateMachinePersist,然后再一個Config類里面轉(zhuǎn)換成下面的StateMachinePersister钾虐,轉(zhuǎn)換的代碼就在上面的PersistConfig類里读跷。

然后我們就能在controller里面使用了

@Resource(name="orderMemoryPersister")
private StateMachinePersister<OrderStates, OrderEvents, String> orderMemorypersister;

......

//保存狀態(tài)機
@RequestMapping("/testMemoryPersister")
    public void tesMemorytPersister(String id) throws Exception {
        StateMachine<OrderStates, OrderEvents> stateMachine = orderStateMachineBuilder.build(beanFactory);
        stateMachine.start();
        
        //發(fā)送PAY事件
        stateMachine.sendEvent(OrderEvents.PAY);
        Order order = new Order();
        order.setId(id);
        
        //持久化stateMachine
        orderMemorypersister.persist(stateMachine, order.getId());
    
    }

//取出狀態(tài)機
@RequestMapping("/testMemoryPersisterRestore")
    public void testMemoryRestore(String id) throws Exception {
        StateMachine<OrderStates, OrderEvents> stateMachine = orderStateMachineBuilder.build(beanFactory);
        orderMemorypersister.restore(stateMachine, id);
        System.out.println("恢復(fù)狀態(tài)機后的狀態(tài)為:" + stateMachine.getState().getId());
    }

2、持久化到redis

??真正的業(yè)務(wù)中禾唁,一般都是多臺機分布式運行效览,所以如果狀態(tài)機只能保存在本地內(nèi)容无切,就不能用在分布式應(yīng)用上了。spring提供了一個方便的辦法丐枉,使用redis解決這個問題哆键。讓我們看看怎么弄。
?? pom文件引入spring-statemachine-redis

<!-- redis持久化狀態(tài)機 -->
        <dependency>
            <groupId>org.springframework.statemachine</groupId>
            <artifactId>spring-statemachine-redis</artifactId>
            <version>1.2.9.RELEASE</version>
        </dependency>

在springboot配置文件里面加上redis參數(shù)瘦锹,我這是application.properties

# REDIS (RedisProperties)
# Redis數(shù)據(jù)庫索引(默認為0)
spring.redis.database=0
# Redis服務(wù)器地址
spring.redis.host=localhost
# Redis服務(wù)器連接端口
spring.redis.port=6379
# Redis服務(wù)器連接密碼(默認為空)
spring.redis.password=
# 連接池最大連接數(shù)(使用負值表示沒有限制)
spring.redis.pool.max-active=8
# 連接池最大阻塞等待時間(使用負值表示沒有限制)
spring.redis.pool.max-wait=-1
# 連接池中的最大空閑連接
spring.redis.pool.max-idle=8
# 連接池中的最小空閑連接
spring.redis.pool.min-idle=0
# 連接超時時間(毫秒)
spring.redis.timeout=0

保證配置的redis開啟并能用籍嘹,我們繼續(xù)⊥湓海回到我們熟悉的PersistConfig

@Configuration
public class PersistConfig {
    
    @Autowired
    private RedisConnectionFactory redisConnectionFactory;
    
    
    /**
     * 注入RedisStateMachinePersister對象
     * 
     * @return
     */
    @Bean(name = "orderRedisPersister")
    public RedisStateMachinePersister<OrderStates, OrderEvents> redisPersister() {
        return new RedisStateMachinePersister<>(redisPersist());
    }

    /**
     * 通過redisConnectionFactory創(chuàng)建StateMachinePersist
     * 
     * @return
     */
    public StateMachinePersist<OrderStates, OrderEvents,String> redisPersist() {
        RedisStateMachineContextRepository<OrderStates, OrderEvents> repository = new RedisStateMachineContextRepository<>(redisConnectionFactory);
        return new RepositoryStateMachinePersist<>(repository);
    }
    
}

這個套路和上面保存到本地內(nèi)存是一樣一樣的辱士,先生成一個StateMachinePersist,這里是通過RedisConnectionFactory生成RepositoryStateMachinePersist听绳,然后再包裝輸出StateMachinePersister颂碘,這里是RedisStateMachinePersister。然后就可以愉快的在controller里面看怎么用了

@Resource(name="orderRedisPersister")
private StateMachinePersister<OrderStates, OrderEvents, String> orderRedisPersister;

......

@RequestMapping("/testRedisPersister")
    public void testRedisPersister(String id) throws Exception {
        StateMachine<OrderStates, OrderEvents> stateMachine = orderStateMachineBuilder.build(beanFactory);
        stateMachine.start();
        Order order = new Order();
        order.setId(id);
        //發(fā)送PAY事件
        Message<OrderEvents> message = MessageBuilder.withPayload(OrderEvents.PAY).setHeader("order", order).build();
        stateMachine.sendEvent(message);
        //持久化stateMachine
        orderRedisPersister.persist(stateMachine, order.getId());
    }
    
    @RequestMapping("/testRedisPersisterRestore")
    public void testRestore(String id) throws Exception {
        StateMachine<OrderStates, OrderEvents> stateMachine = orderStateMachineBuilder.build(beanFactory);
        orderRedisPersister.restore(stateMachine, id);
        System.out.println("恢復(fù)狀態(tài)機后的狀態(tài)為:" + stateMachine.getState().getId());
    }

執(zhí)行完redis保存statemachine后椅挣,大家可以自己在redis客戶端查看以下头岔,是不是有內(nèi)容保存進去了。

3.偽持久化和中間段的狀態(tài)機

??我們設(shè)想一個業(yè)務(wù)場景鼠证,就比如訂單吧峡竣,我們一般的設(shè)計都會把訂單狀態(tài)存到訂單表里面,其他的業(yè)務(wù)信息也都有表保存量九,而狀態(tài)機的主要作用其實是規(guī)范整個訂單業(yè)務(wù)流程的狀態(tài)和事件适掰,所以狀態(tài)機要不要保存真的不重要,我們只需要從訂單表里面把狀態(tài)取出來荠列,知道當(dāng)前是什么狀態(tài)攻谁,然后伴隨著業(yè)務(wù)繼續(xù)流浪到下一個狀態(tài)節(jié)點就好了。我們先實現(xiàn)一個StateMachinePersist弯予,因為我不想真的持久化,所以就敷衍一下个曙,持久化是什么锈嫩,啥也不干。

import org.springframework.statemachine.StateMachineContext;
import org.springframework.statemachine.StateMachinePersist;
import org.springframework.statemachine.support.DefaultStateMachineContext;
import org.springframework.stereotype.Component;

@Component
public class OrderStateMachinePersist implements StateMachinePersist<OrderStates, OrderEvents, Order> {

    @Override
    public void write(StateMachineContext<OrderStates, OrderEvents> context, Order contextObj) throws Exception {
        //這里不做任何持久化工作
    }

    @Override
    public StateMachineContext<OrderStates, OrderEvents> read(Order contextObj) throws Exception {
        StateMachineContext<OrderStates, OrderEvents> result = new DefaultStateMachineContext<OrderStates, OrderEvents>(OrderStates.valueOf(contextObj.getState()), 
                null, null, null, null, "orderMachine");
        return result;
    }
}

然后在PersistConfig里面轉(zhuǎn)換成StateMachinePersister

@Configuration
public class PersistConfig {
@Autowired
    private OrderStateMachinePersist orderStateMachinePersist;
@Bean(name="orderPersister")
    public StateMachinePersister<OrderStates, OrderEvents, Order> orderPersister() {
        return new DefaultStateMachinePersister<OrderStates, OrderEvents, Order>(orderStateMachinePersist);
    }
}

現(xiàn)在問題來了垦搬,不持久化的持久化類是為啥呢呼寸,主要就是為了取一個任何狀態(tài)節(jié)點的狀態(tài)機,方便繼續(xù)往下執(zhí)行猴贰,請看controller

@RestController
@RequestMapping("/statemachine")
public class StateMachineController {

    @Resource(name="orderPersister")
    private StateMachinePersister<OrderStates, OrderEvents, Order> persister;
    
    @RequestMapping("/testOrderRestore")
    public void testOrderRestore(String id) throws Exception {
        StateMachine<OrderStates, OrderEvents> stateMachine = orderStateMachineBuilder.build(beanFactory);
        //訂單
        Order order = new Order();
        order.setId(id);
        order.setState(OrderStates.WAITING_FOR_RECEIVE.toString());
        //恢復(fù)
        persister.restore(stateMachine, order);
        //查看恢復(fù)后狀態(tài)機的狀態(tài)
        System.out.println("恢復(fù)后的狀態(tài):" + stateMachine.getState().getId());
    }
}

??看到?jīng)]有对雪,用builder建了一個新的狀態(tài)機,用restore過了一手米绕,就已經(jīng)是一個到達order指定狀態(tài)的老司機狀態(tài)機了瑟捣,在這里馋艺,持久化不是本意,讓狀態(tài)機能夠隨時抓換到任意狀態(tài)節(jié)點才是目的迈套。在實際的企業(yè)開發(fā)中捐祠,不可能所有情況都是從頭到尾的按狀態(tài)流程來,會有很多意外桑李,比如歷史數(shù)據(jù)踱蛀,故障重啟后的遺留流程......,所以這種可以任意調(diào)節(jié)狀態(tài)的才是我們需要的狀態(tài)機贵白。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末率拒,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子禁荒,更是在濱河造成了極大的恐慌猬膨,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件圈浇,死亡現(xiàn)場離奇詭異寥掐,居然都是意外死亡,警方通過查閱死者的電腦和手機磷蜀,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門召耘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人褐隆,你說我怎么就攤上這事污它。” “怎么了庶弃?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵衫贬,是天一觀的道長。 經(jīng)常有香客問我歇攻,道長固惯,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任缴守,我火速辦了婚禮葬毫,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘屡穗。我一直安慰自己贴捡,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布村砂。 她就那樣靜靜地躺著烂斋,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上汛骂,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天罕模,我揣著相機與錄音,去河邊找鬼香缺。 笑死手销,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的图张。 我是一名探鬼主播锋拖,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼祸轮!你這毒婦竟也來了兽埃?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤适袜,失蹤者是張志新(化名)和其女友劉穎柄错,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體苦酱,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡售貌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了疫萤。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片颂跨。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖扯饶,靈堂內(nèi)的尸體忽然破棺而出恒削,到底是詐尸還是另有隱情,我是刑警寧澤尾序,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布钓丰,位于F島的核電站,受9級特大地震影響每币,放射性物質(zhì)發(fā)生泄漏携丁。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一兰怠、第九天 我趴在偏房一處隱蔽的房頂上張望梦鉴。 院中可真熱鬧,春花似錦痕慢、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至娜庇,卻和暖如春塔次,著一層夾襖步出監(jiān)牢的瞬間方篮,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工励负, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留藕溅,地道東北人。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓继榆,卻偏偏與公主長得像巾表,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子略吨,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,722評論 2 345