Tout comme l'écureuil, un petit animal agile , intelligent , alerte et mignon , squirrel-foundation vise à fournir une implémentation de machine d'état Java légère , hautement flexible et extensible , diagnostiquable , facile à utiliser et de type sûr pour une utilisation en entreprise.
Voici le schéma de la machine à états qui décrit le changement d'état d'un ATM :
L'exemple de code se trouve dans le package "org.squirrelframework.foundation.fsm.atm" .
squirrel-foundation a été déployé sur le référentiel central maven, il vous suffit donc d'ajouter la dépendance suivante au pom.xml.
Dernière version publiée :
<dependency>
<groupId>org.squirrelframework</groupId>
<artifactId>squirrel-foundation</artifactId>
<version>0.3.10</version>
</dependency>
Dernière version d'instantané :
<dependency>
<groupId>org.squirrelframework</groupId>
<artifactId>squirrel-foundation</artifactId>
<version>0.3.11-SNAPSHOT</version>
</dependency>
Pour essayer rapidement les fonctions de la machine à états d'écureuil, veuillez créer un projet maven et inclure correctement la dépendance à la fondation d'écureuil. Ensuite, exécutez simplement l’exemple de code suivant.
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 ());
}
}
À l'heure actuelle, vous avez peut-être de nombreuses questions sur l'exemple de code, soyez patient. Le guide d'utilisation suivant répondra à la plupart de vos questions. Mais avant d’entrer dans les détails, vous devez avoir une compréhension de base des concepts de machines à états. Ces matériaux sont utiles pour comprendre les concepts de machines à états. [diagrammes de machine à états] [machine à états qt]
squirrel-foundation prend en charge à la fois l'API fluide et la manière déclarative pour déclarer une machine à états, et permet également à l'utilisateur de définir les méthodes d'action de manière simple.
L'interface StateMachine prend quatre paramètres de type génériques.
Constructeur de machines d'état
Afin de créer une machine à états, l'utilisateur doit d'abord créer un générateur de machine à états. Par exemple:
StateMachineBuilder < MyStateMachine , MyState , MyEvent , MyContext > builder =
StateMachineBuilderFactory . create ( MyStateMachine . class , MyState . class , MyEvent . class , MyContext . class );
Le constructeur de machine à états prend pour paramètres le type de machine à états (T), l'état (S), l'événement (E) et le contexte (C).
API fluide
Une fois le constructeur de machine à états créé, nous pouvons utiliser une API fluide pour définir l'état/la transition/l'action de la machine à état.
builder . externalTransition (). from ( MyState . A ). to ( MyState . B ). on ( MyEvent . GoToB );
Une transition externe est construite entre l'état 'A' et l'état 'B' et déclenchée lors de la réception de l'événement 'GoToB'.
builder . internalTransition ( TransitionPriority . HIGH ). within ( MyState . A ). on ( MyEvent . WithinA ). perform ( myAction );
Une transition interne avec une priorité élevée est construite à l'intérieur de l'état « A » lors de l'événement « WithinA » qui exécute « myAction ». La transition interne signifie qu'une fois la transition terminée, aucun état n'est sorti ou entré. La priorité de transition est utilisée pour remplacer la transition d'origine lorsque la machine à états est étendue.
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" );
Une transition conditionnelle est construite de l'état « C » à l'état « D » lors de l'événement « GoToD » lorsque le contexte externe satisfait à la restriction de condition, puis appelle la méthode d'action « myInternalTransitionCall ». L'utilisateur peut également utiliser MVEL (un langage d'expression puissant) pour décrire la condition de la manière suivante.
builder . externalTransition (). from ( MyState . C ). to ( MyState . D ). on ( MyEvent . GoToD ). whenMvel (
"MyCondition:::(context!=null && context.getValue()>80)" ). callMethod ( "myInternalTransitionCall" );
Remarque : Les caractères ':::' sont utilisés pour séparer le nom de la condition et l'expression de la condition. Le « contexte » est la variable prédéfinie pointant vers l'objet Contexte actuel.
builder . onEntry ( MyState . A ). perform ( Lists . newArrayList ( action1 , action2 ))
Une liste d’actions d’entrée d’état est définie dans l’exemple de code ci-dessus.
Action d’appel de méthode
L'utilisateur peut définir des actions anonymes lors de la définition de transitions ou d'une entrée/sortie d'état. Cependant, le code d'action sera dispersé à plusieurs endroits, ce qui peut rendre le code difficile à maintenir. De plus, les autres utilisateurs ne peuvent pas annuler les actions. Ainsi, Squirrel-foundation prend également en charge la définition de l'action d'appel de méthode de machine à états qui accompagne la classe de machine à états elle-même.
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
...
}
}
De plus, squirrel-foundation prend également en charge la définition d'actions d'appel de méthode de manière Convention Over Configuration . Fondamentalement, cela signifie que si la méthode déclarée dans la machine à états satisfait à la convention de dénomination et de paramètres, elle sera ajoutée à la liste d'actions de transition et sera également invoquée à certaines phases. par exemple
protected void transitFromAToBOnGoToB ( MyState from , MyState to , MyEvent event , MyContext context )
La méthode nommée transitFrom[SourceStateName]To[TargetStateName]On[EventName] et paramétrée comme [MyState, MyState, MyEvent, MyContext] sera ajoutée à la liste d'actions de transition "A-(GoToB)->B". Lors du passage de l'état « A » à l'état « B » lors de l'événement « GoToB », cette méthode sera invoquée.
protected void transitFromAnyToBOnGoToB ( MyState from , MyState to , MyEvent event , MyContext context )
transitFromAnyTo[TargetStateName]On[EventName] La méthode sera invoquée lors du transit de n'importe quel état vers l'état « B » lors de l'événement « GoToB ».
protected void exitA ( MyState from , MyState to , MyEvent event , MyContext context )
exit[StateName] La méthode sera invoquée lors de la sortie de l'état 'A'. Ainsi que l' entrée[StateName] , beforeExitAny / afterExitAny et beforeEntryAny / afterEntryAny .
Autres modèles de dénomination pris en charge :
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]
Les conventions de méthodes répertoriées ci-dessus fournissaient également des fonctionnalités de type AOP , qui offraient une capacité d'extension flexible intégrée pour la machine à états d'écureuil à n'importe quelle granularité. Pour plus d'informations, veuillez vous référer au scénario de test « org.squirrelframework.foundation.fsm.ExtensionMethodCallTest ». Depuis la version 0.3.1, il existe une autre façon de définir ces méthodes d'extension de type AOP, via une API fluide (merci la suggestion de Vittali), par exemple
// 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" );
Ou via une annotation déclarative, par exemple
// since 0.3.1
@ Transitions ({
@ Transit ( from = "B" , to = "E" , on = "*" , callMethod = "fromBToEOnAny" ),
@ Transit ( from = "*" , to = "E" , on = "ToE" , callMethod = "fromAnyToEOnToE" )
})
Remarque : Ces méthodes d'action seront attachées aux transitions correspondantes et déjà existantes mais pas pour créer de nouvelles transitions. Depuis la version 0.3.4, plusieurs transitions peuvent également être définies une fois à la fois en utilisant l'API suivante, par exemple
// 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 ) );
Plus d'informations peuvent être trouvées dans org.squirrelframework.foundation.fsm.samples.DecisionStateSampleTest ;
Annotation déclarative
Une méthode déclarative est également fournie pour définir et également étendre la machine à états. Voici un exemple.
@ 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 );
...
}
L'annotation peut être définie à la fois dans la classe d'implémentation de la machine à états ou dans n'importe quelle interface que la machine à états sera implémentée. Il peut également être utilisé en combinaison avec l'API fluide, ce qui signifie que la machine d'état définie dans l'API fluide peut également être étendue par ces annotations. (Une chose que vous devrez peut-être remarquer, la méthode définie dans l'interface doit être publique, ce qui signifie également que l'implémentation de l'action d'appel de méthode sera publique pour l'appelant.)
Convertisseurs
Afin de déclarer un état et un événement dans @State et @Transit , l'utilisateur doit implémenter les convertisseurs correspondants pour son type d'état (S) et d'événement (E). La conversion doit implémenter l'interface Converter<T>, qui convertit l'état/événement en/depuis 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 );
}
Enregistrez ensuite ces convertisseurs sur ConverterProvider . par exemple
ConverterProvider . INSTANCE . register ( MyEvent . class , new MyEventConverter ());
ConverterProvider . INSTANCE . register ( MyState . class , new MyStateConverter ());
Remarque : Si vous utilisez uniquement une API fluide pour définir la machine à états, il n'est pas nécessaire d'implémenter les convertisseurs correspondants. De plus, si la classe Event ou State est de type String ou Enumeration, vous n'avez pas besoin d'implémenter ou d'enregistrer explicitement un convertisseur dans la plupart des cas.
Nouvelle instance de machine à états
Après avoir défini le comportement de la machine à états par l'utilisateur, l'utilisateur peut créer une nouvelle instance de machine à états via le générateur. Notez qu'une fois l'instance de machine d'état créée à partir du constructeur, le constructeur ne peut plus être utilisé pour définir un nouvel élément de machine d'état.
T newStateMachine ( S initialStateId , Object ... extraParams );
Pour créer une nouvelle instance de machine d'état à partir du générateur de machine d'état, vous devez transmettre les paramètres suivants.
initialStateId
: Au démarrage, l'état initial de la machine à états.
extraParams
: paramètres supplémentaires nécessaires à la création d’une nouvelle instance de machine d’état. Défini sur "new Object[0]" pour qu'aucun paramètre supplémentaire ne soit nécessaire.
un. Si l'utilisateur a transmis des paramètres supplémentaires lors de la création d'une nouvelle instance de machine d'état, assurez-vous que StateMachineBuilderFactory a également défini le type de paramètres supplémentaires lors de la création du générateur de machine d'état. Sinon, les paramètres supplémentaires seront ignorés. b. Des paramètres supplémentaires peuvent être transmis à l'instance de machine d'état de deux manières. La première consiste à utiliser le constructeur de machine à états, ce qui signifie que l'utilisateur doit définir un constructeur avec le même type et le même ordre de paramètres pour l'instance de machine à états. Une autre façon consiste à définir une méthode nommée postConstruct
et également avec le même type et le même ordre de paramètres.
Si aucun paramètre supplémentaire ne doit être transmis à la machine à états, l'utilisateur peut simplement appeler T newStateMachine(S initialStateId)
pour créer une nouvelle instance de machine à états.
Nouvelle machine à états du constructeur de machines à états. (Dans ce cas, aucun paramètre supplémentaire ne doit être transmis.)
MyStateMachine stateMachine = builder . newStateMachine ( MyState . Initial );
Déclencher des transitions
Une fois la machine à états créée, l'utilisateur peut déclencher des événements avec le contexte pour déclencher une transition à l'intérieur de la machine à états. par exemple
stateMachine . fire ( MyEvent . Prepare , new MyContext ( "Testing" ));
Machine à états non typée
Afin de simplifier l'utilisation de la machine à états et d'éviter trop de types génériques (par exemple StateMachine<T, S, E, C>) qui peuvent rendre le code difficile à lire dans certains cas, tout en conservant une partie importante de la fonctionnalité de sécurité de type sur l'action de transition exécution, UntypedStateMachine a été implémenté à cet effet.
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" );
Pour créer un UntypedStateMachine, l'utilisateur doit d'abord créer un UntypedStateMachineBuilder via StateMachineBuilderFactory. StateMachineBuilderFactory ne prend qu'un seul paramètre qui est le type de classe de machine d'état pour créer UntypedStateMachineBuilder. @StateMachineParameters est utilisé pour déclarer les types de paramètres génériques de machine à états. AbstractUntypedStateMachine est la classe de base de toute machine à états non typée.
Machine à états insensible au contexte
Parfois, la transition d’état ne se soucie pas du contexte, ce qui signifie que la transition est généralement uniquement déterminée par l’événement. Dans ce cas, l'utilisateur peut utiliser une machine à états insensible au contexte pour simplifier les paramètres d'appel de méthode. Déclarer une machine à états insensible au contexte est assez simple. L'utilisateur doit uniquement ajouter l'annotation @ContextInsensitive sur la classe d'implémentation de la machine à états. Après cela, le paramètre de contexte peut être ignoré dans la liste des paramètres de la méthode de transition. par exemple
@ 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 ) {
...
}
}
Gestion des exceptions de transition
Lorsqu'une exception se produit pendant la transition d'état, la liste d'actions exécutées sera abandonnée et la machine d'état entrera dans un état d'erreur, ce qui signifie que l'instance de machine d'état ne peut plus traiter l'événement. Si l'utilisateur continue de déclencher un événement sur l'instance de machine d'état, une IllegalStateException sera levée. Toutes les exceptions survenues pendant la phase de transition, y compris l'exécution de l'action et l'invocation d'un écouteur externe, seront enveloppées dans TransitionException (exception non cochée). Actuellement, la stratégie de gestion des exceptions par défaut est simple et grossière : il suffit de continuer à rejeter l'exception, voir la méthode AbstractStateMachine.afterTransitionCausedException.
protected void afterTransitionCausedException (...) { throw e ; }
Si la machine d'état peut être récupérée à partir de cette exception, l'utilisateur peut étendre la méthode afterTransitionCausedException et ajouter la logique de récupération correspondante dans cette méthode. N'oubliez pas de remettre l'état de la machine d'état à la normale à la fin. par exemple
@ 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 );
}
}
Définir l'état hiérarchique
Un état hiérarchique peut contenir un état imbriqué. Les états enfants peuvent eux-mêmes avoir des enfants imbriqués et l'imbrication peut se dérouler à n'importe quelle profondeur. Lorsqu'un état hiérarchique est actif, un et un seul de ses états enfants est actif. L'état hiérarchique peut être défini via une API ou une annotation.
void defineSequentialStatesOn ( S parentStateId , S ... childStateIds );
builder.defineSequentialStatesOn(State.A, State.BinA, StateCinA) définit deux états enfants "BinA" et "CinA" sous l'état parent "A", le premier état enfant défini sera également l'état initial de l'état hiérarchique "A" . Le même état hiérarchique peut également être défini via une annotation, par exemple
@ 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" )
})
Définir l'état parallèle
L'état parallèle encapsule un ensemble d'états enfants qui sont simultanément actifs lorsque l'élément parent est actif. L'état parallèle peut être défini via une API ou une annotation. par exemple
// 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 );
ou
@ 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" )
})
Pour obtenir les sous-états actuels de l'état parallèle
stateMachine . getSubStatesOn ( MyState . Root ); // return list of current sub states of parallel state
Lorsque tous les états parallèles ont atteint l’état final, un événement de contexte Finish sera déclenché.
Définir un événement de contexte
L'événement contextuel signifie que l'événement défini par l'utilisateur a un contexte prédéfini dans la machine à états. squirrel-foundation a défini trois types d'événements contextuels pour différents cas d'utilisation. Événement de début/de fin : l'événement déclaré comme événement de début/de fin sera utilisé au démarrage/à la fin de la machine d'état. Ainsi, l'utilisateur peut différencier le déclencheur d'action invoqué, par exemple lorsque la machine à états démarre et entre dans son état initial, l'utilisateur peut différencier ces actions d'entrée d'état invoquées par l'événement de démarrage. Événement de fin : lorsque tous les états parallèles ont atteint l'état final, l'événement de fin sera automatiquement déclenché. L'utilisateur peut définir la transition suivante en fonction de l'événement de fin. Pour définir l'événement de contexte, l'utilisateur dispose de deux voies, une API d'annotation ou de création.
@ ContextEvent ( finishEvent = "Finish" )
static class ParallelStateMachine extends AbstractStateMachine <...> {
}
ou
StateMachineBuilder <...> builder = StateMachineBuilderFactory . create (...);
...
builder . defineFinishEvent ( HEvent . Start );
builder . defineTerminateEvent ( HEvent . Terminate );
builder . defineStartEvent ( HEvent . Finish );
Utilisation des états d'historique pour enregistrer et restaurer l'état actuel
Le pseudo-état d'historique permet à une machine d'état de se souvenir de sa configuration d'état. Une transition prenant l’état historique comme cible ramènera la machine à états à cette configuration enregistrée. Si le « type » d'un historique est « superficiel », le processeur de la machine d'état doit enregistrer les enfants actifs directs de son parent avant d'effectuer toute transition qui sort du parent. Si le « type » d'un historique est « profond », le processeur de la machine à états doit enregistrer tous les descendants actifs du parent avant d'effectuer toute transition qui quitte le parent. L'API et les annotations sont prises en charge pour définir le type d'état d'historique. par exemple
// defined history type of state "A" as "deep"
builder . defineSequentialStatesOn ( MyState . A , HistoryType . DEEP , MyState . A1 , MyState . A2 )
ou
@ State ( parent = "A" , name = "A1" , entryCallMethod = "enterA1" , exitCallMethod = "exitA1" , historyType = HistoryType . DEEP )
Remarque : Avant la version 0.3.7, l'utilisateur devait définir "HistoryType.DEEP" pour chaque niveau d'état historique, ce qui n'est pas très pratique. (Merci à Voskuijlen pour avoir fourni la solution Issue33). Désormais, l'utilisateur définit uniquement "HistoryType.DEEP" au niveau supérieur de l'état historique, et toutes ses informations historiques sur l'état des enfants seront mémorisées.
Types de transitions
Selon la spécification UML, une transition peut être de l'un de ces trois types :
- Transition interne Implique que la transition, si elle est déclenchée, se produit sans sortir ni entrer dans l'État source (c'est-à-dire qu'elle ne provoque pas de changement d'état). Cela signifie que la condition d’entrée ou de sortie de l’État source ne sera pas invoquée. Une transition interne peut être effectuée même si la StateMachine se trouve dans une ou plusieurs régions imbriquées dans l'état associé.
- Transition locale Implique que la transition, si elle est déclenchée, ne quittera pas l'État composite (source), mais qu'elle quittera et rentrera dans n'importe quel état de l'État composite qui se trouve dans la configuration d'état actuelle.
- Transition externe Implique que la transition, si elle est déclenchée, quittera l'état composite (source)
squirrel-foundation prend en charge à la fois l'API et les annotations pour déclarer toutes sortes de transitions, par exemple
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 )
ou
@ 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 ),
})
Envoi d'événements de polymorphisme
Au cours du cycle de vie de la machine à états, divers événements seront déclenchés, par exemple
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 */
L'utilisateur peut ajouter un écouteur pour écouter StateMachineEvent, ce qui signifie que tous les événements déclenchés pendant le cycle de vie de la machine d'état seront interceptés par cet écouteur, par exemple :
stateMachine . addStateMachineListener ( new StateMachineListener <...>() {
@ Override
public void stateMachineEvent ( StateMachineEvent <...> event ) {
// ...
}
});
Et l'utilisateur peut également ajouter un écouteur pour écouter TransitionEvent via StateMachine.addTransitionListener, ce qui signifie que tous les événements déclenchés lors de chaque transition d'état, y compris TransitionBeginEvent, TransitionCompleteEvent et TransitionEndEvent, seront interceptés par cet écouteur. L'utilisateur peut également ajouter un écouteur spécifique, par exemple TransitionDeclinedListener, pour écouter TransitionDeclinedEvent lorsque la demande de transition a été refusée.
Écouteur d'événement déclaratif
L'ajout d'un écouteur d'événements ci-dessus à la machine d'état est parfois ennuyeux pour l'utilisateur, et trop de types génériques rendent également le code laid à lire. Pour simplifier l'utilisation de la machine à états, ce qui est plus important pour fournir une intégration non invasive, squirrel-foundation fournit un moyen déclaratif d'ajouter un écouteur d'événement via l'annotation suivante, par exemple
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 );
En faisant cela, le code du module externe n'a pas besoin d'implémenter d'interface d'écoute de machine d'état. Ajoutez seulement quelques annotations sur les méthodes qui seront accrochées pendant la phase de transition. Les paramètres de la méthode sont également de type sécurisé et seront automatiquement déduits pour correspondre à l'événement correspondant. C'est une bonne approche pour la séparation des préoccupations . L'utilisateur peut trouver un exemple d'utilisation dans org.squirrelframework.foundation.fsm.StateMachineLogger .
Méthodes d'extension de transition
Chaque événement de transition a également une méthode d'extension correspondante sur la classe AbstractStateMachine qui peut être étendue dans la classe d'implémentation de machine à états client.
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 ) {
}
En règle générale, l'utilisateur peut intégrer votre logique de traitement métier à ces méthodes d'extension lors de chaque transition d'état, tandis que les différents écouteurs d'événements servent de limite au système de contrôle basé sur la machine d'état, qui peut interagir avec des modules externes (par exemple, interface utilisateur, audit, ESB, etc. ). Par exemple, l'utilisateur peut étendre la méthode afterTransitionCausedException pour le nettoyage de l'environnement lorsqu'une exception s'est produite pendant la transition, et également informer le module d'interface utilisateur d'afficher un message d'erreur via TransitionExceptionEvent.
Action pondérée
L'utilisateur peut définir le poids de l'action pour ajuster l'ordre d'exécution des actions. Les actions lors de l'entrée/sortie d'état et de la transition d'état sont classées par ordre croissant en fonction de leur valeur de poids. Le poids de l'action est de 0 par défaut. L'utilisateur dispose de deux manières de définir le poids de l'action.
La première consiste à ajouter le numéro de poids au nom de la méthode et à le séparer par ':'.
// 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" )
Une autre façon consiste à remplacer la méthode de poids de la classe Action, par exemple
Action <...> newAction = new Action <...>() {
...
@ Override
public int weight () {
return 100 ;
}
}
squirrel-foundation prend également en charge une manière conventionnelle de déclarer le poids d'action. Le poids de l'action d'appel de méthode dont le nom commence par « avant » sera défini sur 100, de sorte que le nom commençant par « après » sera défini sur -100. Généralement, cela signifie que le nom de la méthode d'action commençant par « avant » sera invoqué en premier, tandis que le nom de la méthode d'action commençant par « après » sera invoqué en dernier lieu. "method1:ignore" signifie que la méthode1 ne sera pas invoquée.
Pour plus d'informations, veuillez vous référer au scénario de test « org.squirrelframework.foundation.fsm.WeightedActionTest » ;
Exécution asynchrone
L'annotation @AsyncExecute peut être utilisée sur l'action d'appel de méthode et l'écouteur d'événement déclaratif pour indiquer que cette action ou cet écouteur d'événement sera exécuté de manière asynchrone, par exemple Définir une méthode d'action invoquée de manière asynchrone :
@ 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
}
}
Définissez un événement distribué de manière asynchrone :
public class DeclarativeListener {
@ OnTransitionBegin
@ AsyncExecute
public void onTransitionBegin (...) {
// transition begin event will be dispatched asynchronously to this listener method
}
}
La tâche d'exécution asynchrone sera soumise à un ExecutorService . L'utilisateur peut enregistrer votre instance d'implémentation ExecutorService via SquirrelSingletonProvider , par exemple
ExecutorService executorService = Executors . newFixedThreadPool ( 1 );
SquirrelSingletonProvider . getInstance (). register ( ExecutorService . class , executorService );
Si aucune instance ExecutorService n'a été enregistrée, SquirrelConfiguration en fournira une par défaut.
Postprocesseur de machine d'état
L'utilisateur peut enregistrer le post-processeur pour un type spécifique de machine à états afin d'ajouter une logique de post-traitement après l'instanciation de la machine à états, par exemple
// 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 );
Dans ce cas, lorsque l'utilisateur a créé les instances MyStateMachineImpl et MyStateMachineImplEx, le post-processeur enregistré MyStateMachinePostProcessor sera appelé pour effectuer certains travaux.
Exportation de machines d'état
SCXMLVisitor peut être utilisé pour exporter la définition de la machine à états dans le document [SCXML] 2.
SCXMLVisitor visitor = SquirrelProvider . getInstance (). newInstance ( SCXMLVisitor . class );
stateMachine . accept ( visitor );
visitor . convertSCXMLFile ( "MyStateMachine" , true );
BTW, l'utilisateur peut également appeler StateMachine.exportXMLDefinition(true) pour exporter une définition XML embellie. DotVisitor peut être utilisé pour générer un diagramme d'état qui peut être visualisé par [GraphViz] 3.
DotVisitor visitor = SquirrelProvider . getInstance (). newInstance ( DotVisitor . class );
stateMachine . accept ( visitor );
visitor . convertDotFile ( "SnakeStateMachine" );
Importation de machine d'état
UntypedStateMachineImporter peut être utilisé pour importer une définition similaire à SCXML de machine à états qui a été exportée par SCXMLVisitor ou une définition d'écriture manuscrite. UntypedStateMachineImporter construira un UntypedStateMachineBuilder selon la définition qui pourra ensuite être utilisée pour créer des instances de machine d'état.
UntypedStateMachineBuilder builder = new UntypedStateMachineImporter (). importDefinition ( scxmlDef );
ATMStateMachine stateMachine = builder . newAnyStateMachine ( ATMState . Idle );
Remarque : UntypedStateMachineImporter a fourni un style XML pour définir la machine à états, tout comme l'API du générateur de machine à états ou les annotations déclaratives. La définition similaire à SCXML n'est pas égale au SCXML standard.
Enregistrer/charger les données de la machine d'état
L'utilisateur peut enregistrer les données de la machine d'état lorsque la machine d'état est en état d'inactivité.
StateMachineData . Reader < MyStateMachine , MyState , MyEvent , MyContext >
savedData = stateMachine . dumpSavedData ();
Et également l'utilisateur peut charger les données sauvegardées ci-dessus dans une autre machine d'état dont le statut est terminé ou simplement initialisé.
newStateMachineInstance . loadSavedData ( savedData );
REMARQUE : les données de la machine d'état peuvent être sérialisées vers/désérialisées à partir d'une chaîne codée en Base64 à l'aide de la classe ObjectSerializingSupport .
Configuration de la machine d'état
Lors de la création d'une nouvelle instance de machine d'état, l'utilisateur peut configurer son comportement via StateMachineConfiguration , par exemple
UntypedStateMachine fsm = builder . newUntypedStateMachine ( "a" ,
StateMachineConfiguration . create (). enableAutoStart ( false )
. setIdProvider ( IdProvider . UUIDProvider . getInstance ()),
new Object [ 0 ]); // since 0.3.0
fsm . fire ( TestEvent . toA );
L'exemple de code ci-dessus est utilisé pour créer une instance de machine d'état avec l'UUID comme identifiant et désactiver la fonction de démarrage automatique. StateMachineConfigure peut également être défini sur le constructeur de machine d'état, ce qui signifie que toutes les instances de machine d'état créées par builder.newStateMachine(S initialStateId)
ou builder.newStateMachine(S initialStateId, Object... extraParams)
utiliseront cette configuration.
Diagnostic de la machine d'état
StateMachineLogger est utilisé pour observer l'état interne de la machine à états, comme les performances d'exécution, la séquence d'appel d'action, la progression de la transition, etc.
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 .
...
Étant donné que l'enregistreur de machine d'état v0.3.0 peut être utilisé de manière plus simple en définissant simplement StateMachineConfiguration, activez le mode de débogage, par exemple
StateMachine<?,?,?,?> stateMachine = builder.newStateMachine(HState.A,
StateMachineConfiguration.create().enableDebugMode(true),
new Object[0]);
StateMachinePerformanceMonitor peut être utilisé pour surveiller les informations sur les performances d'exécution de la machine à états, y compris le nombre total de temps de transition, le temps moyen consommé par la transition, etc.
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 ==========================
Ajouter @LogExecTime sur la méthode d'action déconnectera le temps d'exécution de la méthode. Et ajoutez également la classe de machine d'état @LogExecTime sur qui déconnectera tout le temps d'exécution de la méthode d'action. Par exemple, le temps d'exécution de la méthode transitFromAToBOnGoToB sera déconnecté.
@ LogExecTime
protected void transitFromAToBOnGoToB ( MyState from , MyState to , MyEvent event , MyContext context )
État chronométré
Un état temporisé est un état qui peut retarder ou déclencher périodiquement un événement spécifié après l'entrée dans l'état. La tâche chronométrée sera soumise à un ScheduledExecutorService . L'utilisateur peut enregistrer votre instance d'implémentation ScheduledExecutorService via SquirrelSingletonProvider , par exemple
ScheduledExecutorService scheduler = Executors . newScheduledThreadPool ( 1 );
SquirrelSingletonProvider . getInstance (). register ( ScheduledExecutorService . class , scheduler );
Si aucune instance de ScheduledExecutorService n'a été enregistrée, SquirrelConfiguration en fournira une par défaut. Après cela, un état temporisé peut être défini par le constructeur de la machine à états, par exemple
// after 50ms delay fire event "FIRST" every 100ms with null context
builder . defineTimedState ( "A" , 50 , 100 , "FIRST" , null );
builder . internalTransition (). within ( "A" ). on ( "FIRST" );
REMARQUE : Assurez-vous que l'état temporisé doit être défini avant de décrire ses transitions ou ses actions d'entrée/sortie. timeInterval inférieur ou égal à 0 ne sera considéré comme exécuté qu'une seule fois après initialDelay .
État lié (appelé état de sous-machine)
Un état lié spécifie l'insertion de la spécification d'une machine à états de sous-machine. La machine à états qui contient l’état lié est appelée machine à états contenant. La même machine à états peut être une sous-machine plus d'une fois dans le contexte d'une seule machine à états contenant.
Un état lié est sémantiquement équivalent à un état composite. Les régions de la machine à états sous-machine sont les régions de l'état composite. Les actions d'entrée, de sortie et de comportement ainsi que les transitions internes sont définies dans le cadre de l'état. L’état sous-machine est un mécanisme de décomposition qui permet de prendre en compte les comportements courants et de les réutiliser. L’état lié peut être défini en suivant l’exemple de code.
builderOfTestStateMachine . definedLinkedState ( LState . A , builderOfLinkedStateMachine , LState . A1 );
Prise en charge de JMX
Depuis la version 0.3.3, l'utilisateur peut surveiller à distance l'instance de machine d'état (par exemple, l'état actuel, le nom) et modifier les configurations (par exemple, activer/désactiver les journaux/activer le moniteur de performances/événement d'incendie à distance) au moment de l'exécution. Toutes les informations sur les instances de machine d'état seront sous le domaine "org.squirrelframework". L'exemple de code suivant montre comment activer la prise en charge JMX.
UntypedStateMachineBuilder builder = StateMachineBuilderFactory . create (...);
builder . setStateMachineConfiguration ( StateMachineConfiguration . create (). enableRemoteMonitor ( true ));
REMARQUE : la prise en charge des fonctionnalités JMX est obsolète depuis la version 0.3.9-SNAPSHOT.
Voir le fichier EXEMPLES.
Voir le fichier NOTES DE VERSION.
Pour les dernières mises à jour, suivez mon twitter @hhe11 ou +HeHenry
Pour des discussions ou des questions, veuillez rejoindre le groupe des machines à états d'écureuil
Pour tout problème ou exigence, veuillez soumettre un problème
Si vous utilisez le code Squirrel State Machine dans votre application, je vous serais reconnaissant d'en informer l'auteur (e-mail : [email protected]) comme ceci :
Objet : Notification d'utilisation de Squirrel State Machine Texte : J'utilise Squirrel State Machine <lib_version> dans <project_name> - http://link_to_project. Je [autorise | ne permettez pas] de mentionner mon projet dans la section "Qui utilise Squirrel State Machine" sur GitHub.