springspring-statemachine

Persiting a state machine context in HttpSession java.io.NotSerializableException: org.springframework.statemachine.support.DefaultStateMachineContext


I have created a StateMachinePersist implementation to persist the StateMachineContext y HttpSession:

@Suppress("UNCHECKED_CAST")
class SessionStateMachinePersist(private val session: HttpSession) : StateMachinePersist<States, String, String> {

    private val logger = LoggerFactory.getLogger(javaClass)

    override fun read(contextObj: String): StateMachineContext<States, String>?  {
        val stateMachineContext = session.getAttribute("${session.id}-$contextObj") as StateMachineContext<States, String>?
        logger.debug("Session {} has state machine context {}?", session.id, contextObj, stateMachineContext != null)
        return stateMachineContext
    }

    override fun write(context: StateMachineContext<States, String>, contextObj: String) {
        session.setAttribute("${session.id}-$contextObj", context)
    }
}

When it is going to persist the DefaultStateMachineContext it throws the following exception since the DefaultStateMachineContext isn't serializable: Caused by: java.io.NotSerializableException: org.springframework.statemachine.support.DefaultStateMachineContext

How can I use a StateMachineContext implementation that is serializable?


Solution

  • This is how I managed to make the State Machine Context serializable and persist it in the Http Session:

    SerializableStateMachineContext

    class SerializableStateMachineContext<S, E>(var id: String?, var childs: List<StateMachineContext<S, E>>?, var state: S?,
                                                var historyStates: Map<S, S>?, var event: E?, var eventHeaders: Map<String, Any>?,
                                                var extendedState: SerializableExtendedState?) : Serializable {
    
        constructor() : this(null, null, null, null, null, null, null)
    
    }
    

    SerializableExtendedState

    class SerializableExtendedState(var variables: Map<Any, Any>) : Serializable {
    
        constructor() : this(mutableMapOf())
    
    }
    

    SessionStateMachinePersist

    @Suppress("UNCHECKED_CAST")
    class SessionStateMachinePersist(private val session: HttpSession) : StateMachinePersist<States, String, String> {
    
        private val logger = LoggerFactory.getLogger(javaClass)
    
        override fun read(contextObj: String): StateMachineContext<States, String>? {
            val stateMachineContext = session.getAttribute("${session.id}-$contextObj") as SerializableStateMachineContext<States, String>?
            logger.debug("Session {} has state machine context {}?", session.id, contextObj, stateMachineContext != null)
            return DefaultStateMachineContext(stateMachineContext?.childs ?: mutableListOf(), stateMachineContext?.state, stateMachineContext?.event,
                    stateMachineContext?.eventHeaders, DefaultExtendedState(stateMachineContext?.extendedState?.variables ?: mutableMapOf()), stateMachineContext?.historyStates)
        }
    
        override fun write(context: StateMachineContext<States, String>, contextObj: String) {
            val serializableExtendedState = SerializableExtendedState(context.extendedState.variables.toMap())
            val serializableStateMachineContext = SerializableStateMachineContext(null, context.childs, context.state, context.historyStates, context.event, context.eventHeaders, serializableExtendedState)
            session.setAttribute("${session.id}-$contextObj", serializableStateMachineContext)
        }
    }
    

    Then, wherever you have to operate with the state machine you just have to restore it, execute the trigger operation and persist it, e.g.:

        @PostMapping(value = ["/process-input"], consumes = [MediaType.APPLICATION_JSON_VALUE], produces = [MediaType.APPLICATION_JSON_VALUE])
        @ResponseBody
        fun processInput(session: HttpSession, @RequestBody projectCreationInput: ProjectCreationInput, model: ModelMap, status: SessionStatus) {
    
            val stateMachinePersister = buildStateMachinePersister(session)
            stateMachinePersister.restore(stateMachine, "myid")
    
            ...
    
            triggerStateMachineExecution(projectCreationInput, event, status)
    
            ...
    
            stateMachinePersister.persist(stateMachine, "myid")
    
        }
    
        private fun buildStateMachinePersister(session: HttpSession): DefaultStateMachinePersister<States, String, String> {
            val stateMachinePersist = SessionStateMachinePersist(session)
            return DefaultStateMachinePersister(stateMachinePersist)
        }