작고 , 민첩하고 , 똑똑하고, 기민하고 , 귀여운 동물인 다람쥐와 마찬가지로, squirrel-foundation은 엔터프라이즈 사용을 위한 가볍고 , 고도로 유연하고 , 확장 가능 하고, 진단 가능하고 , 사용하기 쉽고 , 유형이 안전한 Java 상태 머신 구현을 제공하는 것을 목표로 합니다.
다음은 ATM의 상태 변화를 설명하는 상태 머신 다이어그램입니다.
샘플 코드는 "org.squirrelframework.foundation.fsm.atm" 패키지에서 찾을 수 있습니다.
squirrel-foundation은 maven 중앙 저장소에 배포되었으므로 pom.xml에 다음 종속성만 추가하면 됩니다.
최신 출시 버전:
<dependency>
<groupId>org.squirrelframework</groupId>
<artifactId>squirrel-foundation</artifactId>
<version>0.3.10</version>
</dependency>
최신 스냅샷 버전:
<dependency>
<groupId>org.squirrelframework</groupId>
<artifactId>squirrel-foundation</artifactId>
<version>0.3.11-SNAPSHOT</version>
</dependency>
다람쥐 상태 머신 기능을 빠르게 사용해 보려면 Maven 프로젝트를 만들고 다람쥐 기반 종속성을 적절하게 포함하세요. 그런 다음 다음 샘플 코드를 실행하세요.
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 ());
}
}
지금은 샘플 코드에 대해 많은 질문이 있을 수 있습니다. 잠시 기다려 주십시오. 다음 사용자 가이드는 대부분의 질문에 대한 답변을 제공합니다. 그러나 세부 사항을 살펴보기 전에 상태 머신 개념에 대한 기본적인 이해가 필요합니다. 이러한 자료는 상태 머신 개념을 이해하는 데 유용합니다. [상태 머신 다이어그램] [qt-상태 머신]
squirrel-foundation은 상태 머신을 선언하기 위한 유창한 API와 선언적 방식을 모두 지원하며 사용자가 간단한 방식으로 작업 메서드를 정의할 수 있도록 합니다.
StateMachine 인터페이스는 네 가지 일반 유형 매개변수를 사용합니다.
상태 머신 빌더
상태 머신을 생성하려면 먼저 상태 머신 빌더를 생성해야 합니다. 예를 들어:
StateMachineBuilder < MyStateMachine , MyState , MyEvent , MyContext > builder =
StateMachineBuilderFactory . create ( MyStateMachine . class , MyState . class , MyEvent . class , MyContext . class );
상태 머신 빌더는 상태 머신(T), 상태(S), 이벤트(E) 및 컨텍스트(C) 유형의 매개변수를 사용합니다.
유창한 API
상태 머신 빌더가 생성된 후 Fluent API를 사용하여 상태 머신의 상태/전환/작업을 정의할 수 있습니다.
builder . externalTransition (). from ( MyState . A ). to ( MyState . B ). on ( MyEvent . GoToB );
상태 'A'에서 상태 'B' 사이에 외부 전환이 구축되고 수신된 이벤트 'GoToB'에서 트리거됩니다.
builder . internalTransition ( TransitionPriority . HIGH ). within ( MyState . A ). on ( MyEvent . WithinA ). perform ( myAction );
우선순위가 높음으로 설정된 내부 전환은 'WithinA' 이벤트에서 'myAction'을 수행하는 상태 'A' 내부에 구축됩니다. 내부 전환은 전환이 완료된 후 상태가 종료되거나 입력되지 않음을 의미합니다. 전환 우선순위는 상태 머신이 확장될 때 원래 전환을 재정의하는 데 사용됩니다.
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" );
외부 컨텍스트가 조건 제한을 충족하면 이벤트 'GoToD'에서 상태 'C'에서 상태 'D'로 조건부 전환이 구축된 다음 작업 메서드 "myInternalTransitionCall"을 호출합니다. 사용자는 MVEL(강력한 표현 언어)을 사용하여 다음과 같은 방법으로 조건을 기술할 수도 있습니다.
builder . externalTransition (). from ( MyState . C ). to ( MyState . D ). on ( MyEvent . GoToD ). whenMvel (
"MyCondition:::(context!=null && context.getValue()>80)" ). callMethod ( "myInternalTransitionCall" );
참고: ':::' 문자는 조건 이름과 조건 표현식을 구분하는 데 사용됩니다. '컨텍스트'는 현재 컨텍스트 객체를 가리키는 미리 정의된 변수입니다.
builder . onEntry ( MyState . A ). perform ( Lists . newArrayList ( action1 , action2 ))
상태 입력 작업 목록은 위의 샘플 코드에 정의되어 있습니다.
메서드 호출 작업
사용자는 전환 정의 또는 상태 입력/종료 중에 익명 작업을 정의할 수 있습니다. 그러나 작업 코드는 여러 위치에 분산되어 코드를 유지 관리하기 어려울 수 있습니다. 또한 다른 사용자는 해당 작업을 무시할 수 없습니다. 따라서 squirrel-foundation은 상태 머신 클래스 자체와 함께 제공되는 상태 머신 메서드 호출 작업을 정의하는 것도 지원합니다.
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
...
}
}
또한 squirrel-foundation은 Convention Over Configuration 방식으로 메서드 호출 작업 정의도 지원합니다. 기본적으로 이는 상태 머신에서 선언된 메서드가 명명 및 매개변수 규칙을 충족하는 경우 전환 작업 목록에 추가되고 특정 단계에서도 호출된다는 의미입니다. 예를 들어
protected void transitFromAToBOnGoToB ( MyState from , MyState to , MyEvent event , MyContext context )
transitFrom[SourceStateName]To[TargetStateName]On[EventName] 으로 이름이 지정되고 [MyState, MyState, MyEvent, MyContext]로 매개변수화된 메서드가 전환 "A-(GoToB)->B" 작업 목록에 추가됩니다. 이벤트 'GoToB'에서 상태 'A'에서 상태 'B'로 전환할 때 이 메서드가 호출됩니다.
protected void transitFromAnyToBOnGoToB ( MyState from , MyState to , MyEvent event , MyContext context )
transitFromAnyTo[TargetStateName]On[EventName] 이벤트 'GoToB'에서 임의의 상태에서 'B' 상태로 전환할 때 메서드가 호출됩니다.
protected void exitA ( MyState from , MyState to , MyEvent event , MyContext context )
종료[StateName] 종료 상태 'A'일 때 메소드가 호출됩니다. 따라서 항목[StateName] , beforeExitAny / afterExitAny 및 beforeEntryAny / afterEntryAny 와 같습니다.
기타 지원되는 명명 패턴:
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]
위에 나열된 메서드 규칙은 AOP와 유사한 기능도 제공하여 모든 세부 수준에서 다람쥐 상태 머신에 대한 내장된 유연한 확장 기능을 제공했습니다. 자세한 내용은 테스트 케이스 " org.squirrelframework.foundation.fsm.ExtensionMethodCallTest "를 참조하세요. 0.3.1부터 유창한 API를 통해 AOP와 유사한 확장 메서드를 정의하는 또 다른 방법이 있습니다(vittali의 제안에 감사드립니다). 예:
// 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" );
또는 선언적 주석을 통해 예를 들어
// since 0.3.1
@ Transitions ({
@ Transit ( from = "B" , to = "E" , on = "*" , callMethod = "fromBToEOnAny" ),
@ Transit ( from = "*" , to = "E" , on = "ToE" , callMethod = "fromAnyToEOnToE" )
})
참고 : 이러한 작업 메서드는 일치하고 이미 존재하는 전환 에 연결되지만 새 전환을 생성하지는 않습니다. 0.3.4부터 다음 API를 사용하여 여러 전환을 한 번에 정의할 수도 있습니다.
// 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 ) );
자세한 내용은 org.squirrelframework.foundation.fsm.samples.DecisionStateSampleTest 에서 확인할 수 있습니다.
선언적 주석
상태 머신을 정의하고 확장하기 위한 선언적 방법도 제공됩니다. 여기에 예가 있습니다.
@ 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 );
...
}
주석은 상태 기계의 구현 클래스 또는 상태 기계가 구현될 인터페이스 모두에서 정의될 수 있습니다. 또한 Fluent API와 혼합하여 사용할 수 있습니다. 즉, Fluent API에 정의된 상태 머신도 이러한 주석으로 확장될 수 있습니다. (한 가지 주의해야 할 점은 인터페이스 내에 정의된 메서드가 공개되어야 한다는 것입니다. 즉, 메서드 호출 작업 구현도 호출자에게 공개됩니다.)
변환기
@State 및 @Transit 내에서 상태 및 이벤트를 선언하려면 사용자는 상태(S) 및 이벤트(E) 유형에 해당하는 변환기를 구현해야 합니다. 변환은 상태/이벤트를 문자열로/에서 변환하는 Converter<T> 인터페이스를 구현해야 합니다.
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 );
}
그런 다음 이러한 변환기를 ConverterProvider 에 등록합니다. 예를 들어
ConverterProvider . INSTANCE . register ( MyEvent . class , new MyEventConverter ());
ConverterProvider . INSTANCE . register ( MyState . class , new MyStateConverter ());
참고: Fluent API만 사용하여 상태 머신을 정의하는 경우 해당 변환기를 구현할 필요가 없습니다. 또한 Event 또는 State 클래스가 String 또는 Enumeration 유형인 경우 대부분의 경우 명시적으로 변환기를 구현하거나 등록할 필요가 없습니다.
새로운 상태 머신 인스턴스
사용자가 상태 머신 동작을 정의한 후 사용자는 빌더를 통해 새로운 상태 머신 인스턴스를 생성할 수 있습니다. 상태 머신 인스턴스가 빌더에서 생성되면 빌더를 사용하여 상태 머신의 새 요소를 더 이상 정의할 수 없습니다.
T newStateMachine ( S initialStateId , Object ... extraParams );
상태 머신 빌더에서 새 상태 머신 인스턴스를 생성하려면 다음 매개변수를 전달해야 합니다.
initialStateId
: 시작되면 상태 머신의 초기 상태입니다.
extraParams
: 새로운 상태 머신 인스턴스를 생성하는 데 필요한 추가 매개변수입니다. 추가 매개변수가 필요하지 않도록 하려면 "new Object[0]" 으로 설정하세요.
에이. 사용자가 새 상태 머신 인스턴스를 생성하는 동안 추가 매개변수를 전달한 경우 StateMachineBuilderFactory도 상태 머신 빌더를 생성할 때 추가 매개변수 유형을 정의했는지 확인하세요. 그렇지 않으면 추가 매개변수가 무시됩니다. 비. 추가 매개변수는 두 가지 방법으로 상태 머신 인스턴스에 전달될 수 있습니다. 하나는 상태 기계 생성자를 통한 것인데, 이는 사용자가 상태 기계 인스턴스에 대해 동일한 매개변수 유형 및 순서를 사용하여 생성자를 정의해야 함을 의미합니다. 또 다른 방법은 postConstruct
라는 메서드를 정의하고 동일한 매개변수 유형과 순서를 사용하는 것입니다.
상태 시스템에 추가 매개변수를 전달할 필요가 없는 경우 사용자는 간단히 T newStateMachine(S initialStateId)
호출하여 새 상태 시스템 인스턴스를 생성할 수 있습니다.
상태 머신 빌더의 새로운 상태 머신. (이 경우 추가 매개변수를 전달할 필요가 없습니다.)
MyStateMachine stateMachine = builder . newStateMachine ( MyState . Initial );
트리거 전환
상태 머신이 생성된 후 사용자는 컨텍스트와 함께 이벤트를 실행하여 상태 머신 내부의 전환을 트리거할 수 있습니다. 예를 들어
stateMachine . fire ( MyEvent . Prepare , new MyContext ( "Testing" ));
유형이 지정되지 않은 상태 머신
상태 기계 사용을 단순화하고 경우에 따라 코드를 읽기 어렵게 만들 수 있는 너무 많은 일반 유형(예: StateMachine<T, S, E, C>)을 피하지만 전환 작업에서 유형 안전 기능의 중요한 부분을 여전히 유지합니다. 실행 시 UntypedStateMachine이 이 목적으로 구현되었습니다.
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" );
UntypedStateMachine을 빌드하려면 먼저 StateMachineBuilderFactory를 통해 UntypedStateMachineBuilder를 생성해야 합니다. StateMachineBuilderFactory는 UntypedStateMachineBuilder를 생성하기 위해 상태 머신 클래스 유형인 하나의 매개변수만 사용합니다. @StateMachineParameters 는 상태 머신 일반 매개변수 유형을 선언하는 데 사용됩니다. AbstractUntypedStateMachine 은 유형이 지정되지 않은 상태 머신의 기본 클래스입니다.
상황에 무관한 상태 머신
때로는 상태 전환이 컨텍스트를 고려하지 않는 경우가 있는데, 이는 대부분 이벤트에 의해서만 결정되는 전환을 의미합니다. 이 경우 사용자는 상황에 맞지 않는 상태 시스템을 사용하여 메서드 호출 매개 변수를 단순화할 수 있습니다. 상황에 무관한 상태 머신을 선언하는 것은 매우 간단합니다. 사용자는 상태 머신 구현 클래스에 @ContextInsensitive 주석만 추가하면 됩니다. 이후에는 전환 메서드 매개변수 목록에서 컨텍스트 매개변수를 무시할 수 있습니다. 예를 들어
@ 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 ) {
...
}
}
전환 예외 처리
상태 전환 중에 예외가 발생하면 실행된 작업 목록이 중단되고 상태 머신이 오류 상태가 됩니다. 이는 상태 머신 인스턴스가 더 이상 이벤트를 처리할 수 없음을 의미합니다. 사용자가 계속해서 상태 머신 인스턴스에 이벤트를 발생시키면 IllegalStateException이 발생합니다. 작업 실행 및 외부 리스너 호출을 포함하여 전환 단계에서 발생한 모든 예외는 TransitionException(확인되지 않은 예외)으로 래핑됩니다. 현재 기본 예외 처리 전략은 예외를 계속해서 발생시키는 단순하고 무례한 방법입니다. AbstractStateMachine.afterTransitionCausedException 메서드를 참조하세요.
protected void afterTransitionCausedException (...) { throw e ; }
이 예외로부터 상태 시스템을 복구할 수 있는 경우 사용자는 afterTransitionCausedException 메서드를 확장하고 이 메서드에 해당 복구 논리를 추가할 수 있습니다. 마지막에 상태 머신 상태를 다시 정상으로 설정하는 것을 잊지 마세요 . 예를 들어
@ 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 );
}
}
계층적 상태 정의
계층적 상태에는 중첩된 상태가 포함될 수 있습니다. 자식 상태에는 중첩된 자식이 있을 수 있으며 중첩은 어떤 깊이로든 진행될 수 있습니다. 계층적 상태가 활성화되면 해당 하위 상태 중 하나만 활성화됩니다. 계층적 상태는 API나 주석을 통해 정의할 수 있습니다.
void defineSequentialStatesOn ( S parentStateId , S ... childStateIds );
builder.defineSequentialStatesOn(State.A, State.BinA, StateCinA)는 상위 상태 "A" 아래에 두 개의 하위 상태 "BinA" 및 "CinA"를 정의합니다. 처음으로 정의된 하위 상태는 계층적 상태 "A"의 초기 상태이기도 합니다. . 동일한 계층적 상태는 주석을 통해 정의될 수도 있습니다.
@ 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" )
})
병렬 상태 정의
병렬 상태는 상위 요소가 활성화될 때 동시에 활성화되는 하위 상태 세트를 캡슐화합니다. 병렬 상태는 API나 주석을 통해 정의할 수 있습니다. 예를 들어
// 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 );
또는
@ 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" )
})
병렬 상태의 현재 하위 상태를 얻으려면
stateMachine . getSubStatesOn ( MyState . Root ); // return list of current sub states of parallel state
모든 병렬 상태가 최종 상태에 도달하면 마침 컨텍스트 이벤트가 시작됩니다.
컨텍스트 이벤트 정의
컨텍스트 이벤트는 사용자 정의 이벤트가 상태 머신에 미리 정의된 컨텍스트를 가지고 있음을 의미합니다. squirrel-foundation은 다양한 사용 사례에 대해 세 가지 유형의 컨텍스트 이벤트를 정의했습니다. 시작/종료 이벤트 : 시작/종료 이벤트로 선언된 이벤트는 상태 머신이 시작/종료될 때 사용됩니다. 따라서 사용자는 호출된 작업 트리거를 구별할 수 있습니다. 예를 들어 상태 머신이 시작되고 초기 상태에 들어갈 때 사용자는 이러한 상태 입력 작업이 시작 이벤트에 의해 호출되었음을 구별할 수 있습니다. 종료 이벤트 : 모든 병렬 상태가 최종 상태에 도달하면 종료 이벤트가 자동으로 발생됩니다. 사용자는 종료 이벤트를 기반으로 다음 전환을 정의할 수 있습니다. 컨텍스트 이벤트를 정의하기 위해 사용자는 주석 또는 빌더 API의 양방향을 사용할 수 있습니다.
@ ContextEvent ( finishEvent = "Finish" )
static class ParallelStateMachine extends AbstractStateMachine <...> {
}
또는
StateMachineBuilder <...> builder = StateMachineBuilderFactory . create (...);
...
builder . defineFinishEvent ( HEvent . Start );
builder . defineTerminateEvent ( HEvent . Terminate );
builder . defineStartEvent ( HEvent . Finish );
기록 상태를 사용하여 현재 상태 저장 및 복원
기록 의사 상태를 사용하면 상태 머신이 상태 구성을 기억할 수 있습니다. 기록 상태를 대상으로 하는 전환은 상태 머신을 이 기록된 구성으로 반환합니다. 기록의 '유형'이 "얕은" 경우 상태 머신 프로세서는 상위에서 나가는 전환을 수행하기 전에 상위의 직접 활성 하위를 기록해야 합니다. 기록의 '유형'이 "deep"인 경우 상태 머신 프로세서는 부모를 종료하는 전환을 수행하기 전에 부모의 모든 활성 하위 항목을 기록해야 합니다. 상태의 기록 유형을 정의하기 위해 API와 주석이 모두 지원됩니다. 예를 들어
// defined history type of state "A" as "deep"
builder . defineSequentialStatesOn ( MyState . A , HistoryType . DEEP , MyState . A1 , MyState . A2 )
또는
@ State ( parent = "A" , name = "A1" , entryCallMethod = "enterA1" , exitCallMethod = "exitA1" , historyType = HistoryType . DEEP )
참고: 0.3.7 이전에는 사용자가 기록 상태의 각 수준에 대해 "HistoryType.DEEP"를 정의해야 하는데 이는 그다지 편리하지 않습니다.(솔루션을 제공한 Voskuijlen에게 감사드립니다. Issue33) 이제 사용자는 기록 상태의 최상위 수준에서만 "HistoryType.DEEP"를 정의하고 모든 하위 상태 기록 정보가 기억됩니다.
전환 유형
UML 사양에 따르면 전환은 다음 세 가지 종류 중 하나일 수 있습니다.
- 내부 전환 트리거 된 경우 소스 상태를 종료하거나 입력하지 않고 전환이 발생함을 의미합니다(즉, 상태 변경이 발생하지 않음). 이는 소스 State의 시작 또는 종료 조건이 호출되지 않음을 의미합니다. StateMachine이 연결된 상태 내에 중첩된 하나 이상의 지역에 있는 경우에도 내부 전환을 수행할 수 있습니다.
- 로컬 전환 전환이 트리거되면 복합(소스) 상태를 종료하지 않지만 현재 상태 구성에 있는 복합 상태 내의 모든 상태를 종료하고 다시 입력함을 의미합니다.
- 외부 전환은 전환이 트리거되면 복합(소스) 상태를 종료함을 의미합니다.
squirrel-foundation은 모든 종류의 전환을 선언하기 위해 API와 주석을 모두 지원합니다.
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 )
또는
@ 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 ),
})
다형성 이벤트 디스패치
상태 머신의 수명 주기 동안 다양한 이벤트가 발생합니다.
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 */
사용자는 StateMachineEvent를 수신하기 위해 리스너를 추가할 수 있습니다. 이는 상태 머신 수명 주기 동안 발생하는 모든 이벤트가 이 리스너에 의해 포착된다는 의미입니다. 예:
stateMachine . addStateMachineListener ( new StateMachineListener <...>() {
@ Override
public void stateMachineEvent ( StateMachineEvent <...> event ) {
// ...
}
});
또한 사용자는 StateMachine.addTransitionListener를 통해 TransitionEvent를 수신하는 리스너를 추가할 수도 있습니다. 이는 TransitionBeginEvent, TransitionCompleteEvent 및 TransitionEndEvent를 포함하여 각 상태 전환 중에 실행되는 모든 이벤트가 이 리스너에 의해 포착된다는 의미입니다. 또는 사용자는 전환 요청이 거부되었을 때 TransitionDeclinedEvent를 수신하기 위해 TransitionDeclinedListener와 같은 특정 리스너를 추가할 수 있습니다.
선언적 이벤트 리스너
위의 이벤트 리스너를 상태 시스템에 추가하면 사용자가 때때로 짜증을 내고 일반 유형이 너무 많아 코드를 읽기 어렵게 만듭니다. 상태 머신 사용을 단순화하고 비침습적 통합을 제공하는 것이 더 중요한 squirrel-foundation은 다음 주석을 통해 이벤트 리스너를 추가하는 선언적 방법을 제공합니다.
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 );
이렇게 하면 외부 모듈 코드는 상태 머신 리스너 인터페이스를 구현할 필요가 없습니다. 전환 단계에서 연결될 메서드에 몇 가지 주석만 추가하세요. 메소드의 매개변수도 유형이 안전하며 해당 이벤트와 일치하도록 자동으로 추론됩니다. 이는 우려 사항 분리를 위한 좋은 접근 방식입니다. 사용자는 org.squirrelframework.foundation.fsm.StateMachineLogger 에서 샘플 사용법을 찾을 수 있습니다.
전환 확장 방법
각 전환 이벤트에는 고객 상태 머신 구현 클래스에서 확장이 허용되는 AbstractStateMachine 클래스의 해당 확장 메서드도 있습니다.
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 ) {
}
일반적으로 사용자는 각 상태 전환 중에 이러한 확장 메서드에서 비즈니스 처리 로직을 연결할 수 있으며, 다양한 이벤트 리스너는 외부 모듈(예: UI, 감사, ESB 등)과 상호 작용할 수 있는 상태 머신 기반 제어 시스템의 경계 역할을 합니다. ). 예를 들어, 사용자는 전환 중 예외가 발생한 경우 환경 정리를 위해 afterTransitionCausedException 메소드를 확장할 수 있으며, TransitionExceptionEvent를 통해 오류 메시지를 표시하도록 사용자 인터페이스 모듈에 알릴 수도 있습니다.
가중 조치
사용자는 액션 가중치를 정의하여 액션 실행 순서를 조정할 수 있습니다. 상태 진입/퇴출 및 상태 전이 중 작업은 가중치 값에 따라 오름차순으로 정렬됩니다. 작업 가중치는 기본적으로 0입니다. 사용자는 작업 가중치를 설정하는 두 가지 방법이 있습니다.
하나는 메소드 이름에 가중치 번호를 추가하고 ':'으로 구분하는 것입니다.
// 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" )
또 다른 방법은 Action 클래스의 가중치 메서드를 재정의하는 것입니다.
Action <...> newAction = new Action <...>() {
...
@ Override
public int weight () {
return 100 ;
}
}
squirrel-foundation은 또한 작업 가중치를 선언하는 일반적인 방식을 지원합니다. 이름이 ' before '로 시작하는 메소드 호출 액션의 가중치는 100으로 설정되므로 ' after '로 시작하는 이름은 -100으로 설정됩니다. 일반적으로 'before'로 시작하는 액션 메소드 이름이 먼저 호출되고, 'after'로 시작되는 액션 메소드 이름이 마지막에 호출된다는 의미입니다. "method1:ignore"는 method1이 호출되지 않음을 의미합니다.
자세한 내용은 테스트 케이스 ' org.squirrelframework.foundation.fsm.WeightedActionTest '를 참조하세요.
비동기 실행
@AsyncExecute 주석은 메서드 호출 작업 및 선언적 이벤트 리스너에 사용되어 이 작업이나 이벤트 리스너가 비동기적으로 실행될 것임을 나타낼 수 있습니다. 예를 들어 비동기적으로 호출되는 작업 메서드를 정의합니다.
@ 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
}
}
비동기적으로 전달되는 이벤트를 정의합니다.
public class DeclarativeListener {
@ OnTransitionBegin
@ AsyncExecute
public void onTransitionBegin (...) {
// transition begin event will be dispatched asynchronously to this listener method
}
}
비동기 실행 작업은 ExecutorService 에 제출됩니다. 사용자는 SquirrelSingletonProvider 를 통해 ExecutorService 구현 인스턴스를 등록할 수 있습니다. 예:
ExecutorService executorService = Executors . newFixedThreadPool ( 1 );
SquirrelSingletonProvider . getInstance (). register ( ExecutorService . class , executorService );
ExecutorService 인스턴스가 등록되지 않은 경우 SquirrelConfiguration은 기본 인스턴스를 제공합니다.
상태 머신 포스트 프로세서
사용자는 상태 머신이 인스턴스화된 후 포스트 프로세스 로직을 추가하기 위해 특정 유형의 상태 머신에 대한 포스트 프로세서를 등록할 수 있습니다.
// 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 );
이 경우 사용자가 MyStateMachineImpl 및 MyStateMachineImplEx 인스턴스를 모두 생성하면 등록된 포스트 프로세서 MyStateMachinePostProcessor가 호출되어 일부 작업을 수행합니다.
상태 머신 내보내기
SCXMLVisitor는 [SCXML] 2 문서의 상태 머신 정의를 내보내는 데 사용할 수 있습니다.
SCXMLVisitor visitor = SquirrelProvider . getInstance (). newInstance ( SCXMLVisitor . class );
stateMachine . accept ( visitor );
visitor . convertSCXMLFile ( "MyStateMachine" , true );
그런데 사용자는 StateMachine.exportXMLDefinition(true)을 호출하여 아름다운 XML 정의를 내보낼 수도 있습니다. DotVisitor를 사용하면 [GraphViz] 3에서 볼 수 있는 상태 다이어그램을 생성할 수 있습니다.
DotVisitor visitor = SquirrelProvider . getInstance (). newInstance ( DotVisitor . class );
stateMachine . accept ( visitor );
visitor . convertDotFile ( "SnakeStateMachine" );
상태 머신 가져오기
UntypedStateMachineImporter는 SCXMLVisitor 또는 필기 정의에서 내보낸 상태 시스템 SCXML과 유사한 정의를 가져오는 데 사용할 수 있습니다. UntypedStateMachineImporter는 나중에 상태 머신 인스턴스를 생성하는 데 사용할 수 있는 정의에 따라 UntypedStateMachineBuilder를 빌드합니다.
UntypedStateMachineBuilder builder = new UntypedStateMachineImporter (). importDefinition ( scxmlDef );
ATMStateMachine stateMachine = builder . newAnyStateMachine ( ATMState . Idle );
참고: UntypedStateMachineImporter는 상태 머신 빌더 API 또는 선언적 주석과 마찬가지로 상태 머신을 정의하기 위한 XML 스타일을 제공했습니다. SCXML과 유사한 정의는 표준 SCXML과 동일하지 않습니다.
상태 머신 데이터 저장/로드
사용자는 상태 머신이 유휴 상태일 때 상태 머신의 데이터를 저장할 수 있습니다.
StateMachineData . Reader < MyStateMachine , MyState , MyEvent , MyContext >
savedData = stateMachine . dumpSavedData ();
또한 사용자는 위의 저장된 데이터를 상태가 종료되거나 방금 초기화된 다른 상태 머신으로 로드할 수 있습니다.
newStateMachineInstance . loadSavedData ( savedData );
참고 : 상태 머신 데이터는 ObjectSerializedSupport 클래스의 도움으로 Base64로 인코딩된 문자열로 직렬화/역직렬화될 수 있습니다.
상태 머신 구성
새로운 상태 머신 인스턴스를 생성할 때 사용자는 StateMachineConfiguration을 통해 해당 동작을 구성할 수 있습니다. 예:
UntypedStateMachine fsm = builder . newUntypedStateMachine ( "a" ,
StateMachineConfiguration . create (). enableAutoStart ( false )
. setIdProvider ( IdProvider . UUIDProvider . getInstance ()),
new Object [ 0 ]); // since 0.3.0
fsm . fire ( TestEvent . toA );
위의 샘플 코드는 UUID를 식별자로 사용하여 상태 머신 인스턴스를 생성하고 자동 시작 기능을 비활성화하는 데 사용됩니다. StateMachineConfigure는 상태 머신 빌더에서 설정할 수도 있습니다. 이는 builder.newStateMachine(S initialStateId)
또는 builder.newStateMachine(S initialStateId, Object... extraParams)
에 의해 생성된 모든 상태 머신 인스턴스가 이 구성을 사용함을 의미합니다.
상태 머신 진단
StateMachineLogger 는 실행 성능, 작업 호출 시퀀스, 전환 진행 등과 같은 상태 시스템의 내부 상태를 관찰하는 데 사용됩니다.
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 .
...
v0.3.0 상태 머신 로거는 StateMachineConfiguration 활성화 디버그 모드를 설정하여 더 쉽게 사용할 수 있으므로 예를 들어
StateMachine<?,?,?,?> stateMachine = builder.newStateMachine(HState.A,
StateMachineConfiguration.create().enableDebugMode(true),
new Object[0]);
StateMachinePerformanceMonitor는 총 전환 시간 수, 평균 전환 소비 시간 등을 포함한 상태 머신 실행 성능 정보를 모니터링하는 데 사용할 수 있습니다.
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 ==========================
작업 메서드에 @LogExecTime을 추가하면 메서드 실행 시간이 로그아웃됩니다. 또한 상태 머신 클래스에 @LogExecTime을 추가하면 모든 작업 메서드 실행 시간이 로그아웃됩니다. 예를 들어 transitFromAToBOnGoToB 메서드의 실행 시간은 로그아웃됩니다.
@ LogExecTime
protected void transitFromAToBOnGoToB ( MyState from , MyState to , MyEvent event , MyContext context )
시간 제한 상태
시간 제한 상태 는 상태에 들어간 후 지정된 이벤트를 지연하거나 주기적으로 트리거할 수 있는 상태입니다. 시간이 지정된 작업은 ScheduledExecutorService 에 제출됩니다. 사용자는 SquirrelSingletonProvider 를 통해 ScheduledExecutorService 구현 인스턴스를 등록할 수 있습니다. 예:
ScheduledExecutorService scheduler = Executors . newScheduledThreadPool ( 1 );
SquirrelSingletonProvider . getInstance (). register ( ScheduledExecutorService . class , scheduler );
ScheduledExecutorService 인스턴스가 등록되지 않은 경우 SquirrelConfiguration은 기본 인스턴스를 제공합니다. 그 후에는 상태 머신 빌더가 시간 제한 상태를 정의할 수 있습니다. 예:
// after 50ms delay fire event "FIRST" every 100ms with null context
builder . defineTimedState ( "A" , 50 , 100 , "FIRST" , null );
builder . internalTransition (). within ( "A" ). on ( "FIRST" );
참고 : 전환 또는 시작/종료 동작을 설명하기 전에 시간 제한 상태를 정의해야 합니다. 0보다 작거나 같은 timeInterval은 initialDelay 이후에 한 번만 실행되는 것으로 간주됩니다.
연결된 상태(하위 머신 상태라고도 함)
연결된 상태는 하위 시스템 상태 기계의 사양 삽입을 지정합니다. 연결된 상태를 포함하는 상태 기계를 포함 상태 기계라고 합니다. 동일한 상태 기계는 단일 포함 상태 기계의 맥락에서 두 번 이상 하위 기계일 수 있습니다.
연결된 상태는 의미상 복합 상태와 동일합니다. 하위 머신 상태 머신의 영역은 복합 상태의 영역입니다. 시작, 종료 및 동작 작업과 내부 전환은 상태의 일부로 정의됩니다. 하위 머신 상태는 일반적인 동작과 재사용을 고려하는 분해 메커니즘입니다. 연결된 상태는 다음 샘플 코드로 정의할 수 있습니다.
builderOfTestStateMachine . definedLinkedState ( LState . A , builderOfLinkedStateMachine , LState . A1 );
JMX 지원
0.3.3부터 사용자는 상태 머신 인스턴스(예: 현재 상태, 이름)를 원격으로 모니터링하고 런타임에 구성을 수정할 수 있습니다(예: 로깅 전환/성능 모니터 전환/원격 실행 이벤트). 모든 상태 머신 인스턴스 정보는 "org.squirrelframework" 도메인 아래에 있습니다. 다음 샘플 코드는 JMX 지원을 활성화하는 방법을 보여줍니다.
UntypedStateMachineBuilder builder = StateMachineBuilderFactory . create (...);
builder . setStateMachineConfiguration ( StateMachineConfiguration . create (). enableRemoteMonitor ( true ));
참고 : JMX 기능 지원은 0.3.9-SNAPSHOT부터 더 이상 사용되지 않습니다.
예제 파일을 참조하세요.
릴리스 노트 파일을 참조하세요.
최신 업데이트를 보려면 내 트위터 @hhe11 또는 +HeHenry를 팔로우하세요.
토론이나 질문이 있으면 다람쥐 상태 머신 그룹에 가입하세요.
문제나 요구사항이 있으면 문제를 제출해 주세요.
애플리케이션에 Squirrel State Machine 코드를 사용하는 경우 작성자에게 다음과 같이 알려주시면 감사하겠습니다(이메일: [email protected]) .
제목: Squirrel State Machine 사용 알림 텍스트: 저는 <project_name> - http://link_to_project에서 Squirrel State Machine <lib_version>을 사용합니다. 나는 [허용 | 허용하지 않음] GitHub의 "Squirrel State Machine을 사용하는 사람" 섹션에서 내 프로젝트를 언급하는 것을 허용하지 않습니다.