Association with @EnableStateMachine and @WithStateMachine

- Fix bug where multiple machines and beans with @WithStateMachine
  didn't properly separate calling methods with @OnTransition.
- Some polish
- Fixes #116
This commit is contained in:
Janne Valkealahti
2015-11-05 13:46:13 +00:00
parent 9304ddf285
commit 58e9f01aaf
12 changed files with 278 additions and 24 deletions

View File

@@ -25,6 +25,7 @@ import java.util.Map.Entry;
import java.util.Stack;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.context.SmartLifecycle;
import org.springframework.core.task.TaskExecutor;
import org.springframework.messaging.Message;
@@ -77,7 +78,7 @@ import org.springframework.util.ObjectUtils;
* @param <E> the type of event
*/
public abstract class AbstractStateMachineFactory<S, E> extends LifecycleObjectSupport implements
StateMachineFactory<S, E> {
StateMachineFactory<S, E>, BeanNameAware {
private final StateMachineTransitions<S, E> stateMachineTransitions;
@@ -89,6 +90,8 @@ public abstract class AbstractStateMachineFactory<S, E> extends LifecycleObjectS
private boolean handleAutostartup = false;
private String beanName;
/**
* Instantiates a new enum state machine factory.
*
@@ -102,6 +105,11 @@ public abstract class AbstractStateMachineFactory<S, E> extends LifecycleObjectS
this.stateMachineTransitions = stateMachineTransitions;
this.stateMachineStates = stateMachineStates;
}
@Override
public void setBeanName(String name) {
this.beanName = name;
}
@SuppressWarnings("unchecked")
@Override
@@ -178,7 +186,7 @@ public abstract class AbstractStateMachineFactory<S, E> extends LifecycleObjectS
Transition<S, E> initialTransition = new InitialTransition<S, E>(rstate);
StateMachine<S, E> m = buildStateMachineInternal(states, new ArrayList<Transition<S, E>>(), rstate,
initialTransition, null, defaultExtendedState, null, contextEvents, resolveBeanFactory(),
resolveTaskExecutor(), resolveTaskScheduler());
resolveTaskExecutor(), resolveTaskScheduler(), beanName);
machine = m;
}
} else {
@@ -196,7 +204,7 @@ public abstract class AbstractStateMachineFactory<S, E> extends LifecycleObjectS
if (machine instanceof LifecycleObjectSupport) {
((LifecycleObjectSupport)machine).setAutoStartup(stateMachineConfigurationConfig.isAutoStart());
}
// set top-level machine as relay
final StateMachine<S, E> fmachine = machine;
fmachine.getStateMachineAccessor().doWithAllRegions(new StateMachineFunction<StateMachineAccess<S, E>>() {
@@ -536,8 +544,9 @@ public abstract class AbstractStateMachineFactory<S, E> extends LifecycleObjectS
}
Transition<S, E> initialTransition = new InitialTransition<S, E>(initialState, initialAction);
StateMachine<S, E> machine = buildStateMachineInternal(states, transitions, initialState, initialTransition, null,
defaultExtendedState, historyState, contextEvents, beanFactory, taskExecutor, taskScheduler);
StateMachine<S, E> machine = buildStateMachineInternal(states, transitions, initialState, initialTransition,
null, defaultExtendedState, historyState, contextEvents, beanFactory, taskExecutor, taskScheduler,
beanName);
return machine;
}
@@ -545,7 +554,7 @@ public abstract class AbstractStateMachineFactory<S, E> extends LifecycleObjectS
Collection<Transition<S, E>> transitions, State<S, E> initialState, Transition<S, E> initialTransition,
Message<E> initialEvent, ExtendedState extendedState, PseudoState<S, E> historyState,
Boolean contextEventsEnabled, BeanFactory beanFactory, TaskExecutor taskExecutor,
TaskScheduler taskScheduler);
TaskScheduler taskScheduler, String beanName);
protected abstract State<S, E> buildStateInternal(S id, Collection<E> deferred, Collection<? extends Action<S, E>> entryActions, Collection<? extends Action<S, E>> exitActions,
PseudoState<S, E> pseudoState);

View File

@@ -18,6 +18,7 @@ package org.springframework.statemachine.config;
import java.util.Collection;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.core.task.TaskExecutor;
import org.springframework.messaging.Message;
import org.springframework.scheduling.TaskScheduler;
@@ -56,7 +57,7 @@ public class ObjectStateMachineFactory<S, E> extends AbstractStateMachineFactory
Collection<Transition<S, E>> transitions, State<S, E> initialState, Transition<S, E> initialTransition,
Message<E> initialEvent, ExtendedState extendedState, PseudoState<S, E> historyState,
Boolean contextEventsEnabled, BeanFactory beanFactory, TaskExecutor taskExecutor,
TaskScheduler taskScheduler) {
TaskScheduler taskScheduler, String beanName) {
ObjectStateMachine<S, E> machine = new ObjectStateMachine<S, E>(states, transitions, initialState, initialTransition, initialEvent,
extendedState);
machine.setHistoryState(historyState);
@@ -72,6 +73,9 @@ public class ObjectStateMachineFactory<S, E> extends AbstractStateMachineFactory
if (taskScheduler != null) {
machine.setTaskScheduler(taskScheduler);;
}
if (machine instanceof BeanNameAware) {
((BeanNameAware)machine).setBeanName(beanName);
}
machine.afterPropertiesSet();
return machine;
}

View File

@@ -21,7 +21,7 @@ import org.springframework.statemachine.annotation.WithStateMachine;
import org.springframework.statemachine.processor.StateMachineAnnotationPostProcessor;
/**
* Configuration for annotation port processor which is needed i.e. when
* Configuration for annotation post processor which is needed i.e. when
* {@link WithStateMachine} is used.
*
* @author Janne Valkealahti

View File

@@ -19,6 +19,7 @@ import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.context.SmartLifecycle;
@@ -68,11 +69,12 @@ public class StateMachineConfiguration<S extends Enum<S>, E extends Enum<E>> ext
private static class StateMachineDelegatingFactoryBean<S extends Enum<S>, E extends Enum<E>>
extends BeanDelegatingFactoryBean<StateMachine<S, E>,StateMachineConfigBuilder<S, E>,StateMachineConfig<S, E>>
implements SmartLifecycle {
implements SmartLifecycle, BeanNameAware {
private String clazzName;
private Boolean contextEvents;
private SmartLifecycle lifecycle;
private String beanName;
public StateMachineDelegatingFactoryBean(StateMachineConfigBuilder<S, E> builder, Class<StateMachine<S, E>> clazz,
String clazzName, Boolean contextEvents) {
@@ -80,6 +82,11 @@ public class StateMachineConfiguration<S extends Enum<S>, E extends Enum<E>> ext
this.clazzName = clazzName;
this.contextEvents = contextEvents;
}
@Override
public void setBeanName(String name) {
this.beanName = name;
}
@Override
public void afterPropertiesSet() throws Exception {
@@ -97,6 +104,7 @@ public class StateMachineConfiguration<S extends Enum<S>, E extends Enum<E>> ext
stateMachineConfigurationConfig, stateMachineTransitions, stateMachineStates);
stateMachineFactory.setBeanFactory(getBeanFactory());
stateMachineFactory.setContextEventsEnabled(contextEvents);
stateMachineFactory.setBeanName(beanName);
StateMachine<S, E> stateMachine = stateMachineFactory.getStateMachine();
this.lifecycle = (SmartLifecycle) stateMachine;
setObject(stateMachine);

View File

@@ -34,6 +34,7 @@ public interface MethodAnnotationPostProcessor<T extends Annotation> {
* <code>null</code>. Caller of this method is then responsible to handle
* newly created object.
*
* @param beanClass the bean class
* @param bean the bean
* @param beanName the bean name
* @param method the method
@@ -41,6 +42,6 @@ public interface MethodAnnotationPostProcessor<T extends Annotation> {
* @param annotation the annotation
* @return the post processed object
*/
Object postProcess(Object bean, String beanName, Method method, T metaAnnotation, Annotation annotation);
Object postProcess(Class<?> beanClass, Object bean, String beanName, Method method, T metaAnnotation, Annotation annotation);
}

View File

@@ -40,8 +40,8 @@ public class StateMachineActivatorAnnotationPostProcessor implements MethodAnnot
}
@Override
public Object postProcess(Object bean, String beanName, Method method, OnTransition metaAnnotation, Annotation annotation) {
StateMachineHandler<Object, Object> handler = new StateMachineOnTransitionHandler<Object, Object>(bean, method, metaAnnotation, annotation);
public Object postProcess(Class<?> beanClass, Object bean, String beanName, Method method, OnTransition metaAnnotation, Annotation annotation) {
StateMachineHandler<Object, Object> handler = new StateMachineOnTransitionHandler<Object, Object>(beanClass, bean, method, metaAnnotation, annotation);
Integer order = findOrder(bean, method);
if (order != null) {

View File

@@ -133,7 +133,7 @@ public class StateMachineAnnotationPostProcessor implements BeanPostProcessor, B
.get(metaAnnotation.annotationType()) : null;
if (postProcessor != null && shouldCreateHandler(annotation)) {
Object result = postProcessor.postProcess(bean, beanName, method, metaAnnotation, annotation);
Object result = postProcessor.postProcess(beanClass, bean, beanName, method, metaAnnotation, annotation);
if (result != null && result instanceof StateMachineHandler) {
String endpointBeanName = generateBeanName(beanName, method, annotation.annotationType());

View File

@@ -33,6 +33,8 @@ import org.springframework.statemachine.annotation.WithStateMachine;
*/
public class StateMachineHandler<S, E> implements Ordered {
private final Class<?> beanClass;
private final StateMachineRuntimeProcessor<?, S, E> processor;
private int order = Ordered.LOWEST_PRECEDENCE;
@@ -40,30 +42,34 @@ public class StateMachineHandler<S, E> implements Ordered {
/**
* Instantiates a new container handler.
*
* @param beanClass the bean class
* @param target the target bean
* @param method the method
*/
public StateMachineHandler(Object target, Method method) {
this(new MethodInvokingStateMachineRuntimeProcessor<Object, S, E>(target, method));
public StateMachineHandler(Class<?> beanClass, Object target, Method method) {
this(beanClass, new MethodInvokingStateMachineRuntimeProcessor<Object, S, E>(target, method));
}
/**
* Instantiates a new container handler.
*
* @param beanClass the bean class
* @param target the target bean
* @param methodName the method name
*/
public StateMachineHandler(Object target, String methodName) {
this(new MethodInvokingStateMachineRuntimeProcessor<Object, S, E>(target, methodName));
public StateMachineHandler(Class<?> beanClass, Object target, String methodName) {
this(beanClass, new MethodInvokingStateMachineRuntimeProcessor<Object, S, E>(target, methodName));
}
/**
* Instantiates a new container handler.
*
* @param <T> the generic type
* @param beanClass the bean class
* @param processor the processor
*/
public <T> StateMachineHandler(MethodInvokingStateMachineRuntimeProcessor<T, S, E> processor) {
public <T> StateMachineHandler(Class<?> beanClass, MethodInvokingStateMachineRuntimeProcessor<T, S, E> processor) {
this.beanClass = beanClass;
this.processor = processor;
}
@@ -71,6 +77,15 @@ public class StateMachineHandler<S, E> implements Ordered {
public int getOrder() {
return order;
}
/**
* Gets the bean class.
*
* @return the bean class
*/
public Class<?> getBeanClass() {
return beanClass;
}
/**
* Sets the order used get value from {@link #getOrder()}.

View File

@@ -36,13 +36,14 @@ public class StateMachineOnTransitionHandler<S, E> extends StateMachineHandler<S
/**
* Instantiates a new state machine on transition handler.
*
* @param beanClass the bean class
* @param target the target
* @param method the method
* @param metaAnnotation the meta annotation
* @param annotation the annotation
*/
public StateMachineOnTransitionHandler(Object target, Method method, OnTransition metaAnnotation, Annotation annotation) {
super(target, method);
public StateMachineOnTransitionHandler(Class<?> beanClass, Object target, Method method, OnTransition metaAnnotation, Annotation annotation) {
super(beanClass, target, method);
this.metaAnnotation = metaAnnotation;
this.annotation = annotation;
}

View File

@@ -44,6 +44,7 @@ import org.springframework.statemachine.access.StateMachineAccess;
import org.springframework.statemachine.access.StateMachineAccessor;
import org.springframework.statemachine.access.StateMachineFunction;
import org.springframework.statemachine.annotation.OnTransition;
import org.springframework.statemachine.annotation.WithStateMachine;
import org.springframework.statemachine.listener.StateMachineListener;
import org.springframework.statemachine.processor.StateMachineHandler;
import org.springframework.statemachine.processor.StateMachineOnTransitionHandler;
@@ -64,6 +65,7 @@ import org.springframework.statemachine.transition.TransitionKind;
import org.springframework.statemachine.trigger.DefaultTriggerContext;
import org.springframework.statemachine.trigger.Trigger;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
@@ -890,9 +892,9 @@ public abstract class AbstractStateMachine<S, E> extends StateMachineObjectSuppo
Assert.state(beanFactory instanceof ListableBeanFactory, "Bean factory must be instance of ListableBeanFactory");
if (!handlersInitialized) {
Map<String, StateMachineOnTransitionHandler> handlersx = ((ListableBeanFactory) beanFactory)
Map<String, StateMachineOnTransitionHandler> handlersMap = ((ListableBeanFactory) beanFactory)
.getBeansOfType(StateMachineOnTransitionHandler.class);
for (Entry<String, StateMachineOnTransitionHandler> entry : handlersx.entrySet()) {
for (Entry<String, StateMachineOnTransitionHandler> entry : handlersMap.entrySet()) {
handlers.put(entry.getKey(), entry.getValue());
}
handlersInitialized = true;
@@ -901,9 +903,13 @@ public abstract class AbstractStateMachine<S, E> extends StateMachineObjectSuppo
List<StateMachineHandler<S, E>> handlersList = new ArrayList<StateMachineHandler<S, E>>();
for (Entry<String, StateMachineOnTransitionHandler<S, E>> entry : handlers.entrySet()) {
// add only matching names from beanName and WithStateMachine name field
WithStateMachine withStateMachine = AnnotationUtils.findAnnotation(entry.getValue().getBeanClass(), WithStateMachine.class);
if (withStateMachine == null || !ObjectUtils.nullSafeEquals(withStateMachine.name(), getBeanName())) {
continue;
}
OnTransition metaAnnotation = entry.getValue().getMetaAnnotation();
Annotation annotation = entry.getValue().getAnnotation();
if (transitionHandlerMatch(metaAnnotation, annotation, sourceState, targetState)) {
handlersList.add(entry.getValue());
}

View File

@@ -20,6 +20,7 @@ import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.core.OrderComparator;
import org.springframework.messaging.Message;
import org.springframework.statemachine.StateMachine;
@@ -38,7 +39,7 @@ import org.springframework.util.Assert;
* @param <S> the type of state
* @param <E> the type of event
*/
public abstract class StateMachineObjectSupport<S, E> extends LifecycleObjectSupport {
public abstract class StateMachineObjectSupport<S, E> extends LifecycleObjectSupport implements BeanNameAware {
private static final Log log = LogFactory.getLog(StateMachineObjectSupport.class);
@@ -52,7 +53,24 @@ public abstract class StateMachineObjectSupport<S, E> extends LifecycleObjectSup
private final StateMachineInterceptorList<S, E> interceptors =
new StateMachineInterceptorList<S, E>();
private String beanName;
@Override
public void setBeanName(String name) {
beanName = name;
}
/**
* Returns a bean name known to context per contract
* with {@link BeanNameAware}.
*
* @return a bean name
*/
protected String getBeanName() {
return beanName;
}
/**
* Gets the state machine event publisher.
*

View File

@@ -0,0 +1,192 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.statemachine.annotation;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.statemachine.AbstractStateMachineTests;
import org.springframework.statemachine.ObjectStateMachine;
import org.springframework.statemachine.annotation.OnTransition;
import org.springframework.statemachine.annotation.WithStateMachine;
import org.springframework.statemachine.config.EnableStateMachine;
import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter;
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
public class ClassAnnotationTests extends AbstractStateMachineTests {
@Test
@SuppressWarnings("unchecked")
public void testClassAnnotations() throws Exception {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(BaseConfig.class, BeanConfig.class, FooConfig.class, BarConfig.class);
ObjectStateMachine<TestStates,TestEvents> fooMachine =
context.getBean("fooMachine", ObjectStateMachine.class);
ObjectStateMachine<TestStates,TestEvents> barMachine =
context.getBean("barMachine", ObjectStateMachine.class);
assertThat(context.containsBean("fooMachine"), is(true));
assertThat(context.containsBean("barMachine"), is(true));
fooMachine.start();
barMachine.start();
FooBean fooBean = context.getBean(FooBean.class);
BarBean barBean = context.getBean(BarBean.class);
fooBean.resetMethodLatch();
// this event should cause 'FooBean.fooMethod' to get called
fooMachine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).build());
assertThat(fooBean.onFooMethodLatch.await(2, TimeUnit.SECONDS), is(true));
assertThat(barBean.onBarMethodLatch.await(2, TimeUnit.SECONDS), is(false));
fooBean.resetMethodLatch();
// this event should cause 'BarBean.barMethod' to get called
barMachine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).build());
assertThat(fooBean.onFooMethodLatch.await(2, TimeUnit.SECONDS), is(false));
assertThat(barBean.onBarMethodLatch.await(2, TimeUnit.SECONDS), is(true));
context.close();
}
@WithStateMachine(name = "fooMachine")
static class FooBean {
CountDownLatch onFooMethodLatch;
public void resetMethodLatch() {
onFooMethodLatch = new CountDownLatch(1);
}
@OnTransition(source = "S1", target = "S2")
public void fooMethod() {
onFooMethodLatch.countDown();
}
}
@WithStateMachine(name = "barMachine")
static class BarBean {
CountDownLatch onBarMethodLatch = new CountDownLatch(1);
@OnTransition(source = "S1", target = "S2")
public void barMethod() {
onBarMethodLatch.countDown();
}
}
@Configuration
static class BeanConfig {
@Bean
public FooBean fooBean() {
return new FooBean();
}
@Bean
public BarBean barBean() {
return new BarBean();
}
}
@Configuration
@EnableStateMachine(name = "fooMachine")
static class FooConfig extends EnumStateMachineConfigurerAdapter<TestStates, TestEvents> {
@Override
public void configure(StateMachineStateConfigurer<TestStates, TestEvents> states) throws Exception {
states
.withStates()
.initial(TestStates.S1)
.state(TestStates.S2);
}
@Override
public void configure(StateMachineTransitionConfigurer<TestStates, TestEvents> transitions) throws Exception {
transitions
.withExternal()
.source(TestStates.S1)
.target(TestStates.S2)
.event(TestEvents.E1)
.guard(testGuard())
.action(testAction());
}
@Bean
public TestGuard testGuard() {
return new TestGuard();
}
@Bean
public TestAction testAction() {
return new TestAction();
}
}
@Configuration
@EnableStateMachine(name = "barMachine")
static class BarConfig extends EnumStateMachineConfigurerAdapter<TestStates, TestEvents> {
@Override
public void configure(StateMachineStateConfigurer<TestStates, TestEvents> states) throws Exception {
states
.withStates()
.initial(TestStates.S1)
.state(TestStates.S2);
}
@Override
public void configure(StateMachineTransitionConfigurer<TestStates, TestEvents> transitions) throws Exception {
transitions
.withExternal()
.source(TestStates.S1)
.target(TestStates.S2)
.event(TestEvents.E1)
.guard(testGuard())
.action(testAction());
}
@Bean
public TestGuard testGuard() {
return new TestGuard();
}
@Bean
public TestAction testAction() {
return new TestAction();
}
}
}