Adsense

2018年1月16日火曜日

Spring Statemachineを使ってドラクエ11のポーカーを自動プレイするプログラムを作ってロイヤルストレートスライムを出すまでの道のり 第3回

前回に引き続き、もう少しSpring Statemachineを触ってみます。

Action

前回はラムダ式でActionを設定していましたが、複雑な処理を行う場合はActionインターフェースを実装したクラスを作成することになります。

public class StateEntryAction implements Action<States, Events> {

    private static final Logger LOGGER = LoggerFactory.getLogger(StateEntryAction.class);

    @Override
    public void execute(StateContext<States, Events> context) {
        LOGGER.info("StateEntryAction.execute()");
    }
}

上記のように唯一のメソッドであるexecuteをオーバーライドして処理を実装します。
引数のStateContextは状態に関する様々な情報にアクセスできます。使い方は後日説明する予定です。

Actionは様々な場所に設定できます。
前回はTransition(状態遷移)に対して設定しましたが、以下のように設定するとState(状態)に対してActionを設定できます。

    @Bean
    public InitialAction initialAction() { return new InitialAction(); }
    @Bean
    public StateEntryAction stateEntryAction() { return new StateEntryAction(); }
    @Bean
    public StateExitAction stateExitAction() { return new StateExitAction(); }
    @Bean
    public StateDoAction stateDoAction() { return new StateDoAction(); }

    @Override
    public void configure(StateMachineStateConfigurer<States, Events> states)
            throws Exception {
        states.withStates()
            .initial(INITIAL_STATE, initialAction()) // 初期処理 
            .state(READY_STATE, stateEntryAction(), stateExitAction())  //Entry Action, Exit Action
            .state(RUN_STATE, stateDoAction())  //Do Action
            .end(FINAL_STATE);
    }

initialメソッドの第2引数にActionを設定することで、INITIAL_STATEに入るときだけ(= 一度だけ)実行するActionを設定できます。初期化処理を実装する場合に最適です。

stateメソッドは注意が必要です。
まず、引数が3つのstateメソッドの場合、第2引数が状態に入ったときに実行されるEntry Action、第3引数が状態を出るときに実行されるExit Actionになります。Etnry ActionかExit Actionのどちらか一つでいい場合は、いらない方の引数にnullを渡せばOKです。
そして、引数が2つのstateメソッドの場合、第2引数のActionはその状態中に実行されるDo Actionになります。

さて、Do ActionとはEntry Actionとどう違うのでしょうか?
通常、状態遷移マシンで do アクティビティといえば、その状態にとどまっているときに実行され続ける動作を書くものです。
Spring Statemachineでは、Do Actionは別スレッドで動作します。
先程の状態遷移マシンを動かしてみると以下のようになります。


2018-01-16 21:37:35.609  INFO 7740 --- [           main] c.e.s.demo.statemachine.InitialAction    : InitialAction.execute()
2018-01-16 21:37:35.609  INFO 7740 --- [           main] c.e.s.d.statemachine.StateEntryAction    : StateEntryAction.execute()
2018-01-16 21:37:35.609  INFO 7740 --- [           main] c.e.s.demo.statemachine.StateExitAction  : StateExitAction.execute()
2018-01-16 21:37:35.609  INFO 7740 --- [pool-2-thread-1] c.e.s.demo.statemachine.StateDoAction    : StateDoAction.execute()


IntialAction, EntryAction, ExitActionはmainスレッドで動いていますが、DoActionはpool-2-thread-1 という別のスレッドで動いていることが分かります。

状態に入ったときに完了まで確実に実行したいActionはEntryActionとして定義し、状態に入っているときに継続して実行し続けるような処理はDoActionとして定義すると良いでしょう。

Spring Statemachineのリファレンスを見るとDo ActionについてはState Actionsという表現で説明されています。リファレンスによれば、State ActionsはSpringのTaskSchedulerで実行されるようです。別の状態に遷移した場合にはtaskがキャンセルされるため、InterruptedExceptionをキャッチしたり、Thread#interrupted()で割り込まれたことを検知してState Actionsを停止する処理を実装する必要があるようです。

また、Entry ActionやExit Actionは同期的に動いている(sendEventしたスレッドでそのまま実行される)のですが、これはStateMachineに設定されたtaskExecutorのデフォルトがSyncTaskExecutorを使用しているためです。taskExecutorを非同期的なものに変更すればEntry ActionやExit Actionも別スレッドで動作させることができます。

Guard

次にガード条件を表すGuardインターフェースを見てみます。

public class SampleGuard implements Guard<States, Events> {

    private static final Logger LOGGER = LoggerFactory.getLogger(SampleGuard.class);

    @Override
    public boolean evaluate(StateContext<States, Events> context) {
        Integer count = context.getMessageHeaders().get("count", Integer.class);

        if (count != null && count > 10) {
            LOGGER.info("ガード条件が trueなので遷移が実行される");
            return true;
        }
        return false;
    }
}

evaluateメソッドがtrueを返すとこのガード条件が設定された状態遷移が実行されますが、falseを返すと状態遷移は実行されません。ここでは、引数のStateContext経由で渡されたcountの値を元にガード条件を判定しています。

状態遷移に対してガード条件を設定するには以下のようにConfigクラスでguardメソッドを使用します。

    @Bean
    public SampleGuard sampleGuard() {
        return new SampleGuard();
    }
    
    @Override
    public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
            throws Exception {
        transitions
            .withExternal()
                .source(READY_STATE)
                .target(RUN_STATE)
                .event(RUN_EVENT)
                .guard(sampleGuard())
                .action(c -> System.out.println("READY to RUN"))
                .and()
            // 以下略


StateMachineListener

状態遷移マシンに各種変化が起きたときに、ロギングなどの共通的な処理を仕掛けたい場合は、StateMachineListenerインターフェースが使えます。
StateMachineListenerAdapterクラスを継承すれば、必要なメソッドのみをオーバーライドできるので便利です。

下記では、状態の変化が発生したときにロギングを行うリスナを実装しています。
stateChangedメソッドの引数には、遷移元の状態と遷移先の状態が渡されます。
初期状態(INITIAL_STATE)に入るときには、fromの引数がnullになるため注意が必要です。

public class SampleStateListener extends StateMachineListenerAdapter<States, Events> {

    private static final Logger LOGGER = LoggerFactory
            .getLogger(SampleStateListener.class);

    @Override
    public void stateChanged(State<States, Events> from, State<States, Events> to) {
        String fromState = (from == null) ? "-" : from.getId().toString();
        String toState = (to == null) ? "-" : to.getId().toString();
        LOGGER.info("from: {}, to: {}", fromState, toState);
    }
}


リスナの設定はConfigクラスで下記のように行います。

    @Bean
    public SampleStateListener sampleStateListener() { return new SampleStateListener(); }
    
    @Override
    public void configure(StateMachineConfigurationConfigurer<States, Events> config)
            throws Exception {
        config.withConfiguration()
            .listener(sampleStateListener());
    }

リスナは他にも状態遷移が発生した場合や、状態遷移マシンが開始・停止した場合等にも処理を仕掛けられます。詳しくはStateMachineListenerAdapterのJavadocを参照してください。メソッド名からどのタイミングで処理を仕掛けられるか大体分かると思います。
(extendedStateChangedとstateContextメソッドは分かりにくいかもしれませんが、後日説明する予定です。)

そろそろ教科書的なお勉強は飽きてきましたね。
次回はドラクエ11のポーカーを自動実行する話に入りたいと思います。

0 件のコメント:

コメントを投稿