Al igual que la ardilla, un animal pequeño , ágil , inteligente , alerta y lindo , squirrel-foundation tiene como objetivo proporcionar una implementación de máquina de estado Java liviana , altamente flexible y extensible , diagnosticable , fácil de usar y segura para uso empresarial.
Aquí está el diagrama de la máquina de estados que describe el cambio de estado de un cajero automático:
El código de muestra se puede encontrar en el paquete "org.squirrelframework.foundation.fsm.atm" .
squirrel-foundation se ha implementado en el repositorio central de Maven, por lo que solo necesita agregar la siguiente dependencia a pom.xml.
Última versión lanzada:
<dependency>
<groupId>org.squirrelframework</groupId>
<artifactId>squirrel-foundation</artifactId>
<version>0.3.10</version>
</dependency>
Última versión instantánea:
<dependency>
<groupId>org.squirrelframework</groupId>
<artifactId>squirrel-foundation</artifactId>
<version>0.3.11-SNAPSHOT</version>
</dependency>
Para probar rápidamente las funciones de la máquina de estado de squirrel, cree un proyecto maven e incluya la dependencia de squirrel-foundation correctamente. Luego simplemente ejecute el siguiente código de muestra.
public class QuickStartSample {
// 1. Define State Machine Event
enum FSMEvent {
ToA , ToB , ToC , ToD
}
// 2. Define State Machine Class
@ StateMachineParameters ( stateType = String . class , eventType = FSMEvent . class , contextType = Integer . class )
static class StateMachineSample extends AbstractUntypedStateMachine {
protected void fromAToB ( String from , String to , FSMEvent event , Integer context ) {
System . out . println ( "Transition from '" + from + "' to '" + to + "' on event '" + event +
"' with context '" + context + "'." );
}
protected void ontoB ( String from , String to , FSMEvent event , Integer context ) {
System . out . println ( "Entry State ' " + to + " ' ." );
}
}
public static void main ( String [] args ) {
// 3. Build State Transitions
UntypedStateMachineBuilder builder = StateMachineBuilderFactory . create ( StateMachineSample . class );
builder . externalTransition (). from ( "A" ). to ( "B" ). on ( FSMEvent . ToB ). callMethod ( "fromAToB" );
builder . onEntry ( "B" ). callMethod ( "ontoB" );
// 4. Use State Machine
UntypedStateMachine fsm = builder . newStateMachine ( "A" );
fsm . fire ( FSMEvent . ToB , 10 );
System . out . println ( "Current state is " + fsm . getCurrentState ());
}
}
En este momento es posible que tenga muchas preguntas sobre el código de muestra, tenga paciencia. La siguiente guía del usuario responderá a la mayoría de sus preguntas. Pero antes de entrar en detalles, es necesario tener conocimientos básicos de los conceptos de las máquinas de estados. Estos materiales son buenos para comprender los conceptos de las máquinas de estados. [diagramas-de-máquina-de-estados] [máquina-de-estados-qt]
squirrel-foundation admite API fluidas y métodos declarativos para declarar una máquina de estado, y también permite al usuario definir los métodos de acción de manera sencilla.
La interfaz StateMachine toma cuatro parámetros de tipo genérico.
Constructor de máquinas de estado
Para crear una máquina de estados, el usuario debe crear primero un generador de máquinas de estados. Por ejemplo:
StateMachineBuilder < MyStateMachine , MyState , MyEvent , MyContext > builder =
StateMachineBuilderFactory . create ( MyStateMachine . class , MyState . class , MyEvent . class , MyContext . class );
El generador de máquinas de estados toma parámetros que son tipo de máquina de estados (T), estado (S), evento (E) y contexto (C).
API fluida
Después de crear el generador de máquinas de estados, podemos usar una API fluida para definir el estado/transición/acción de la máquina de estados.
builder . externalTransition (). from ( MyState . A ). to ( MyState . B ). on ( MyEvent . GoToB );
Se construye una transición externa entre el estado 'A' y el estado 'B' y se activa al recibir el evento 'GoToB'.
builder . internalTransition ( TransitionPriority . HIGH ). within ( MyState . A ). on ( MyEvent . WithinA ). perform ( myAction );
Una transición interna con prioridad establecida en alta se construye dentro del estado 'A' en el evento 'WithinA' que realiza 'myAction'. La transición interna significa que una vez completada la transición, no se sale ni se ingresa a ningún estado. La prioridad de transición se utiliza para anular la transición original cuando se extiende la máquina de estado.
builder . externalTransition (). from ( MyState . C ). to ( MyState . D ). on ( MyEvent . GoToD ). when (
new Condition < MyContext >() {
@ Override
public boolean isSatisfied ( MyContext context ) {
return context != null && context . getValue ()> 80 ;
}
@ Override
public String name () {
return "MyCondition" ;
}
}). callMethod ( "myInternalTransitionCall" );
Se crea una transición condicional del estado 'C' al estado 'D' en el evento 'GoToD' cuando el contexto externo cumple la restricción de condición, luego llama al método de acción "myInternalTransitionCall". El usuario también puede utilizar MVEL (un potente lenguaje de expresión) para describir la condición de la siguiente manera.
builder . externalTransition (). from ( MyState . C ). to ( MyState . D ). on ( MyEvent . GoToD ). whenMvel (
"MyCondition:::(context!=null && context.getValue()>80)" ). callMethod ( "myInternalTransitionCall" );
Nota: Los caracteres ':::' se utilizan para separar el nombre de la condición y la expresión de la condición. El 'contexto' es el punto variable predefinido del objeto Contexto actual.
builder . onEntry ( MyState . A ). perform ( Lists . newArrayList ( action1 , action2 ))
En el código de muestra anterior se define una lista de acciones de entrada de estado.
Método de llamada a acción
El usuario puede definir acciones anónimas durante las transiciones definidas o la entrada/salida de estado. Sin embargo, el código de acción estará disperso en muchos lugares, lo que puede dificultar el mantenimiento del código. Además, otro usuario no puede anular las acciones. Por lo tanto, squirrel-foundation también admite la definición de la acción de llamada al método de la máquina de estado que viene junto con la propia clase de máquina de estado.
StateMachineBuilder <...> builder = StateMachineBuilderFactory . create (
MyStateMachine . class , MyState . class , MyEvent . class , MyContext . class );
builder . externalTransition (). from ( A ). to ( B ). on ( toB ). callMethod ( "fromAToB" );
// All transition action method stays with state machine class
public class MyStateMachine <...> extends AbstractStateMachine <...> {
protected void fromAToB ( MyState from , MyState to , MyEvent event , MyContext context ) {
// this method will be called during transition from "A" to "B" on event "toB"
// the action method parameters types and order should match
...
}
}
Además, squirrel-foundation también admite la definición de acciones de llamada a métodos mediante una convención sobre configuración . Básicamente, esto significa que si el método declarado en la máquina de estado cumple con la convención de nombres y parámetros, se agregará a la lista de acciones de transición y también se invocará en una fase determinada. p.ej
protected void transitFromAToBOnGoToB ( MyState from , MyState to , MyEvent event , MyContext context )
El método denominado transitFrom[SourceStateName]To[TargetStateName]On[EventName] y parametrizado como [MyState, MyState, MyEvent, MyContext] se agregará a la lista de acciones de transición "A-(GoToB)->B". Al pasar del estado 'A' al estado 'B' en el evento 'GoToB', se invocará este método.
protected void transitFromAnyToBOnGoToB ( MyState from , MyState to , MyEvent event , MyContext context )
transitFromAnyTo[TargetStateName]On[EventName] El método se invocará cuando se transite de cualquier estado al estado 'B' en el evento 'GoToB'.
protected void exitA ( MyState from , MyState to , MyEvent event , MyContext context )
exit[StateName] El método se invocará cuando salga del estado 'A'. Así como la entrada[StateName] , beforeExitAny / afterExitAny y beforeEntryAny / afterEntryAny .
Otros patrones de nombres admitidos:
transitFrom[fromStateName]To[toStateName]On[eventName]When[conditionName]
transitFrom[fromStateName]To[toStateName]On[eventName]
transitFromAnyTo[toStateName]On[eventName]
transitFrom[fromStateName]ToAnyOn[eventName]
transitFrom[fromStateName]To[toStateName]
on[eventName]
Las convenciones de métodos enumeradas anteriormente también proporcionaron funcionalidades similares a AOP , que proporcionaron una capacidad de extensión flexible incorporada para la máquina de estados ardilla con cualquier granularidad. Para obtener más información, consulte el caso de prueba " org.squirrelframework.foundation.fsm.ExtensionMethodCallTest ". Desde 0.3.1, hay otra forma de definir estos métodos de extensión similares a AOP que es a través de una API fluida (gracias por sugerencia de vittali), por ejemplo
// since 0.3.1
// the same effect as add method transitFromAnyToCOnToC in your state machine
builder . transit (). fromAny (). to ( "C" ). on ( "ToC" ). callMethod ( "fromAnyToC" );
// the same effect as add method transitFromBToAnyOnToC in your state machine
builder . transit (). from ( "B" ). toAny (). on ( "ToC" ). callMethod ( "fromBToAny" );
// the same effect as add method transitFromBToAny in your state machine
builder . transit (). from ( "B" ). toAny (). onAny (). callMethod ( "fromBToAny" );
O mediante anotación declarativa, por ejemplo
// since 0.3.1
@ Transitions ({
@ Transit ( from = "B" , to = "E" , on = "*" , callMethod = "fromBToEOnAny" ),
@ Transit ( from = "*" , to = "E" , on = "ToE" , callMethod = "fromAnyToEOnToE" )
})
Nota : Estos métodos de acción se adjuntarán a las transiciones coincidentes y ya existentes, pero no para crear transiciones nuevas. Desde 0.3.4, también se pueden definir múltiples transiciones una vez a la vez usando la siguiente API, por ejemplo
// transitions(A->B@A2B=>a2b, A->C@A2C=>a2c, A->D@A2D) will be defined at once
builder . transitions (). from ( State . _A ). toAmong ( State . B , State . C , State . D ).
onEach ( Event . A2B , Event . A2C , Event . A2D ). callMethod ( "a2b|a2c|_" );
// transitions(A->_A@A2ANY=>DecisionMaker, _A->A@ANY2A) will be defined at once
builder . localTransitions (). between ( State . A ). and ( State . _A ).
onMutual ( Event . A2ANY , Event . ANY2A ).
perform ( Lists . newArrayList ( new DecisionMaker ( "SomeLocalState" ), null ) );
Puede encontrar más información en org.squirrelframework.foundation.fsm.samples.DecisionStateSampleTest ;
Anotación declarativa
También se proporciona una forma declarativa para definir y ampliar la máquina de estados. Aquí hay un ejemplo.
@ States ({
@ State ( name = "A" , entryCallMethod = "entryStateA" , exitCallMethod = "exitStateA" ),
@ State ( name = "B" , entryCallMethod = "entryStateB" , exitCallMethod = "exitStateB" )
})
@ Transitions ({
@ Transit ( from = "A" , to = "B" , on = "GoToB" , callMethod = "stateAToStateBOnGotoB" ),
@ Transit ( from = "A" , to = "A" , on = "WithinA" , callMethod = "stateAToStateAOnWithinA" , type = TransitionType . INTERNAL )
})
interface MyStateMachine extends StateMachine < MyStateMachine , MyState , MyEvent , MyContext > {
void entryStateA ( MyState from , MyState to , MyEvent event , MyContext context );
void stateAToStateBOnGotoB ( MyState from , MyState to , MyEvent event , MyContext context )
void stateAToStateAOnWithinA ( MyState from , MyState to , MyEvent event , MyContext context )
void exitStateA ( MyState from , MyState to , MyEvent event , MyContext context );
...
}
La anotación se puede definir tanto en la clase de implementación de la máquina de estado como en cualquier interfaz en la que se implementará la máquina de estado. También se puede usar combinado con API fluida, lo que significa que la máquina de estado definida en API fluida también se puede ampliar con estas anotaciones. (Una cosa que quizás deba tener en cuenta es que el método definido dentro de la interfaz debe ser público, lo que significa que también la implementación de la acción de llamada al método será pública para la persona que llama).
Convertidores
Para declarar el estado y el evento dentro de @State y @Transit , el usuario debe implementar los convertidores correspondientes para su estado (S) y tipo de evento (E). La conversión debe implementar la interfaz Converter<T>, que convierte el estado/evento a/desde String.
public interface Converter < T > extends SquirrelComponent {
/**
* Convert object to string.
* @param obj converted object
* @return string description of object
*/
String convertToString ( T obj );
/**
* Convert string to object.
* @param name name of the object
* @return converted object
*/
T convertFromString ( String name );
}
Luego registre estos convertidores en ConverterProvider . p.ej
ConverterProvider . INSTANCE . register ( MyEvent . class , new MyEventConverter ());
ConverterProvider . INSTANCE . register ( MyState . class , new MyStateConverter ());
Nota: Si solo usa API fluida para definir la máquina de estado, no es necesario implementar los convertidores correspondientes. Y además, si la clase Evento o Estado es del tipo Cadena o Enumeración, no es necesario implementar o registrar un convertidor explícitamente en la mayoría de los casos.
Nueva instancia de máquina de estados
Después de que el usuario definiera el comportamiento de la máquina de estado, el usuario podría crear una nueva instancia de máquina de estado a través del constructor. Tenga en cuenta que una vez que se crea la instancia de la máquina de estados desde el constructor, el constructor ya no se puede utilizar para definir ningún elemento nuevo de la máquina de estados.
T newStateMachine ( S initialStateId , Object ... extraParams );
Para crear una nueva instancia de máquina de estados desde el generador de máquinas de estados, debe pasar los siguientes parámetros.
initialStateId
: cuando se inicia, el estado inicial de la máquina de estados.
extraParams
: parámetros adicionales necesarios para crear una nueva instancia de máquina de estado. Establezca en "nuevo objeto [0]" para que no se necesiten parámetros adicionales.
a. Si el usuario pasó parámetros adicionales al crear una nueva instancia de máquina de estado, asegúrese de que StateMachineBuilderFactory también haya definido el tipo de parámetros adicionales al crear el generador de máquina de estado. De lo contrario, se ignorará el parámetro adicional. b. Se pueden pasar parámetros adicionales a la instancia de la máquina de estado de dos maneras. Uno es a través del constructor de la máquina de estados, lo que significa que el usuario debe definir un constructor con el mismo tipo y orden de parámetros para la instancia de la máquina de estados. Otra forma es definir un método llamado postConstruct
y también con el mismo tipo y orden de parámetros.
Si no es necesario pasar parámetros adicionales a la máquina de estado, el usuario puede simplemente llamar T newStateMachine(S initialStateId)
para crear una nueva instancia de máquina de estado.
Nueva máquina de estados del fabricante de máquinas de estados. (En este caso, no es necesario pasar parámetros adicionales).
MyStateMachine stateMachine = builder . newStateMachine ( MyState . Initial );
Transiciones de activación
Una vez creada la máquina de estados, el usuario puede activar eventos junto con el contexto para desencadenar la transición dentro de la máquina de estados. p.ej
stateMachine . fire ( MyEvent . Prepare , new MyContext ( "Testing" ));
Máquina de estados sin tipo
Para simplificar el uso de la máquina de estados y evitar demasiados tipos genéricos (por ejemplo, StateMachine<T, S, E, C>) que pueden dificultar la lectura del código en algunos casos, pero aún así se mantiene una parte importante de la característica de seguridad de tipos en la acción de transición. ejecución, se implementó UntypedStateMachine para este propósito.
enum TestEvent {
toA , toB , toC , toD
}
@ Transitions ({
@ Transit ( from = "A" , to = "B" , on = "toB" , callMethod = "fromAToB" ),
@ Transit ( from = "B" , to = "C" , on = "toC" ),
@ Transit ( from = "C" , to = "D" , on = "toD" )
})
@ StateMachineParameters ( stateType = String . class , eventType = TestEvent . class , contextType = Integer . class )
class UntypedStateMachineSample extends AbstractUntypedStateMachine {
// No need to specify constructor anymore since 0.2.9
// protected UntypedStateMachineSample(ImmutableUntypedState initialState,
// Map<Object, ImmutableUntypedState> states) {
// super(initialState, states);
// }
protected void fromAToB ( String from , String to , TestEvent event , Integer context ) {
// transition action still type safe ...
}
protected void transitFromDToAOntoA ( String from , String to , TestEvent event , Integer context ) {
// transition action still type safe ...
}
}
UntypedStateMachineBuilder builder = StateMachineBuilderFactory . create (
UntypedStateMachineSample . class );
// state machine builder not type safe anymore
builder . externalTransition (). from ( "D" ). to ( "A" ). on ( TestEvent . toA );
UntypedStateMachine fsm = builder . newStateMachine ( "A" );
Para construir un UntypedStateMachine, el usuario debe crear primero un UntypedStateMachineBuilder a través de StateMachineBuilderFactory. StateMachineBuilderFactory toma solo un parámetro que es el tipo de clase de máquina de estado para crear UntypedStateMachineBuilder. @StateMachineParameters se utiliza para declarar tipos de parámetros genéricos de la máquina de estado. AbstractUntypedStateMachine es la clase base de cualquier máquina de estados sin tipo.
Máquina de estados insensible al contexto
A veces, a la transición de estado no le importa el contexto, lo que significa que la transición en su mayoría está determinada únicamente por el evento. En este caso, el usuario puede utilizar una máquina de estado insensible al contexto para simplificar los parámetros de llamada al método. Declarar una máquina de estado insensible al contexto es bastante simple. El usuario solo necesita agregar la anotación @ContextInsensible en la clase de implementación de la máquina de estado. Después de eso, el parámetro de contexto se puede ignorar en la lista de parámetros del método de transición. p.ej
@ ContextInsensitive
public class ATMStateMachine extends AbstractStateMachine < ATMStateMachine , ATMState , String , Void > {
// no need to add context parameter here anymore
public void transitFromIdleToLoadingOnConnected ( ATMState from , ATMState to , String event ) {
...
}
public void entryLoading ( ATMState from , ATMState to , String event ) {
...
}
}
Manejo de excepciones de transición
Cuando ocurre una excepción durante la transición de estado, la lista de acciones ejecutadas se cancelará y la máquina de estado entrará en estado de error, lo que significa que la instancia de la máquina de estado ya no puede procesar el evento. Si el usuario continúa activando un evento en la instancia de la máquina de estado, se descartará una IllegalStateException. Todas las excepciones que ocurrieron durante la fase de transición, incluida la ejecución de la acción y la invocación del oyente externo, se incluirán en TransitionException (excepción no marcada). Actualmente, la estrategia de manejo de excepciones predeterminada es simple y grosera: simplemente continúa descartando la excepción; consulte el método AbstractStateMachine.afterTransitionCausedException.
protected void afterTransitionCausedException (...) { throw e ; }
Si la máquina de estado se puede recuperar de esta excepción, el usuario puede extender el método afterTransitionCausedException y agregar la lógica de recuperación correspondiente en este método. NO olvide restablecer el estado de la máquina de estado a la normalidad al final. p.ej
@ Override
protected void afterTransitionCausedException ( Object fromState , Object toState , Object event , Object context ) {
Throwable targeException = getLastException (). getTargetException ();
// recover from IllegalArgumentException thrown out from state 'A' to 'B' caused by event 'ToB'
if ( targeException instanceof IllegalArgumentException &&
fromState . equals ( "A" ) && toState . equals ( "B" ) && event . equals ( "ToB" )) {
// do some error clean up job here
// ...
// after recovered from this exception, reset the state machine status back to normal
setStatus ( StateMachineStatus . IDLE );
} else if (...) {
// recover from other exception ...
} else {
super . afterTransitionCausedException ( fromState , toState , event , context );
}
}
Definir estado jerárquico
Un estado jerárquico puede contener un estado anidado. Los estados hijos pueden tener hijos anidados y el anidamiento puede realizarse a cualquier profundidad. Cuando un estado jerárquico está activo, uno y sólo uno de sus estados secundarios está activo. El estado jerárquico se puede definir mediante API o anotaciones.
void defineSequentialStatesOn ( S parentStateId , S ... childStateIds );
builder.defineSequentialStatesOn(State.A, State.BinA, StateCinA) define dos estados secundarios "BinA" y "CinA" bajo el estado principal "A", el primer estado secundario definido también será el estado inicial del estado jerárquico "A" . El mismo estado jerárquico también se puede definir mediante anotaciones, por ejemplo
@ States ({
@ State ( name = "A" , entryMethodCall = "entryA" , exitMethodCall = "exitA" ),
@ State ( parent = "A" , name = "BinA" , entryMethodCall = "entryBinA" , exitMethodCall = "exitBinA" , initialState = true ),
@ State ( parent = "A" , name = "CinA" , entryMethodCall = "entryCinA" , exitMethodCall = "exitCinA" )
})
Definir estado paralelo
El estado paralelo encapsula un conjunto de estados secundarios que están activos simultáneamente cuando el elemento principal está activo. El estado paralelo se puede definir mediante API o anotaciones. p.ej
// defines two region states "RegionState1" and "RegionState2" under parent parallel state "Root"
builder . defineParallelStatesOn ( MyState . Root , MyState . RegionState1 , MyState . RegionState2 );
builder . defineSequentialStatesOn ( MyState . RegionState1 , MyState . State11 , MyState . State12 );
builder . externalTransition (). from ( MyState . State11 ). to ( MyState . State12 ). on ( MyEvent . Event1 );
builder . defineSequentialStatesOn ( MyState . RegionState2 , MyState . State21 , MyState . State22 );
builder . externalTransition (). from ( MyState . State21 ). to ( MyState . State22 ). on ( MyEvent . Event2 );
o
@ States ({
@ State ( name = "Root" , entryCallMethod = "enterRoot" , exitCallMethod = "exitRoot" , compositeType = StateCompositeType . PARALLEL ),
@ State ( parent = "Root" , name = "RegionState1" , entryCallMethod = "enterRegionState1" , exitCallMethod = "exitRegionState1" ),
@ State ( parent = "Root" , name = "RegionState2" , entryCallMethod = "enterRegionState2" , exitCallMethod = "exitRegionState2" )
})
Para obtener los subestados actuales del estado paralelo
stateMachine . getSubStatesOn ( MyState . Root ); // return list of current sub states of parallel state
Cuando todos los estados paralelos alcancen el estado final, se activará un evento de contexto Finalizar .
Definir evento de contexto
Evento de contexto significa que el evento definido por el usuario tiene un contexto predefinido en la máquina de estado. squirrel-foundation definió tres tipos de eventos de contexto para diferentes casos de uso. Evento de inicio/terminación : el evento declarado como evento de inicio/terminación se utilizará cuando la máquina de estado se inicie/termine. Por lo tanto, el usuario puede diferenciar el desencadenante de la acción invocada, por ejemplo, cuando la máquina de estado se inicia y entra en su estado inicial, el usuario puede diferenciar que la acción de entrada de estado fue invocada por el evento de inicio. Evento de finalización : cuando todos los estados paralelos alcancen el estado final, el evento de finalización se activará automáticamente. El usuario puede definir la siguiente transición según el evento de finalización. Para definir el evento de contexto, el usuario tiene una API bidireccional, de anotación o de creación.
@ ContextEvent ( finishEvent = "Finish" )
static class ParallelStateMachine extends AbstractStateMachine <...> {
}
o
StateMachineBuilder <...> builder = StateMachineBuilderFactory . create (...);
...
builder . defineFinishEvent ( HEvent . Start );
builder . defineTerminateEvent ( HEvent . Terminate );
builder . defineStartEvent ( HEvent . Finish );
Uso de estados históricos para guardar y restaurar el estado actual
El pseudoestado histórico permite que una máquina de estados recuerde su configuración de estado. Una transición que tome el estado histórico como objetivo devolverá la máquina de estado a esta configuración registrada. Si el 'tipo' de un historial es "superficial", el procesador de la máquina de estado debe registrar los hijos activos directos de su padre antes de realizar cualquier transición que salga del padre. Si el 'tipo' de un historial es "profundo", el procesador de la máquina de estado debe registrar todos los descendientes activos del padre antes de realizar cualquier transición que salga del padre. Se admiten API y anotaciones para definir el tipo de estado del historial. p.ej
// defined history type of state "A" as "deep"
builder . defineSequentialStatesOn ( MyState . A , HistoryType . DEEP , MyState . A1 , MyState . A2 )
o
@ State ( parent = "A" , name = "A1" , entryCallMethod = "enterA1" , exitCallMethod = "exitA1" , historyType = HistoryType . DEEP )
Nota: Antes de 0.3.7, el usuario necesitaba definir "HistoryType.DEEP" para cada nivel de estado histórico, lo cual no es muy conveniente (gracias a Voskuijlen por proporcionar la solución al problema 33). Ahora el usuario solo define "HistoryType.DEEP" en el nivel superior del estado histórico, y se recordará toda la información histórica del estado secundario.
Tipos de transición
Según la especificación UML, una transición puede ser de uno de estos tres tipos:
- Transición interna Implica que la Transición, si se desencadena, ocurre sin salir o entrar al Estado de origen (es decir, no causa un cambio de estado). Esto significa que no se invocará la condición de entrada o salida del Estado de origen. Se puede realizar una transición interna incluso si StateMachine está en una o más regiones anidadas dentro del estado asociado.
- Transición local Implica que la transición, si se activa, no saldrá del estado compuesto (fuente), pero saldrá y volverá a entrar en cualquier estado dentro del estado compuesto que esté en la configuración de estado actual.
- Transición externa Implica que la transición, si se activa, saldrá del estado compuesto (fuente).
squirrel-foundation admite API y anotaciones para declarar todo tipo de transiciones, por ejemplo
builder . externalTransition (). from ( MyState . A ). to ( MyState . B ). on ( MyEvent . A2B );
builder . internalTransition (). within ( MyState . A ). on ( MyEvent . innerA );
builder . localTransition (). from ( MyState . A ). to ( MyState . CinA ). on ( MyEvent . intoC )
o
@ Transitions ({
@ Transition ( from = "A" , to = "B" , on = "A2B" ), //default value of transition type is EXTERNAL
@ Transition ( from = "A" , on = "innerA" , type = TransitionType . INTERNAL ),
@ Transition ( from = "A" , to = "CinA" , on = "intoC" , type = TransitionType . LOCAL ),
})
Envío de eventos de polimorfismo
Durante el ciclo de vida de la máquina de estados, se activarán varios eventos, por ejemplo
State Machine Lifecycle Events
|--StateMachineEvent /* Base event of all state machine event */
|--StartEvent /* Fired when state machine started */
|--TerminateEvent /* Fired when state machine terminated */
|--TransitionEvent /* Base event of all transition event */
|--TransitionBeginEvent /* Fired when transition began */
|--TransitionCompleteEvent /* Fired when transition completed */
|--TransitionExceptionEvent /* Fired when transition threw exception */
|--TransitionDeclinedEvent /* Fired when transition declined */
|--TransitionEndEvent /* Fired when transition end no matter declined or complete */
El usuario puede agregar un oyente para escuchar StateMachineEvent, lo que significa que todos los eventos activados durante el ciclo de vida de la máquina de estado serán capturados por este oyente, por ejemplo,
stateMachine . addStateMachineListener ( new StateMachineListener <...>() {
@ Override
public void stateMachineEvent ( StateMachineEvent <...> event ) {
// ...
}
});
Y el usuario también puede agregar un oyente para escuchar TransitionEvent a través de StateMachine.addTransitionListener, lo que significa que todos los eventos activados durante cada transición de estado, incluidos TransitionBeginEvent, TransitionCompleteEvent y TransitionEndEvent, serán capturados por este oyente. O el usuario puede agregar un oyente específico, por ejemplo, TransitionDeclinedListener para escuchar TransitionDeclinedEvent cuando se rechazó la solicitud de transición.
Oyente de eventos declarativo
Agregar el detector de eventos anterior a la máquina de estado a veces resulta molesto para el usuario, y demasiados tipos genéricos también hacen que el código sea feo de leer. Para simplificar el uso de la máquina de estado, más importante para proporcionar una integración no invasiva, squirrel-foundation proporciona una forma declarativa de agregar un detector de eventos mediante la siguiente anotación, por ejemplo
static class ExternalModule {
@ OnTransitionEnd
@ ListenerOrder ( 10 ) // Since 0.3.1 ListenerOrder can be used to insure listener invoked orderly
public void transitionEnd () {
// method annotated with TransitionEnd will be invoked when transition end...
// the method must be public and return nothing
}
@ OnTransitionBegin
public void transitionBegin ( TestEvent event ) {
// method annotated with TransitionBegin will be invoked when transition begin...
}
// 'event'(E), 'from'(S), 'to'(S), 'context'(C) and 'stateMachine'(T) can be used in MVEL scripts
@ OnTransitionBegin ( when = "event.name().equals( " toB " )" )
public void transitionBeginConditional () {
// method will be invoked when transition begin while transition caused by event "toB"
}
@ OnTransitionComplete
public void transitionComplete ( String from , String to , TestEvent event , Integer context ) {
// method annotated with TransitionComplete will be invoked when transition complete...
}
@ OnTransitionDecline
public void transitionDeclined ( String from , TestEvent event , Integer context ) {
// method annotated with TransitionDecline will be invoked when transition declined...
}
@ OnBeforeActionExecuted
public void onBeforeActionExecuted ( Object sourceState , Object targetState ,
Object event , Object context , int [] mOfN , Action <?, ?, ?,?> action ) {
// method annotated with OnAfterActionExecuted will be invoked before action invoked
}
@ OnAfterActionExecuted
public void onAfterActionExecuted ( Object sourceState , Object targetState ,
Object event , Object context , int [] mOfN , Action <?, ?, ?,?> action ) {
// method annotated with OnAfterActionExecuted will be invoked after action invoked
}
@ OnActionExecException
public void onActionExecException ( Action <?, ?, ?,?> action , TransitionException e ) {
// method annotated with OnActionExecException will be invoked when action thrown exception
}
}
ExternalModule externalModule = new ExternalModule ();
fsm . addDeclarativeListener ( externalModule );
...
fsm . removeDeclarativeListener ( externalModule );
Al hacer este código de módulo externo, no es necesario implementar ninguna interfaz de escucha de máquina de estado. Solo agregue algunas anotaciones sobre los métodos que se engancharán durante la fase de transición. Los parámetros del método también son seguros y se inferirán automáticamente para que coincidan con el evento correspondiente. Este es un buen enfoque para la separación de preocupaciones . El usuario puede encontrar ejemplos de uso en org.squirrelframework.foundation.fsm.StateMachineLogger .
Métodos de extensión de transición
Cada evento de transición también tiene un método de extensión correspondiente en la clase AbstractStateMachine que puede extenderse en la clase de implementación de la máquina de estado del cliente.
protected void afterTransitionCausedException ( Exception e , S fromState , S toState , E event , C context ) {
}
protected void beforeTransitionBegin ( S fromState , E event , C context ) {
}
protected void afterTransitionCompleted ( S fromState , S toState , E event , C context ) {
}
protected void afterTransitionEnd ( S fromState , S toState , E event , C context ) {
}
protected void afterTransitionDeclined ( S fromState , E event , C context ) {
}
protected void beforeActionInvoked ( S fromState , S toState , E event , C context ) {
}
Normalmente, el usuario puede conectar la lógica de procesamiento de su negocio en estos métodos de extensión durante cada transición de estado, mientras que los distintos detectores de eventos sirven como límite del sistema de control basado en la máquina de estados, que puede interactuar con módulos externos (por ejemplo, UI, auditoría, ESB, etc.). ). Por ejemplo, el usuario puede extender el método afterTransitionCausedException para la limpieza del entorno cuando se produjo una excepción durante la transición y también notificar al módulo de interfaz de usuario para que muestre un mensaje de error a través de TransitionExceptionEvent.
Acción ponderada
El usuario puede definir el peso de la acción para ajustar el orden de ejecución de la acción. Las acciones durante la entrada/salida de estado y la transición de estado se ordenan en orden ascendente según su valor de peso. El peso de la acción es 0 de forma predeterminada. El usuario tiene dos formas de establecer el peso de la acción.
Una es agregar el número de peso al nombre del método y separarlo por ':'.
// define state entry action 'goEntryD' weight -150
@ State ( name = "D" , entryCallMethod = "goEntryD:-150" )
// define transition action 'goAToC1' weight +150
@ Transit ( from = "A" , to = "C" , on = "ToC" , callMethod = "goAToC1:+150" )
Otra forma es anular el método de peso de la clase Acción, por ejemplo
Action <...> newAction = new Action <...>() {
...
@ Override
public int weight () {
return 100 ;
}
}
squirrel-foundation también admite una forma convencional de declarar el peso de la acción. El peso de la acción de llamada al método cuyo nombre comienza con ' antes ' se establecerá en 100, por lo que el nombre que comienza con ' después ' se establecerá en -100. Generalmente significa que el nombre del método de acción que comienza con "antes" se invocará al principio, mientras que el nombre del método de acción que comienza con "después" se invocará al final. "método1:ignorar" significa que no se invocará el método1.
Para obtener más información, consulte el caso de prueba ' org.squirrelframework.foundation.fsm.WeightedActionTest ';
Ejecución asincronizada
La anotación @AsyncExecute se puede utilizar en la acción de llamada al método y en el detector de eventos declarativo para indicar que esta acción o detector de eventos se ejecutará de forma asincrónica, por ejemplo, Defina el método de acción invocado de forma asincrónica:
@ ContextInsensitive
@ StateMachineParameters ( stateType = String . class , eventType = String . class , contextType = Void . class )
public class ConcurrentSimpleStateMachine extends AbstractUntypedStateMachine {
// No need to specify constructor anymore since 0.2.9
// protected ConcurrentSimpleStateMachine(ImmutableUntypedState initialState,
// Map<Object, ImmutableUntypedState> states) {
// super(initialState, states);
// }
@ AsyncExecute
protected void fromAToB ( String from , String to , String event ) {
// this action method will be invoked asynchronously
}
}
Definir evento enviado asincrónicamente:
public class DeclarativeListener {
@ OnTransitionBegin
@ AsyncExecute
public void onTransitionBegin (...) {
// transition begin event will be dispatched asynchronously to this listener method
}
}
La tarea de ejecución asincrónica se enviará a un ExecutorService . El usuario puede registrar su instancia de implementación de ExecutorService a través de SquirrelSingletonProvider , por ejemplo
ExecutorService executorService = Executors . newFixedThreadPool ( 1 );
SquirrelSingletonProvider . getInstance (). register ( ExecutorService . class , executorService );
Si no se registró ninguna instancia de ExecutorService, SquirrelConfiguration proporcionará una predeterminada.
Postprocesador de máquina de estados
El usuario puede registrar el posprocesador para un tipo específico de máquina de estado con el fin de agregar lógica de posprocesamiento después de crear una instancia de la máquina de estado, por ejemplo.
// 1 User defined a state machine interface
interface MyStateMachine extends StateMachine < MyStateMachine , MyState , MyEvent , MyContext > {
. . .
}
// 2 Both MyStateMachineImpl and MyStateMachineImplEx are implemented MyStateMachine
class MyStateMachineImpl implements MyStateMachine {
. . .
}
class MyStateMachineImplEx implements MyStateMachine {
. . .
}
// 3 User define a state machine post processor
MyStateMachinePostProcessor implements SquirrelPostProcessor < MyStateMachine > {
void postProcess ( MyStateMachine component ) {
. . .
}
}
// 4 User register state machine post process
SquirrelPostProcessorProvider . getInstance (). register ( MyStateMachine . class , MyStateMachinePostProcessor . class );
En este caso, cuando el usuario creó la instancia MyStateMachineImpl y MyStateMachineImplEx, se llamará al posprocesador registrado MyStateMachinePostProcessor para realizar algún trabajo.
Exportación de máquinas de estado
SCXMLVisitor se puede utilizar para exportar la definición de la máquina de estado en el documento [SCXML] 2.
SCXMLVisitor visitor = SquirrelProvider . getInstance (). newInstance ( SCXMLVisitor . class );
stateMachine . accept ( visitor );
visitor . convertSCXMLFile ( "MyStateMachine" , true );
Por cierto, el usuario también puede llamar a StateMachine.exportXMLDefinition(true) para exportar una definición XML embellecida. DotVisitor se puede utilizar para generar un diagrama de estado que se puede ver con [GraphViz] 3.
DotVisitor visitor = SquirrelProvider . getInstance (). newInstance ( DotVisitor . class );
stateMachine . accept ( visitor );
visitor . convertDotFile ( "SnakeStateMachine" );
Importación de máquinas de estado
UntypedStateMachineImporter se puede utilizar para importar una definición similar a SCXML de la máquina de estado que fue exportada por SCXMLVisitor o una definición de escritura a mano. UntypedStateMachineImporter construirá un UntypedStateMachineBuilder de acuerdo con la definición que luego se puede usar para crear instancias de máquina de estado.
UntypedStateMachineBuilder builder = new UntypedStateMachineImporter (). importDefinition ( scxmlDef );
ATMStateMachine stateMachine = builder . newAnyStateMachine ( ATMState . Idle );
Nota: UntypedStateMachineImporter proporcionó un estilo XML para definir la máquina de estados al igual que la API del generador de máquinas de estados o las anotaciones declarativas. La definición similar a SCXML no es igual al SCXML estándar.
Guardar/cargar datos de la máquina de estado
El usuario puede guardar datos de la máquina de estado cuando la máquina de estado está en estado inactivo.
StateMachineData . Reader < MyStateMachine , MyState , MyEvent , MyContext >
savedData = stateMachine . dumpSavedData ();
Y también el usuario puede cargar los datos guardados anteriores en otra máquina de estado cuyo estado finalice o simplemente se inicialice.
newStateMachineInstance . loadSavedData ( savedData );
NOTA : Los datos de la máquina de estado se pueden serializar o deserializar desde una cadena codificada en Base64 con la ayuda de la clase ObjectSerializableSupport .
Configuración de la máquina de estados
Al crear una nueva instancia de máquina de estado, el usuario puede configurar su comportamiento a través de StateMachineConfiguration , por ejemplo
UntypedStateMachine fsm = builder . newUntypedStateMachine ( "a" ,
StateMachineConfiguration . create (). enableAutoStart ( false )
. setIdProvider ( IdProvider . UUIDProvider . getInstance ()),
new Object [ 0 ]); // since 0.3.0
fsm . fire ( TestEvent . toA );
El código de muestra anterior se utiliza para crear una instancia de máquina de estado con UUID como identificador y deshabilitar la función de inicio automático. StateMachineConfigure también se puede configurar en el generador de máquinas de estado, lo que significa que todas las instancias de máquinas de estados creadas por builder.newStateMachine(S initialStateId)
o builder.newStateMachine(S initialStateId, Object... extraParams)
utilizarán esta configuración.
Diagnóstico de la máquina de estado
StateMachineLogger se utiliza para observar el estado interno de la máquina de estado, como el rendimiento de la ejecución, la secuencia de llamada de acción, el progreso de la transición, etc., por ejemplo.
StateMachine <?,?,?,?> stateMachine = builder . newStateMachine ( HState . A );
StateMachineLogger fsmLogger = new StateMachineLogger ( stateMachine );
fsmLogger . startLogging ();
...
stateMachine . fire ( HEvent . B2A , 1 );
...
fsmLogger . terminateLogging ();
-------------------------------------------------------------------------------------------
Console Log :
HierachicalStateMachine : Transition from "B2a" on "B2A" with context "1" begin .
Before execute method call action "leftB2a" ( 1 of 6 ).
Before execute method call action "exitB2" ( 2 of 6 ).
...
Before execute method call action "entryA1" ( 6 of 6 ).
HierachicalStateMachine : Transition from "B2a" to "A1" on "B2A" complete which took 2 ms .
...
Dado que el registrador de máquina de estado v0.3.0 se puede usar de manera más sencilla simplemente configurando StateMachineConfiguration para habilitar el modo de depuración, por ejemplo
StateMachine<?,?,?,?> stateMachine = builder.newStateMachine(HState.A,
StateMachineConfiguration.create().enableDebugMode(true),
new Object[0]);
StateMachinePerformanceMonitor se puede utilizar para monitorear la información de rendimiento de ejecución de la máquina de estado, incluido el recuento total de tiempos de transición, el tiempo promedio consumido por la transición, etc., por ejemplo.
final UntypedStateMachine fsm = builder . newStateMachine ( "D" );
final StateMachinePerformanceMonitor performanceMonitor =
new StateMachinePerformanceMonitor ( "Sample State Machine Performance Info" );
fsm . addDeclarativeListener ( performanceMonitor );
for ( int i = 0 ; i < 10000 ; i ++) {
fsm . fire ( FSMEvent . ToA , 10 );
fsm . fire ( FSMEvent . ToB , 10 );
fsm . fire ( FSMEvent . ToC , 10 );
fsm . fire ( FSMEvent . ToD , 10 );
}
fsm . removeDeclarativeListener ( performanceMonitor );
System . out . println ( performanceMonitor . getPerfModel ());
-------------------------------------------------------------------------------------------
Console Log :
========================== Sample State Machine Performance Info ==========================
Total Transition Invoked : 40000
Total Transition Failed : 0
Total Transition Declained : 0
Average Transition Comsumed : 0.0004 ms
Transition Key Invoked Times Average Time Max Time Min Time
C --{ ToD , 10 }-> D 10000 0.0007 ms 5 ms 0 ms
B --{ ToC , 10 }-> C 10000 0.0001 ms 1 ms 0 ms
D --{ ToA , 10 }-> A 10000 0.0009 ms 7 ms 0 ms
A --{ ToB , 10 }-> B 10000 0.0000 ms 1 ms 0 ms
Total Action Invoked : 40000
Total Action Failed : 0
Average Action Execution Comsumed : 0.0000 ms
Action Key Invoked Times Average Time Max Time Min Time
instan ... Test$1 40000 0.0000 ms 1 ms 0 ms
========================== Sample State Machine Performance Info ==========================
Agregar @LogExecTime en el método de acción cerrará la sesión del tiempo de ejecución del método. Y también agregue la clase @LogExecTime en la máquina de estado para cerrar la sesión de todo el tiempo de ejecución del método de acción. Por ejemplo, se cerrará la sesión del tiempo de ejecución del método transitFromAToBOnGoToB .
@ LogExecTime
protected void transitFromAToBOnGoToB ( MyState from , MyState to , MyEvent event , MyContext context )
Estado cronometrado
Un estado cronometrado es un estado que puede retrasar o desencadenar periódicamente un evento específico después de ingresar al estado. La tarea cronometrada se enviará a ScheduledExecutorService . El usuario puede registrar su instancia de implementación ScheduledExecutorService a través de SquirrelSingletonProvider , por ejemplo
ScheduledExecutorService scheduler = Executors . newScheduledThreadPool ( 1 );
SquirrelSingletonProvider . getInstance (). register ( ScheduledExecutorService . class , scheduler );
Si no se registró ninguna instancia de ScheduledExecutorService, SquirrelConfiguration proporcionará una predeterminada. Después de eso, el constructor de la máquina de estados puede definir un estado cronometrado, por ejemplo
// after 50ms delay fire event "FIRST" every 100ms with null context
builder . defineTimedState ( "A" , 50 , 100 , "FIRST" , null );
builder . internalTransition (). within ( "A" ). on ( "FIRST" );
NOTA : Asegúrese de que se defina el estado cronometrado antes de describir sus transiciones o acciones de entrada/salida. timeInterval menor o igual a 0 se considerará ejecutado solo una vez después de initialDelay .
Estado vinculado (llamado estado de submáquina)
Un estado vinculado especifica la inserción de la especificación de una máquina de estados de submáquina. La máquina de estados que contiene el estado vinculado se llama máquina de estados contenedora. La misma máquina de estados puede ser una submáquina más de una vez en el contexto de una única máquina de estados que la contiene.
Un estado vinculado es semánticamente equivalente a un estado compuesto. Las regiones de la máquina de estados de la submáquina son las regiones del estado compuesto. Las acciones de entrada, salida y comportamiento y transiciones internas se definen como parte del estado. El estado de la submáquina es un mecanismo de descomposición que permite factorizar comportamientos comunes y su reutilización. El estado vinculado se puede definir siguiendo el código de muestra.
builderOfTestStateMachine . definedLinkedState ( LState . A , builderOfLinkedStateMachine , LState . A1 );
Soporte JMX
Desde 0.3.3, el usuario puede monitorear remotamente la instancia de la máquina de estado (por ejemplo, estado actual, nombre) y modificar configuraciones (por ejemplo, alternar registros/alternar monitor de rendimiento/evento de incendio remoto) en tiempo de ejecución. Toda la información de las instancias de la máquina de estado estará en el dominio "org.squirrelframework". El siguiente código de muestra muestra cómo habilitar la compatibilidad con JMX.
UntypedStateMachineBuilder builder = StateMachineBuilderFactory . create (...);
builder . setStateMachineConfiguration ( StateMachineConfiguration . create (). enableRemoteMonitor ( true ));
NOTA : La compatibilidad con la función JMX está obsoleta desde 0.3.9-SNAPSHOT.
Ver archivo de EJEMPLOS.
Consulte el archivo NOTAS DE LA VERSIÓN.
Para conocer las últimas actualizaciones, siga mi Twitter @ hhe11 o +HeHenry.
Para discusiones o preguntas, únase al grupo de máquinas de estados de ardilla
Para cualquier problema o requisito, envíe un problema
Si utiliza el código de Squirrel State Machine en su aplicación, le agradecería que se lo informara al autor (correo electrónico: [email protected]) de esta manera :
Asunto: Texto de notificación de uso de Squirrel State Machine: Utilizo Squirrel State Machine <lib_version> en <project_name> - http://link_to_project. Yo [permito | no permita] mencionar mi proyecto en la sección "Quién usa Squirrel State Machine" en GitHub.