├── src ├── resources │ └── application.properties └── main │ ├── java │ └── com │ │ └── mypanacea │ │ ├── domain │ │ ├── statemachine │ │ │ ├── event │ │ │ │ └── PurchaseEvent.java │ │ │ ├── state │ │ │ │ └── PurchaseState.java │ │ │ ├── guard │ │ │ │ └── HideGuard.java │ │ │ ├── action │ │ │ │ ├── ErrorAction.java │ │ │ │ ├── BuyAction.java │ │ │ │ ├── CancelAction.java │ │ │ │ └── ReservedAction.java │ │ │ ├── persist │ │ │ │ └── PurchaseStateMachinePersister.java │ │ │ └── listener │ │ │ │ └── PurchaseStateMachineApplicationListener.java │ │ └── service │ │ │ └── purchase │ │ │ ├── PurchaseService.java │ │ │ └── impl │ │ │ └── DefaultPurchaseService.java │ │ ├── StatemachineApplication.java │ │ ├── web │ │ └── controller │ │ │ └── PurchaseController.java │ │ └── config │ │ └── StateMachineConfig.java │ └── test │ └── java │ └── com │ └── mypanacea │ └── StatemachineApplicationTests.java ├── README.md └── pom.xml /src/resources/application.properties: -------------------------------------------------------------------------------- 1 | logging.level.root=OFF 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Statemachine 2 | Spring State Machine Wellcome! 3 | 4 | This webapp is a purchase abstraction. App used Spring StateMachine Framework for implementation 5 | finite state machine theory 6 | -------------------------------------------------------------------------------- /src/main/java/com/mypanacea/domain/statemachine/event/PurchaseEvent.java: -------------------------------------------------------------------------------- 1 | package com.mypanacea.domain.statemachine.event; 2 | 3 | public enum PurchaseEvent { 4 | RESERVE, BUY, RESERVE_DECLINE 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/com/mypanacea/domain/statemachine/state/PurchaseState.java: -------------------------------------------------------------------------------- 1 | package com.mypanacea.domain.statemachine.state; 2 | 3 | public enum PurchaseState { 4 | NEW, RESERVED, CANCEL_RESERVED, PURCHASE_COMPLETE 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/com/mypanacea/StatemachineApplication.java: -------------------------------------------------------------------------------- 1 | package com.mypanacea; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.context.annotation.ComponentScan; 6 | 7 | @SpringBootApplication 8 | public class StatemachineApplication { 9 | 10 | public static void main(String[] args) { 11 | SpringApplication.run(StatemachineApplication.class, args); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/mypanacea/domain/statemachine/guard/HideGuard.java: -------------------------------------------------------------------------------- 1 | package com.mypanacea.domain.statemachine.guard; 2 | 3 | import com.mypanacea.domain.statemachine.event.PurchaseEvent; 4 | import com.mypanacea.domain.statemachine.state.PurchaseState; 5 | import org.springframework.statemachine.StateContext; 6 | import org.springframework.statemachine.guard.Guard; 7 | 8 | 9 | public class HideGuard implements Guard { 10 | 11 | @Override 12 | public boolean evaluate(StateContext context) { 13 | return false; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/mypanacea/domain/statemachine/action/ErrorAction.java: -------------------------------------------------------------------------------- 1 | package com.mypanacea.domain.statemachine.action; 2 | 3 | 4 | import com.mypanacea.domain.statemachine.event.PurchaseEvent; 5 | import com.mypanacea.domain.statemachine.state.PurchaseState; 6 | import org.springframework.statemachine.StateContext; 7 | import org.springframework.statemachine.action.Action; 8 | 9 | public class ErrorAction implements Action { 10 | @Override 11 | public void execute(final StateContext context) { 12 | System.out.println("Ошибка при переходе в статус " + context.getTarget().getId()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/mypanacea/domain/statemachine/action/BuyAction.java: -------------------------------------------------------------------------------- 1 | package com.mypanacea.domain.statemachine.action; 2 | 3 | import com.mypanacea.domain.statemachine.event.PurchaseEvent; 4 | import com.mypanacea.domain.statemachine.state.PurchaseState; 5 | import org.springframework.statemachine.StateContext; 6 | import org.springframework.statemachine.action.Action; 7 | 8 | 9 | public class BuyAction implements Action { 10 | @Override 11 | public void execute(final StateContext context) { 12 | final String productId = context.getExtendedState().get("PRODUCT_ID", String.class); 13 | System.out.println("Товар с номером " + productId + " успешно куплен"); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/mypanacea/domain/statemachine/action/CancelAction.java: -------------------------------------------------------------------------------- 1 | package com.mypanacea.domain.statemachine.action; 2 | 3 | import com.mypanacea.domain.statemachine.event.PurchaseEvent; 4 | import com.mypanacea.domain.statemachine.state.PurchaseState; 5 | import org.springframework.statemachine.StateContext; 6 | import org.springframework.statemachine.action.Action; 7 | 8 | public class CancelAction implements Action { 9 | @Override 10 | public void execute(final StateContext context) { 11 | final String productId = context.getExtendedState().get("PRODUCT_ID", String.class); 12 | System.out.println("Резервирование товара " + productId + " отменено"); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/mypanacea/domain/statemachine/action/ReservedAction.java: -------------------------------------------------------------------------------- 1 | package com.mypanacea.domain.statemachine.action; 2 | 3 | import com.mypanacea.domain.statemachine.event.PurchaseEvent; 4 | import com.mypanacea.domain.statemachine.state.PurchaseState; 5 | import org.springframework.statemachine.StateContext; 6 | import org.springframework.statemachine.action.Action; 7 | 8 | public class ReservedAction implements Action { 9 | @Override 10 | public void execute(final StateContext context) { 11 | final String productId = context.getExtendedState().get("PRODUCT_ID", String.class); 12 | System.out.println("Товар с номером " + productId + " зарезервирован."); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/mypanacea/domain/statemachine/persist/PurchaseStateMachinePersister.java: -------------------------------------------------------------------------------- 1 | package com.mypanacea.domain.statemachine.persist; 2 | 3 | import com.mypanacea.domain.statemachine.event.PurchaseEvent; 4 | import com.mypanacea.domain.statemachine.state.PurchaseState; 5 | import org.springframework.statemachine.StateMachineContext; 6 | import org.springframework.statemachine.StateMachinePersist; 7 | 8 | import java.util.HashMap; 9 | 10 | public class PurchaseStateMachinePersister implements StateMachinePersist { 11 | 12 | private final HashMap> contexts = new HashMap<>(); 13 | 14 | @Override 15 | public void write(final StateMachineContext context, final String contextObj) { 16 | contexts.put(contextObj, context); 17 | } 18 | 19 | @Override 20 | public StateMachineContext read(final String contextObj) { 21 | return contexts.get(contextObj); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/mypanacea/web/controller/PurchaseController.java: -------------------------------------------------------------------------------- 1 | package com.mypanacea.web.controller; 2 | 3 | import com.mypanacea.domain.service.purchase.PurchaseService; 4 | import org.springframework.web.bind.annotation.RequestMapping; 5 | import org.springframework.web.bind.annotation.RestController; 6 | 7 | @RestController 8 | @SuppressWarnings("unused") 9 | public class PurchaseController { 10 | 11 | private final PurchaseService purchaseService; 12 | 13 | public PurchaseController(PurchaseService purchaseService) { 14 | this.purchaseService = purchaseService; 15 | } 16 | 17 | @RequestMapping(path = "/reserve") 18 | public boolean reserve(final String userId, final String productId) { 19 | return purchaseService.reserved(userId, productId); 20 | } 21 | 22 | @RequestMapping(path = "/cancel") 23 | public boolean cancelReserve(final String userId) { 24 | return purchaseService.cancelReserve(userId); 25 | } 26 | 27 | @RequestMapping(path = "/buy") 28 | public boolean buyReserve(final String userId) { 29 | return purchaseService.buy(userId); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.1.6.RELEASE 9 | 10 | 11 | com.example 12 | statemachine 13 | 0.0.1-SNAPSHOT 14 | statemachine 15 | Demo project for Spring Boot 16 | 17 | 18 | 1.8 19 | 20 | 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter-web 25 | 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter-test 30 | test 31 | 32 | 33 | org.springframework.statemachine 34 | spring-statemachine-core 35 | 2.1.3.RELEASE 36 | 37 | 38 | org.springframework.statemachine 39 | spring-statemachine-test 40 | 2.1.3.RELEASE 41 | test 42 | 43 | 44 | 45 | 46 | 47 | 48 | org.springframework.boot 49 | spring-boot-maven-plugin 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/main/java/com/mypanacea/domain/service/purchase/PurchaseService.java: -------------------------------------------------------------------------------- 1 | package com.mypanacea.domain.service.purchase; 2 | 3 | public interface PurchaseService { 4 | /** 5 | * Резервирование товара перед покупкой, зарезервированный товар может находиться в корзине сколько угодно долго 6 | * 7 | * @param userId id пользователя, так как приложение простое, для того чтоб различать пользователей id будем 8 | * принимать прямо в http-запросе 9 | * @param productId id продукта, который начинает процедуру покупки 10 | * @return успешная/не успешная операция, в нашем примере операция может стать не успешной если при попытке восстановить 11 | * машину их импровизированного репозитория произойдет ошибка. 12 | */ 13 | boolean reserved(String userId, String productId); 14 | 15 | /** 16 | * Отмена резервирования товара/удаление из пользовательской корзины 17 | * 18 | * @param userId id пользователя, так как приложение простое, для того чтоб различать пользователей id будем 19 | * принимать прямо в http-запросе 20 | * @return успешная/не успешная операция, в нашем примере операция может стать не успешной если при попытке восстановить 21 | * машину их импровизированного репозитория произойдет ошибка. 22 | */ 23 | boolean cancelReserve(String userId); 24 | 25 | /** 26 | * Покупка ранее зарезервированного товара 27 | * 28 | * @param userId id пользователя, так как приложение простое, для того чтоб различать пользователей id будем 29 | * принимать прямо в http-запросе 30 | * @return успешная/не успешная операция, в нашем примере операция может стать не успешной если при попытке восстановить 31 | * машину их импровизированного репозитория произойдет ошибка. 32 | */ 33 | boolean buy(String userId); 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/mypanacea/domain/statemachine/listener/PurchaseStateMachineApplicationListener.java: -------------------------------------------------------------------------------- 1 | package com.mypanacea.domain.statemachine.listener; 2 | 3 | import com.mypanacea.domain.statemachine.event.PurchaseEvent; 4 | import com.mypanacea.domain.statemachine.state.PurchaseState; 5 | import org.springframework.messaging.Message; 6 | import org.springframework.statemachine.StateContext; 7 | import org.springframework.statemachine.StateMachine; 8 | import org.springframework.statemachine.listener.StateMachineListener; 9 | import org.springframework.statemachine.state.State; 10 | import org.springframework.statemachine.transition.Transition; 11 | 12 | public class PurchaseStateMachineApplicationListener implements StateMachineListener { 13 | @Override 14 | public void stateChanged(final State from, final State to) { 15 | if (from.getId() != null) { 16 | System.out.println("Переход из статуса " + from.getId() + " в статус " + to.getId()); 17 | } 18 | } 19 | 20 | @Override 21 | public void stateEntered(final State state) { 22 | 23 | } 24 | 25 | @Override 26 | public void stateExited(final State state) { 27 | 28 | } 29 | 30 | @Override 31 | public void eventNotAccepted(final Message event) { 32 | System.out.println("Евент не принят " + event); 33 | } 34 | 35 | @Override 36 | public void transition(final Transition transition) { 37 | 38 | } 39 | 40 | @Override 41 | public void transitionStarted(final Transition transition) { 42 | 43 | } 44 | 45 | @Override 46 | public void transitionEnded(final Transition transition) { 47 | 48 | } 49 | 50 | @Override 51 | public void stateMachineStarted(final StateMachine stateMachine) { 52 | System.out.println("Machine started"); 53 | } 54 | 55 | @Override 56 | public void stateMachineStopped(final StateMachine stateMachine) { 57 | 58 | } 59 | 60 | @Override 61 | public void stateMachineError(final StateMachine stateMachine, Exception exception) { 62 | } 63 | 64 | @Override 65 | public void extendedStateChanged(final Object key, final Object value) { 66 | 67 | } 68 | 69 | @Override 70 | public void stateContext(final StateContext stateContext) { 71 | 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/mypanacea/domain/service/purchase/impl/DefaultPurchaseService.java: -------------------------------------------------------------------------------- 1 | package com.mypanacea.domain.service.purchase.impl; 2 | 3 | import com.mypanacea.domain.service.purchase.PurchaseService; 4 | import com.mypanacea.domain.statemachine.event.PurchaseEvent; 5 | import com.mypanacea.domain.statemachine.state.PurchaseState; 6 | import org.springframework.statemachine.StateMachine; 7 | import org.springframework.statemachine.config.StateMachineFactory; 8 | import org.springframework.statemachine.persist.StateMachinePersister; 9 | import org.springframework.stereotype.Service; 10 | 11 | import static com.mypanacea.domain.statemachine.event.PurchaseEvent.*; 12 | 13 | @Service 14 | @SuppressWarnings("all") 15 | public class DefaultPurchaseService implements PurchaseService { 16 | 17 | private final StateMachinePersister persister; 18 | 19 | private final StateMachineFactory stateMachineFactory; 20 | 21 | public DefaultPurchaseService( 22 | StateMachinePersister persister, 23 | StateMachineFactory stateMachineFactory 24 | ) { 25 | this.persister = persister; 26 | this.stateMachineFactory = stateMachineFactory; 27 | } 28 | 29 | @Override 30 | public boolean reserved(final String userId, final String productId) { 31 | final StateMachine stateMachine = stateMachineFactory.getStateMachine(); 32 | stateMachine.getExtendedState().getVariables().put("PRODUCT_ID", productId); 33 | stateMachine.sendEvent(RESERVE); 34 | try { 35 | persister.persist(stateMachine, userId); 36 | } catch (final Exception e) { 37 | e.printStackTrace(); 38 | return false; 39 | } 40 | return true; 41 | } 42 | 43 | @Override 44 | public boolean cancelReserve(final String userId) { 45 | final StateMachine stateMachine = stateMachineFactory.getStateMachine(); 46 | try { 47 | persister.restore(stateMachine, userId); 48 | stateMachine.sendEvent(RESERVE_DECLINE); 49 | } catch (Exception e) { 50 | e.printStackTrace(); 51 | return false; 52 | } 53 | return true; 54 | } 55 | 56 | @Override 57 | public boolean buy(final String userId) { 58 | final StateMachine stateMachine = stateMachineFactory.getStateMachine(); 59 | try { 60 | persister.restore(stateMachine, userId); 61 | stateMachine.sendEvent(BUY); 62 | } catch (Exception e) { 63 | e.printStackTrace(); 64 | return false; 65 | } 66 | return true; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/test/java/com/mypanacea/StatemachineApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.mypanacea; 2 | 3 | import com.mypanacea.domain.statemachine.event.PurchaseEvent; 4 | import com.mypanacea.domain.statemachine.state.PurchaseState; 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | import org.springframework.statemachine.StateMachine; 10 | import org.springframework.statemachine.config.StateMachineFactory; 11 | import org.springframework.statemachine.test.StateMachineTestPlan; 12 | import org.springframework.statemachine.test.StateMachineTestPlanBuilder; 13 | import org.springframework.test.context.junit4.SpringRunner; 14 | 15 | import static com.mypanacea.domain.statemachine.event.PurchaseEvent.*; 16 | import static com.mypanacea.domain.statemachine.state.PurchaseState.*; 17 | 18 | @RunWith(SpringRunner.class) 19 | @SpringBootTest 20 | public class StatemachineApplicationTests { 21 | 22 | @Autowired 23 | private StateMachineFactory factory; 24 | 25 | @Test 26 | public void contextLoads() { 27 | } 28 | 29 | @Test 30 | public void testWhenReservedCancel() throws Exception { 31 | StateMachine machine = factory.getStateMachine(); 32 | StateMachineTestPlan plan = 33 | StateMachineTestPlanBuilder.builder() 34 | .defaultAwaitTime(2) 35 | .stateMachine(machine) 36 | .step() 37 | .expectStates(NEW) 38 | .expectStateChanged(0) 39 | .and() 40 | .step() 41 | .sendEvent(RESERVE) 42 | .expectState(RESERVED) 43 | .expectStateChanged(1) 44 | .and() 45 | .step() 46 | .sendEvent(RESERVE_DECLINE) 47 | .expectState(CANCEL_RESERVED) 48 | .expectStateChanged(1) 49 | .and() 50 | .build(); 51 | plan.test(); 52 | } 53 | 54 | @Test 55 | public void testWhenPurchaseComplete() throws Exception { 56 | StateMachine machine = factory.getStateMachine(); 57 | StateMachineTestPlan plan = 58 | StateMachineTestPlanBuilder.builder() 59 | .defaultAwaitTime(2) 60 | .stateMachine(machine) 61 | .step() 62 | .expectStates(NEW) 63 | .expectStateChanged(0) 64 | .and() 65 | .step() 66 | .sendEvent(RESERVE) 67 | .expectState(RESERVED) 68 | .expectStateChanged(1) 69 | .and() 70 | .step() 71 | .sendEvent(BUY) 72 | .expectState(PURCHASE_COMPLETE) 73 | .expectStateChanged(1) 74 | .and() 75 | .build(); 76 | plan.test(); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/com/mypanacea/config/StateMachineConfig.java: -------------------------------------------------------------------------------- 1 | package com.mypanacea.config; 2 | 3 | import com.mypanacea.domain.statemachine.action.BuyAction; 4 | import com.mypanacea.domain.statemachine.action.CancelAction; 5 | import com.mypanacea.domain.statemachine.action.ErrorAction; 6 | import com.mypanacea.domain.statemachine.action.ReservedAction; 7 | import com.mypanacea.domain.statemachine.event.PurchaseEvent; 8 | import com.mypanacea.domain.statemachine.guard.HideGuard; 9 | import com.mypanacea.domain.statemachine.listener.PurchaseStateMachineApplicationListener; 10 | import com.mypanacea.domain.statemachine.persist.PurchaseStateMachinePersister; 11 | import com.mypanacea.domain.statemachine.state.PurchaseState; 12 | import org.springframework.context.annotation.Bean; 13 | import org.springframework.context.annotation.Configuration; 14 | import org.springframework.statemachine.action.Action; 15 | import org.springframework.statemachine.config.EnableStateMachineFactory; 16 | import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter; 17 | import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer; 18 | import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; 19 | import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; 20 | import org.springframework.statemachine.guard.Guard; 21 | import org.springframework.statemachine.persist.DefaultStateMachinePersister; 22 | import org.springframework.statemachine.persist.StateMachinePersister; 23 | 24 | import java.util.EnumSet; 25 | 26 | import static com.mypanacea.domain.statemachine.event.PurchaseEvent.*; 27 | import static com.mypanacea.domain.statemachine.state.PurchaseState.*; 28 | 29 | @Configuration 30 | @EnableStateMachineFactory 31 | public class StateMachineConfig extends EnumStateMachineConfigurerAdapter { 32 | 33 | @Override 34 | public void configure(final StateMachineConfigurationConfigurer config) throws Exception { 35 | config 36 | .withConfiguration() 37 | .autoStartup(true) 38 | .listener(new PurchaseStateMachineApplicationListener()); 39 | } 40 | 41 | @Override 42 | public void configure(final StateMachineStateConfigurer states) throws Exception { 43 | states 44 | .withStates() 45 | .initial(NEW) 46 | .end(PURCHASE_COMPLETE) 47 | .states(EnumSet.allOf(PurchaseState.class)); 48 | 49 | } 50 | 51 | @Override 52 | public void configure(final StateMachineTransitionConfigurer transitions) throws Exception { 53 | transitions 54 | .withExternal() 55 | .source(NEW) 56 | .target(RESERVED) 57 | .event(RESERVE) 58 | .action(reservedAction(), errorAction()) 59 | 60 | .and() 61 | .withExternal() 62 | .source(RESERVED) 63 | .target(CANCEL_RESERVED) 64 | .event(RESERVE_DECLINE) 65 | .action(cancelAction(), errorAction()) 66 | 67 | .and() 68 | .withExternal() 69 | .source(RESERVED) 70 | .target(PURCHASE_COMPLETE) 71 | .event(BUY) 72 | .guard(hideGuard()) 73 | .action(buyAction(), errorAction()); 74 | } 75 | 76 | @Bean 77 | public Action reservedAction() { 78 | return new ReservedAction(); 79 | } 80 | 81 | @Bean 82 | public Action cancelAction() { 83 | return new CancelAction(); 84 | } 85 | 86 | @Bean 87 | public Action buyAction() { 88 | return new BuyAction(); 89 | } 90 | 91 | @Bean 92 | public Action errorAction() { 93 | return new ErrorAction(); 94 | } 95 | 96 | @Bean 97 | public Guard hideGuard() { 98 | return new HideGuard(); 99 | } 100 | 101 | @Bean 102 | public StateMachinePersister persister() { 103 | return new DefaultStateMachinePersister<>(new PurchaseStateMachinePersister()); 104 | } 105 | } 106 | --------------------------------------------------------------------------------