├── .gitignore ├── README.md ├── src └── main │ ├── java │ └── com │ │ └── mosa │ │ ├── entity │ │ ├── model │ │ │ ├── EntityEvents.java │ │ │ ├── EntityStates.java │ │ │ └── Entity.java │ │ ├── utils │ │ │ └── EntityConstants.java │ │ ├── repository │ │ │ └── EntityRepository.java │ │ ├── statemachine │ │ │ ├── EntityPersistHandlerConfig.java │ │ │ ├── actions │ │ │ │ └── IdleToActiveAction.java │ │ │ ├── guards │ │ │ │ └── IdleToActiveGuard.java │ │ │ ├── EntityPersistStateChangeListener.java │ │ │ └── EntityStateMachineConfig.java │ │ ├── controller │ │ │ └── EntityController.java │ │ └── service │ │ │ └── EntityService.java │ │ └── Application.java │ └── resources │ ├── db │ └── migration │ │ └── V1__init.sql │ └── application.yaml ├── pom.xml └── Spring Statemachine.jmx /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /target 3 | *.iml -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # spring-statemachine-persist-example 2 | An example spring boot application using spring's state machine with persist recipe. 3 | -------------------------------------------------------------------------------- /src/main/java/com/mosa/entity/model/EntityEvents.java: -------------------------------------------------------------------------------- 1 | package com.mosa.entity.model; 2 | 3 | public enum EntityEvents { 4 | ACTIVATE, IDLE, DELETE 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/com/mosa/entity/model/EntityStates.java: -------------------------------------------------------------------------------- 1 | package com.mosa.entity.model; 2 | 3 | public enum EntityStates { 4 | IDLE, ACTIVE, DELETED; 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/com/mosa/entity/utils/EntityConstants.java: -------------------------------------------------------------------------------- 1 | package com.mosa.entity.utils; 2 | 3 | public class EntityConstants { 4 | public final static String entityHeader = "entity"; 5 | } 6 | -------------------------------------------------------------------------------- /src/main/resources/db/migration/V1__init.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `entity` ( 2 | `id` INT PRIMARY KEY AUTO_INCREMENT, 3 | `name` VARCHAR(255) NOT NULL UNIQUE, 4 | `state` VARCHAR(255) NOT NULL 5 | ); -------------------------------------------------------------------------------- /src/main/java/com/mosa/entity/repository/EntityRepository.java: -------------------------------------------------------------------------------- 1 | package com.mosa.entity.repository; 2 | 3 | import com.mosa.entity.model.Entity; 4 | import org.springframework.data.repository.CrudRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | public interface EntityRepository extends CrudRepository { 9 | } -------------------------------------------------------------------------------- /src/main/java/com/mosa/Application.java: -------------------------------------------------------------------------------- 1 | package com.mosa; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.boot.autoconfigure.domain.EntityScan; 6 | 7 | @SpringBootApplication 8 | @EntityScan 9 | public class Application { 10 | public static void main(String[] args) { 11 | SpringApplication.run(Application.class, args); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/mosa/entity/model/Entity.java: -------------------------------------------------------------------------------- 1 | package com.mosa.entity.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import javax.persistence.*; 9 | 10 | @javax.persistence.Entity 11 | @Table(name = "entity") 12 | @Data 13 | @Builder 14 | @NoArgsConstructor 15 | @AllArgsConstructor 16 | public class Entity { 17 | @Id 18 | @Column(name = "id", nullable = false, updatable = false) 19 | @GeneratedValue(strategy = GenerationType.AUTO) 20 | private Long id; 21 | 22 | @Column(name = "name", nullable = false, unique = true) 23 | private String name; 24 | 25 | @Column(name = "state") 26 | @Enumerated(value = EnumType.STRING) 27 | private EntityStates state; 28 | } 29 | -------------------------------------------------------------------------------- /src/main/resources/application.yaml: -------------------------------------------------------------------------------- 1 | spring: 2 | datasource: 3 | # Connection url for the database 4 | url: jdbc:mysql://localhost:3306/ssm?useSSL=false 5 | # Username and password 6 | username: root 7 | password: root 8 | # Keep the connection alive if idle for a long time (needed in production) 9 | testWhileIdle: true 10 | validationQuery: SELECT 1 11 | jpa: 12 | # Show or not log for each sql query 13 | show-sql: false 14 | hibernate: 15 | # Hibernate ddl auto (create, create-drop, update): with "update" the database 16 | # schema will be automatically updated accordingly to java entities found in 17 | # the project 18 | ddl-auto: update 19 | properties: 20 | hibernate: 21 | # Allows Hibernate to generate SQL optimized for a particular DBMS 22 | dialect: org.hibernate.dialect.MySQL5Dialect 23 | -------------------------------------------------------------------------------- /src/main/java/com/mosa/entity/statemachine/EntityPersistHandlerConfig.java: -------------------------------------------------------------------------------- 1 | package com.mosa.entity.statemachine; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.statemachine.StateMachine; 7 | import org.springframework.statemachine.recipes.persist.PersistStateMachineHandler; 8 | 9 | @Configuration 10 | public class EntityPersistHandlerConfig { 11 | 12 | @Autowired 13 | private StateMachine entityStateMachine; 14 | 15 | @Autowired 16 | private EntityPersistStateChangeListener entityPersistStateChangeListener; 17 | 18 | @Bean 19 | public PersistStateMachineHandler persistStateMachineHandler() { 20 | PersistStateMachineHandler handler = new PersistStateMachineHandler(entityStateMachine); 21 | handler.addPersistStateChangeListener(entityPersistStateChangeListener); 22 | return handler; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/mosa/entity/statemachine/actions/IdleToActiveAction.java: -------------------------------------------------------------------------------- 1 | package com.mosa.entity.statemachine.actions; 2 | 3 | import com.mosa.entity.model.Entity; 4 | import com.mosa.entity.utils.EntityConstants; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.statemachine.StateContext; 8 | import org.springframework.statemachine.action.Action; 9 | import org.springframework.stereotype.Component; 10 | 11 | @Component 12 | public class IdleToActiveAction implements Action { 13 | 14 | private final static Logger logger = LoggerFactory.getLogger(IdleToActiveAction.class); 15 | 16 | @Override 17 | public void execute(StateContext context) { 18 | Entity entity = (Entity) context.getMessageHeader(EntityConstants.entityHeader); 19 | if (entity == null) { 20 | logger.debug("Action: Wrong transition?"); 21 | } else { 22 | logger.debug("Action: changing the idle entity to active.. {}", entity); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/mosa/entity/statemachine/guards/IdleToActiveGuard.java: -------------------------------------------------------------------------------- 1 | package com.mosa.entity.statemachine.guards; 2 | 3 | import com.mosa.entity.model.Entity; 4 | import com.mosa.entity.utils.EntityConstants; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.statemachine.StateContext; 8 | import org.springframework.statemachine.guard.Guard; 9 | import org.springframework.stereotype.Component; 10 | 11 | @Component 12 | public class IdleToActiveGuard implements Guard { 13 | 14 | private final static Logger logger = LoggerFactory.getLogger(IdleToActiveGuard.class); 15 | 16 | @Override 17 | public boolean evaluate(StateContext context) { 18 | Entity entity = (Entity) context.getMessageHeader(EntityConstants.entityHeader); 19 | if (entity == null) { 20 | logger.debug("Guard: Wrong transition?"); 21 | } else { 22 | logger.debug("Guard: protecting the transition.. {}", entity); 23 | } 24 | return entity != null; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/mosa/entity/controller/EntityController.java: -------------------------------------------------------------------------------- 1 | package com.mosa.entity.controller; 2 | 3 | import com.mosa.entity.model.Entity; 4 | import com.mosa.entity.model.EntityEvents; 5 | import com.mosa.entity.service.EntityService; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.web.bind.annotation.*; 8 | 9 | import java.util.List; 10 | 11 | @RestController 12 | @RequestMapping("/entity") 13 | public class EntityController { 14 | 15 | @Autowired 16 | private EntityService entityService; 17 | 18 | @RequestMapping(value = "/") 19 | public List getEntities() { 20 | return entityService.getEntities(); 21 | } 22 | 23 | @RequestMapping(value = "/{id}") 24 | public Entity getEntity(@PathVariable("id") Long id) { 25 | return entityService.getEntity(id); 26 | } 27 | 28 | @RequestMapping(value = "/create", method = RequestMethod.POST) 29 | public Entity createEntity(@RequestBody Entity entity) { 30 | return entityService.createEntity(entity); 31 | } 32 | 33 | @RequestMapping(value = "/{id}/update/{event}") 34 | public Boolean sendEvent(@PathVariable("id") Long id, @PathVariable("event") EntityEvents event) { 35 | return entityService.updateState(id, event); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/mosa/entity/service/EntityService.java: -------------------------------------------------------------------------------- 1 | package com.mosa.entity.service; 2 | 3 | import com.google.common.collect.Lists; 4 | import com.mosa.entity.model.Entity; 5 | import com.mosa.entity.model.EntityEvents; 6 | import com.mosa.entity.repository.EntityRepository; 7 | import com.mosa.entity.utils.EntityConstants; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.messaging.support.MessageBuilder; 10 | import org.springframework.statemachine.recipes.persist.PersistStateMachineHandler; 11 | import org.springframework.stereotype.Service; 12 | 13 | import java.util.List; 14 | 15 | @Service 16 | public class EntityService { 17 | 18 | @Autowired 19 | private EntityRepository entityRepository; 20 | 21 | @Autowired 22 | private PersistStateMachineHandler persistStateMachineHandler; 23 | 24 | public List getEntities() { 25 | return Lists.newArrayList(entityRepository.findAll()); 26 | } 27 | 28 | public Entity getEntity(Long id) { 29 | return entityRepository.findOne(id); 30 | } 31 | 32 | public Entity createEntity(Entity entity) { 33 | return entityRepository.save(entity); 34 | } 35 | 36 | public Boolean updateState(Long id, EntityEvents event) { 37 | Entity entity = entityRepository.findOne(id); 38 | return persistStateMachineHandler.handleEventWithState( 39 | MessageBuilder.withPayload(event.name()).setHeader(EntityConstants.entityHeader, entity).build(), 40 | entity.getState().name() 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/mosa/entity/statemachine/EntityPersistStateChangeListener.java: -------------------------------------------------------------------------------- 1 | package com.mosa.entity.statemachine; 2 | 3 | import com.mosa.entity.model.Entity; 4 | import com.mosa.entity.model.EntityStates; 5 | import com.mosa.entity.repository.EntityRepository; 6 | import com.mosa.entity.utils.EntityConstants; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.messaging.Message; 11 | import org.springframework.statemachine.StateMachine; 12 | import org.springframework.statemachine.recipes.persist.PersistStateMachineHandler.PersistStateChangeListener; 13 | import org.springframework.statemachine.state.State; 14 | import org.springframework.statemachine.transition.Transition; 15 | import org.springframework.stereotype.Component; 16 | 17 | @Component 18 | public class EntityPersistStateChangeListener implements PersistStateChangeListener { 19 | 20 | private final static Logger logger = LoggerFactory.getLogger(EntityPersistStateChangeListener.class); 21 | 22 | @Autowired 23 | private EntityRepository entityRepository; 24 | 25 | @Override 26 | public void onPersist(State state, 27 | Message message, 28 | Transition transition, 29 | StateMachine stateMachine) { 30 | if (message != null && message.getHeaders().containsKey(EntityConstants.entityHeader)) { 31 | Entity entity = message.getHeaders().get(EntityConstants.entityHeader, Entity.class); 32 | entity.setState(EntityStates.valueOf(state.getId())); 33 | logger.debug("Persisting: the new entity.. {}", entity); 34 | entityRepository.save(entity); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.mosa 8 | SpringStateMachine 9 | 1.0-SNAPSHOT 10 | 11 | 12 | org.springframework.boot 13 | spring-boot-starter-parent 14 | 1.4.4.RELEASE 15 | 16 | 17 | 18 | org.springframework.boot 19 | spring-boot-starter-web 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-starter-data-jpa 24 | 25 | 26 | 27 | org.springframework.statemachine 28 | spring-statemachine-core 29 | 1.2.1.RELEASE 30 | 31 | 32 | org.springframework.statemachine 33 | spring-statemachine-recipes-common 34 | 1.2.1.RELEASE 35 | 36 | 37 | 38 | mysql 39 | mysql-connector-java 40 | 6.0.5 41 | 42 | 43 | org.flywaydb 44 | flyway-core 45 | 4.0.3 46 | 47 | 48 | 49 | org.projectlombok 50 | lombok 51 | 1.16.12 52 | 53 | 54 | com.google.guava 55 | guava 56 | 21.0 57 | 58 | 59 | 60 | 61 | 62 | 63 | org.apache.maven.plugins 64 | maven-compiler-plugin 65 | 66 | 1.8 67 | 1.8 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/main/java/com/mosa/entity/statemachine/EntityStateMachineConfig.java: -------------------------------------------------------------------------------- 1 | package com.mosa.entity.statemachine; 2 | 3 | import com.mosa.entity.model.EntityEvents; 4 | import com.mosa.entity.model.EntityStates; 5 | import com.mosa.entity.statemachine.actions.IdleToActiveAction; 6 | import com.mosa.entity.statemachine.guards.IdleToActiveGuard; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.statemachine.config.EnableStateMachine; 10 | import org.springframework.statemachine.config.StateMachineConfigurerAdapter; 11 | import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; 12 | import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; 13 | 14 | import java.util.EnumSet; 15 | import java.util.HashSet; 16 | import java.util.Set; 17 | 18 | @Configuration 19 | @EnableStateMachine(name = "entityStateMachine") 20 | public class EntityStateMachineConfig extends StateMachineConfigurerAdapter { 21 | 22 | @Autowired 23 | private IdleToActiveGuard idleToActiveGuard; 24 | 25 | @Autowired 26 | private IdleToActiveAction idleToActiveAction; 27 | 28 | @Override 29 | public void configure(StateMachineStateConfigurer states) 30 | throws Exception { 31 | Set stringStates = new HashSet<>(); 32 | EnumSet.allOf(EntityStates.class).forEach(entity -> stringStates.add(entity.name())); 33 | states.withStates() 34 | .initial(EntityStates.IDLE.name()) 35 | .end(EntityStates.DELETED.name()) 36 | .states(stringStates); 37 | } 38 | 39 | @Override 40 | public void configure(StateMachineTransitionConfigurer transitions) 41 | throws Exception { 42 | transitions.withExternal() 43 | .source(EntityStates.IDLE.name()).target(EntityStates.ACTIVE.name()) 44 | .event(EntityEvents.ACTIVATE.name()) 45 | .guard(idleToActiveGuard).action(idleToActiveAction) 46 | .and().withExternal() 47 | .source(EntityStates.ACTIVE.name()).target(EntityStates.IDLE.name()) 48 | .event(EntityEvents.IDLE.name()) 49 | .and().withExternal() 50 | .source(EntityStates.IDLE.name()).target(EntityStates.DELETED.name()) 51 | .event(EntityEvents.DELETE.name()) 52 | .and().withExternal() 53 | .source(EntityStates.ACTIVE.name()).target(EntityStates.DELETED.name()) 54 | .event(EntityEvents.DELETE.name()); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Spring Statemachine.jmx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | false 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | continue 16 | 17 | false 18 | 100 19 | 20 | 10 21 | 1 22 | 1486138100000 23 | 1486138100000 24 | false 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | Accept 33 | application/json 34 | 35 | 36 | Content-Type 37 | application/json 38 | 39 | 40 | 41 | 42 | 43 | true 44 | 45 | 46 | 47 | false 48 | { 49 | "name": "${__UUID}", 50 | "state": "IDLE" 51 | } 52 | = 53 | 54 | 55 | 56 | localhost 57 | 8080 58 | 59 | 60 | 61 | UTF-8 62 | /entity/create 63 | POST 64 | true 65 | false 66 | true 67 | false 68 | false 69 | 70 | 71 | 72 | 73 | 74 | continue 75 | 76 | false 77 | 100 78 | 79 | 10 80 | 1 81 | 1486142801000 82 | 1486142801000 83 | false 84 | 85 | 86 | 87 | 88 | 89 | 1 90 | 91 | 1 92 | entity_id 93 | 94 | false 95 | 96 | 97 | 98 | 99 | 100 | 101 | localhost 102 | 8080 103 | 104 | 105 | 106 | 107 | /entity/${entity_id} 108 | GET 109 | true 110 | false 111 | true 112 | false 113 | false 114 | 115 | 116 | 117 | 118 | 119 | continue 120 | 121 | false 122 | 50 123 | 124 | 10 125 | 1 126 | 1486140713000 127 | 1486140713000 128 | false 129 | 130 | 131 | 132 | 133 | 134 | 1 135 | 136 | 1 137 | activate_value 138 | 139 | false 140 | 141 | 142 | 143 | 144 | 145 | 146 | localhost 147 | 8080 148 | 149 | 150 | 151 | 152 | /entity/${activate_value}/update/ACTIVATE 153 | GET 154 | true 155 | false 156 | true 157 | false 158 | false 159 | 160 | 161 | 162 | 163 | 164 | continue 165 | 166 | false 167 | 50 168 | 169 | 10 170 | 1 171 | 1486140907000 172 | 1486140907000 173 | false 174 | 175 | 176 | 177 | 178 | 179 | 501 180 | 181 | 1 182 | delete_value 183 | 184 | false 185 | 186 | 187 | 188 | 189 | 190 | 191 | localhost 192 | 8080 193 | 194 | 195 | 196 | 197 | /entity/${delete_value}/update/DELETE 198 | GET 199 | true 200 | false 201 | true 202 | false 203 | false 204 | 205 | 206 | 207 | 208 | 209 | false 210 | 211 | saveConfig 212 | 213 | 214 | true 215 | true 216 | true 217 | 218 | true 219 | true 220 | true 221 | true 222 | false 223 | true 224 | true 225 | false 226 | false 227 | false 228 | false 229 | false 230 | false 231 | false 232 | false 233 | 0 234 | true 235 | 236 | 237 | 238 | 239 | 240 | 241 | false 242 | 243 | saveConfig 244 | 245 | 246 | true 247 | true 248 | true 249 | 250 | true 251 | true 252 | true 253 | true 254 | false 255 | true 256 | true 257 | false 258 | false 259 | false 260 | false 261 | false 262 | false 263 | false 264 | false 265 | 0 266 | true 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | --------------------------------------------------------------------------------