Panduan untuk Proyek Mesin Status Musim Semi

1. Perkenalan

Artikel ini difokuskan pada proyek Spring's State Machine - yang dapat digunakan untuk mewakili alur kerja atau jenis masalah representasi automata keadaan terbatas lainnya.

2. Ketergantungan Maven

Untuk memulai, kita perlu menambahkan dependensi Maven utama:

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

Versi terbaru dari ketergantungan ini dapat ditemukan di sini.

3. Konfigurasi Mesin Status

Sekarang, mari kita mulai dengan mendefinisikan mesin status sederhana:

@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 bahwa kelas ini diberi keterangan sebagai konfigurasi Spring konvensional serta mesin status. Ini juga perlu memperluas StateMachineConfigurerAdapter sehingga berbagai metode inisialisasi dapat dipanggil. Di salah satu metode konfigurasi, kami mendefinisikan semua kemungkinan status mesin status, di metode lain, bagaimana peristiwa mengubah status saat ini.

Konfigurasi di atas menetapkan mesin status transisi garis lurus yang cukup sederhana yang seharusnya cukup mudah diikuti.

Sekarang kita perlu memulai konteks Spring dan mendapatkan referensi ke mesin status yang ditentukan oleh konfigurasi kita:

@Autowired private StateMachine stateMachine;

Setelah kami memiliki mesin negara, itu perlu dimulai:

stateMachine.start();

Sekarang mesin kita dalam keadaan awal, kita bisa mengirim kejadian dan dengan demikian memicu transisi:

stateMachine.sendEvent("E1");

Kami selalu dapat memeriksa status mesin negara saat ini:

stateMachine.getState();

4. Tindakan

Mari kita tambahkan beberapa tindakan untuk dieksekusi di sekitar transisi status. Pertama, kami mendefinisikan tindakan kami sebagai kacang musim semi di file 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 transisi 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 dijalankan ketika transisi dari SI ke S1 melalui peristiwa E1 terjadi. Tindakan dapat dilampirkan ke negara bagian itu sendiri:

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

Fungsi definisi status ini menerima operasi yang akan dijalankan saat mesin berada dalam status target dan, secara opsional, penangan tindakan kesalahan.

Penangan tindakan kesalahan tidak jauh berbeda dari tindakan lainnya, tetapi akan dipanggil jika pengecualian dilemparkan kapan saja selama evaluasi tindakan status:

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

Dimungkinkan juga untuk mendaftarkan tindakan individu untuk transisi status masuk , lakukan , dan keluar :

@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 dijalankan pada transisi status yang sesuai. Misalnya, kami mungkin ingin memverifikasi beberapa prasyarat pada saat masuk atau memicu beberapa pelaporan pada saat keluar.

5. Pendengar Global

Pemroses acara global dapat ditentukan untuk mesin negara. Listener ini akan dipanggil setiap kali transisi status terjadi dan dapat digunakan untuk hal-hal seperti logging atau keamanan.

Pertama, kita perlu menambahkan metode konfigurasi lain - metode yang tidak berurusan dengan status atau transisi tetapi dengan konfigurasi untuk mesin status itu sendiri.

Kita perlu mendefinisikan 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 kami hanya mengesampingkan stateChanged meskipun banyak pengait lain yang tersedia.

6. Status Diperluas

Spring State Machine melacak statusnya, tetapi untuk melacak status aplikasi kita , baik itu beberapa nilai yang dihitung, entri dari admin, atau respons dari memanggil sistem eksternal, kita perlu menggunakan apa yang disebut status diperpanjang .

Misalkan kita ingin memastikan bahwa permohonan akun melewati dua tingkat persetujuan. Kami dapat melacak jumlah persetujuan menggunakan bilangan bulat yang disimpan dalam status diperpanjang:

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

7. Penjaga

Penjaga dapat digunakan untuk memvalidasi beberapa data sebelum transisi ke keadaan dijalankan. Seorang penjaga terlihat sangat mirip dengan suatu 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 konstanta string untuk mendefinisikan status dan peristiwa untuk kejelasan dan kesederhanaan. Pada sistem produksi dunia nyata, orang mungkin ingin menggunakan enum Java untuk menghindari kesalahan ejaan dan mendapatkan lebih banyak keamanan tipe.

Pertama, kita perlu mendefinisikan semua kemungkinan status dan kejadian di sistem kita:

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

Kita juga perlu meneruskan enum kita sebagai parameter generik saat kita memperluas konfigurasi:

public class SimpleEnumStateMachineConfiguration extends StateMachineConfigurerAdapter 

Setelah ditentukan, kita dapat menggunakan konstanta enum sebagai ganti string. Misalnya untuk mendefinisikan transisi:

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

14. Kesimpulan

Artikel ini membahas beberapa fitur mesin status Spring.

Seperti biasa, Anda dapat menemukan kode sumber sampel di GitHub.