Panduan untuk Projek Mesin Spring State

1. Pengenalan

Artikel ini difokuskan pada projek Spring's State Machine - yang dapat digunakan untuk mewakili aliran kerja atau masalah perwakilan automata keadaan tertentu yang lain.

2. Ketergantungan Maven

Untuk memulakan, kita perlu menambahkan kebergantungan Maven utama:

 org.springframework.statemachine spring-statemachine-core 1.2.3.RELEASE 

Versi terbaru dari pergantungan ini boleh didapati di sini.

3. Konfigurasi Mesin Negeri

Sekarang, mari kita mulakan dengan menentukan mesin keadaan mudah:

@Configuration @EnableStateMachine public class SimpleStateMachineConfiguration extends StateMachineConfigurerAdapter { @Override public void configure(StateMachineStateConfigurer states) throws Exception { states .withStates() .initial("SI") .end("SF") .states( new HashSet(Arrays.asList("S1", "S2", "S3"))); } @Override public void configure( StateMachineTransitionConfigurer transitions) throws Exception { transitions.withExternal() .source("SI").target("S1").event("E1").and() .withExternal() .source("S1").target("S2").event("E2").and() .withExternal() .source("S2").target("SF").event("end"); } }

Perhatikan bahawa kelas ini dijelaskan sebagai konfigurasi Spring konvensional dan juga mesin keadaan. Ia juga perlu memperluas StateMachineConfigurerAdapter sehingga pelbagai kaedah inisialisasi dapat digunakan. Dalam salah satu kaedah konfigurasi, kami menentukan semua kemungkinan keadaan keadaan mesin, dan yang lain, bagaimana peristiwa mengubah keadaan semasa.

Konfigurasi di atas menunjukkan mesin keadaan peralihan garis lurus yang cukup mudah dan cukup mudah diikuti.

Sekarang kita perlu memulakan konteks Spring dan mendapatkan rujukan ke mesin negara yang ditentukan oleh konfigurasi kita:

@Autowired private StateMachine stateMachine;

Setelah kita mempunyai mesin negara, ia harus dimulakan:

stateMachine.start();

Sekarang mesin kita berada dalam keadaan awal, kita dapat menghantar acara dan dengan itu mencetuskan peralihan:

stateMachine.sendEvent("E1");

Kami sentiasa dapat memeriksa keadaan mesin keadaan semasa:

stateMachine.getState();

4. Tindakan

Mari kita tambahkan beberapa tindakan yang akan dilaksanakan di sekitar peralihan negara. Pertama, kami menentukan tindakan kami sebagai kacang kacang dalam fail konfigurasi yang sama:

@Bean public Action initAction() { return ctx -> System.out.println(ctx.getTarget().getId()); }

Kemudian kita dapat mendaftarkan tindakan yang dibuat di atas pada peralihan di kelas konfigurasi kita:

@Override public void configure( StateMachineTransitionConfigurer transitions) throws Exception { transitions.withExternal() transitions.withExternal() .source("SI").target("S1") .event("E1").action(initAction())

Tindakan ini akan dilaksanakan ketika peralihan dari SI ke S1 melalui peristiwa E1 berlaku. Tindakan boleh dilampirkan ke negeri-negeri itu sendiri:

@Bean public Action executeAction() { return ctx -> System.out.println("Do" + ctx.getTarget().getId()); } states .withStates() .state("S3", executeAction(), errorAction());

Fungsi definisi keadaan ini menerima operasi yang akan dijalankan ketika mesin berada dalam keadaan sasaran dan, secara opsional, pengendali tindakan kesalahan.

Penangan tindakan ralat tidak jauh berbeza dengan tindakan lain, tetapi ia akan dikenakan sekiranya pengecualian dilemparkan setiap saat semasa penilaian tindakan negara:

@Bean public Action errorAction() { return ctx -> System.out.println( "Error " + ctx.getSource().getId() + ctx.getException()); }

Anda juga boleh mendaftarkan tindakan individu untuk memasuki , melakukan dan keluar dari peralihan keadaan:

@Bean public Action entryAction() { return ctx -> System.out.println( "Entry " + ctx.getTarget().getId()); } @Bean public Action executeAction() { return ctx -> System.out.println("Do " + ctx.getTarget().getId()); } @Bean public Action exitAction() { return ctx -> System.out.println( "Exit " + ctx.getSource().getId() + " -> " + ctx.getTarget().getId()); }
states .withStates() .stateEntry("S3", entryAction()) .stateDo("S3", executeAction()) .stateExit("S3", exitAction());

Tindakan masing-masing akan dilaksanakan pada peralihan keadaan yang sesuai. Sebagai contoh, kami mungkin ingin mengesahkan beberapa pra-syarat pada masa kemasukan atau mencetuskan beberapa pelaporan pada saat keluar.

5. Pendengar Global

Pendengar acara global dapat ditentukan untuk mesin negara. Pendengar ini akan dipanggil setiap kali peralihan keadaan berlaku dan dapat digunakan untuk perkara seperti pembalakan atau keselamatan.

Pertama, kita perlu menambahkan kaedah konfigurasi lain - kaedah yang tidak berkaitan dengan keadaan atau peralihan tetapi dengan konfigurasi untuk mesin keadaan itu sendiri.

Kita perlu menentukan pendengar dengan memperluas StateMachineListenerAdapter :

public class StateMachineListener extends StateMachineListenerAdapter { @Override public void stateChanged(State from, State to) { System.out.printf("Transitioned from %s to %s%n", from == null ? "none" : from.getId(), to.getId()); } }

Di sini kita hanya mengatasi keadaan yang diubah walaupun terdapat banyak cangkuk genap yang lain.

6. Negeri Diperluas

Spring State Machine melacak keadaannya, tetapi untuk mengetahui keadaan aplikasi kita , sama ada beberapa nilai yang dihitung, entri dari pentadbir atau respons daripada memanggil sistem luaran, kita perlu menggunakan apa yang disebut keadaan lanjutan .

Anggaplah kita ingin memastikan bahawa permohonan akaun melalui dua tahap persetujuan. Kami dapat mengesan jumlah kelulusan menggunakan bilangan bulat yang disimpan dalam keadaan lanjutan:

@Bean public Action executeAction() { return ctx -> { int approvals = (int) ctx.getExtendedState().getVariables() .getOrDefault("approvalCount", 0); approvals++; ctx.getExtendedState().getVariables() .put("approvalCount", approvals); }; }

7. Pengawal

Penjaga dapat digunakan untuk mengesahkan beberapa data sebelum peralihan ke keadaan dilakukan. Seorang pengawal kelihatan sangat mirip dengan tindakan:

@Bean public Guard simpleGuard() { return ctx -> (int) ctx.getExtendedState() .getVariables() .getOrDefault("approvalCount", 0) > 0; }

The noticeable difference here is that a guard returns a true or false which will inform the state machine whether the transition should be allowed to occur.

Support for SPeL expressions as guards also exists. The example above could also have been written as:

.guardExpression("extendedState.variables.approvalCount > 0")

8. State Machine from a Builder

StateMachineBuilder can be used to create a state machine without using Spring annotations or creating a Spring context:

StateMachineBuilder.Builder builder = StateMachineBuilder.builder(); builder.configureStates().withStates() .initial("SI") .state("S1") .end("SF"); builder.configureTransitions() .withExternal() .source("SI").target("S1").event("E1") .and().withExternal() .source("S1").target("SF").event("E2"); StateMachine machine = builder.build();

9. Hierarchical States

Hierarchical states can be configured by using multiple withStates() in conjunction with parent():

states .withStates() .initial("SI") .state("SI") .end("SF") .and() .withStates() .parent("SI") .initial("SUB1") .state("SUB2") .end("SUBEND");

This kind of setup allows the state machine to have multiple states, so a call to getState() will produce multiple IDs. For example, immediately after startup the following expression results in:

stateMachine.getState().getIds() ["SI", "SUB1"]

10. Junctions (Choices)

So far, we've created state transitions which were linear by nature. Not only is this rather uninteresting, but it also does not reflect real-life use-cases that a developer will be asked to implement either. The odds are conditional paths will need to be implemented, and Spring state machine's junctions (or choices) allow us to do just that.

First, we need to mark a state a junction (choice) in the state definition:

states .withStates() .junction("SJ")

Then in the transitions, we define first/then/last options which correspond to an if-then-else structure:

.withJunction() .source("SJ") .first("high", highGuard()) .then("medium", mediumGuard()) .last("low")

first and then take a second argument which is a regular guard which will be invoked to find out which path to take:

@Bean public Guard mediumGuard() { return ctx -> false; } @Bean public Guard highGuard() { return ctx -> false; }

Note that a transition does not stop at a junction node but will immediately execute defined guards and go to one of the designated routes.

In the example above, instructing state machine to transition to SJ will result in the actual state to become low as the both guards just return false.

A final note is that the API provides both junctions and choices. However, functionally they are identical in every aspect.

11. Fork

Sometimes it becomes necessary to split the execution into multiple independent execution paths. This can be achieved using the fork functionality.

First, we need to designate a node as a fork node and create hierarchical regions into which the state machine will perform the split:

states .withStates() .initial("SI") .fork("SFork") .and() .withStates() .parent("SFork") .initial("Sub1-1") .end("Sub1-2") .and() .withStates() .parent("SFork") .initial("Sub2-1") .end("Sub2-2");

Then define fork transition:

.withFork() .source("SFork") .target("Sub1-1") .target("Sub2-1");

12. Join

The complement of the fork operation is the join. It allows us to set a state transitioning to which is dependent on completing some other states:

As with forking, we need to designate a join node in the state definition:

states .withStates() .join("SJoin")

Then in transitions, we define which states need to complete to enable our join state:

transitions .withJoin() .source("Sub1-2") .source("Sub2-2") .target("SJoin");

That's it! With this configuration, when both Sub1-2 and Sub2-2 are achieved, the state machine will transition to SJoin

13. Enums Instead of Strings

Dalam contoh di atas, kami telah menggunakan pemalar rentetan untuk menentukan keadaan dan peristiwa untuk kejelasan dan kesederhanaan. Pada sistem produksi dunia nyata, seseorang mungkin ingin menggunakan enum Java untuk mengelakkan kesalahan ejaan dan mendapatkan lebih banyak jenis keselamatan.

Pertama, kita perlu menentukan semua keadaan dan peristiwa yang mungkin berlaku dalam sistem kita:

public enum ApplicationReviewStates { PEER_REVIEW, PRINCIPAL_REVIEW, APPROVED, REJECTED } public enum ApplicationReviewEvents { APPROVE, REJECT }

Kami juga perlu memberikan jumlah kami sebagai parameter generik semasa kami memperluas konfigurasi:

public class SimpleEnumStateMachineConfiguration extends StateMachineConfigurerAdapter 

Setelah ditentukan, kita boleh menggunakan pemalar enum dan bukan rentetan. Contohnya untuk menentukan peralihan:

transitions.withExternal() .source(ApplicationReviewStates.PEER_REVIEW) .target(ApplicationReviewStates.PRINCIPAL_REVIEW) .event(ApplicationReviewEvents.APPROVE)

14. Kesimpulannya

Artikel ini meneroka beberapa ciri mesin keadaan Spring.

Seperti biasa anda boleh menemui contoh kod sumber di GitHub.