diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configuration/StateMachineCommonConfiguration.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configuration/StateMachineCommonConfiguration.java index 2a5613e7..68ae8887 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configuration/StateMachineCommonConfiguration.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configuration/StateMachineCommonConfiguration.java @@ -42,4 +42,8 @@ public class StateMachineCommonConfiguration { return new ConcurrentTaskScheduler(); } + @Bean(name = StateMachineHandlerApplicationListener.BEAN_NAME) + public StateMachineHandlerApplicationListener stateMachineHandlerApplicationListener() { + return new StateMachineHandlerApplicationListener(); + } } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configuration/StateMachineHandlerApplicationListener.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configuration/StateMachineHandlerApplicationListener.java new file mode 100644 index 00000000..3c1d96df --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configuration/StateMachineHandlerApplicationListener.java @@ -0,0 +1,46 @@ +/* + * Copyright 2016 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.config.configuration; + +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextRefreshedEvent; + +/** + * Spring {@link ApplicationListener} which hooks to {@code ContextRefreshedEvent} + * and tracks when was a last time context was refreshed. + * + * @author Janne Valkealahti + * + */ +public class StateMachineHandlerApplicationListener implements ApplicationListener { + + public final static String BEAN_NAME = "stateMachineHandlerApplicationListener"; + private Long lastRefreshTime = null; + + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + lastRefreshTime = System.currentTimeMillis(); + } + + /** + * Gets the last refresh time. + * + * @return the last refresh time or {@code NULL} if not yet refreshed. + */ + public Long getLastRefreshTime() { + return lastRefreshTime; + } +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/processor/StateMachineHandlerCallHelper.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/processor/StateMachineHandlerCallHelper.java index e019ae59..51f6dc42 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/processor/StateMachineHandlerCallHelper.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/processor/StateMachineHandlerCallHelper.java @@ -44,6 +44,7 @@ import org.springframework.statemachine.annotation.OnTransition; import org.springframework.statemachine.annotation.OnTransitionEnd; import org.springframework.statemachine.annotation.OnTransitionStart; import org.springframework.statemachine.annotation.WithStateMachine; +import org.springframework.statemachine.config.configuration.StateMachineHandlerApplicationListener; import org.springframework.statemachine.state.State; import org.springframework.statemachine.support.StateMachineUtils; import org.springframework.util.Assert; @@ -64,6 +65,8 @@ public class StateMachineHandlerCallHelper implements InitializingBean, Be private final Log log = LogFactory.getLog(StateMachineHandlerCallHelper.class); private final Map> cache = new HashMap<>(); private ListableBeanFactory beanFactory; + private StateMachineHandlerApplicationListener stateMachineHandlerApplicationListener; + private long last = Long.MIN_VALUE; @SuppressWarnings("unchecked") @Override @@ -72,6 +75,10 @@ public class StateMachineHandlerCallHelper implements InitializingBean, Be log.info("Beanfactory is not instance of ListableBeanFactory, was " + beanFactory + " thus Disabling handlers."); return; } + if (beanFactory.containsBean(StateMachineHandlerApplicationListener.BEAN_NAME)) { + this.stateMachineHandlerApplicationListener = beanFactory.getBean(StateMachineHandlerApplicationListener.BEAN_NAME, + StateMachineHandlerApplicationListener.class); + } for (StateMachineHandler handler : beanFactory.getBeansOfType(StateMachineHandler.class).values()) { Annotation annotation = handler.getAnnotation(); Annotation metaAnnotation = handler.getMetaAnnotation(); @@ -98,7 +105,7 @@ public class StateMachineHandlerCallHelper implements InitializingBean, Be public void callOnStateChanged(String stateMachineId, StateContext stateContext) { List> handlersList = new ArrayList>(); String cacheKey = OnStateChanged.class.getName() + stateMachineId; - List list = cache.get(cacheKey); + List list = getCacheEntries(cacheKey); if (list == null) { return; } @@ -115,7 +122,7 @@ public class StateMachineHandlerCallHelper implements InitializingBean, Be public void callOnStateEntry(String stateMachineId, StateContext stateContext) { List> handlersList = new ArrayList>(); String cacheKey = OnStateEntry.class.getName() + stateMachineId; - List list = cache.get(cacheKey); + List list = getCacheEntries(cacheKey); if (list == null) { return; } @@ -132,7 +139,7 @@ public class StateMachineHandlerCallHelper implements InitializingBean, Be public void callOnStateExit(String stateMachineId, StateContext stateContext) { List> handlersList = new ArrayList>(); String cacheKey = OnStateExit.class.getName() + stateMachineId; - List list = cache.get(cacheKey); + List list = getCacheEntries(cacheKey); if (list == null) { return; } @@ -149,7 +156,7 @@ public class StateMachineHandlerCallHelper implements InitializingBean, Be public void callOnEventNotAccepted(String stateMachineId, StateContext stateContext) { List> handlersList = new ArrayList>(); String cacheKey = OnEventNotAccepted.class.getName() + stateMachineId; - List list = cache.get(cacheKey); + List list = getCacheEntries(cacheKey); if (list == null) { return; } @@ -170,7 +177,7 @@ public class StateMachineHandlerCallHelper implements InitializingBean, Be public void callOnTransitionStart(String stateMachineId, StateContext stateContext) { List> handlersList = new ArrayList>(); String cacheKey = OnTransitionStart.class.getName() + stateMachineId; - List list = cache.get(cacheKey); + List list = getCacheEntries(cacheKey); if (list == null) { return; } @@ -187,7 +194,7 @@ public class StateMachineHandlerCallHelper implements InitializingBean, Be public void callOnTransition(String stateMachineId, StateContext stateContext) { List> handlersList = new ArrayList>(); String cacheKey = OnTransition.class.getName() + stateMachineId; - List list = cache.get(cacheKey); + List list = getCacheEntries(cacheKey); if (list == null) { return; } @@ -204,7 +211,7 @@ public class StateMachineHandlerCallHelper implements InitializingBean, Be public void callOnTransitionEnd(String stateMachineId, StateContext stateContext) { List> handlersList = new ArrayList>(); String cacheKey = OnTransitionEnd.class.getName() + stateMachineId; - List list = cache.get(cacheKey); + List list = getCacheEntries(cacheKey); if (list == null) { return; } @@ -221,7 +228,7 @@ public class StateMachineHandlerCallHelper implements InitializingBean, Be public void callOnStateMachineStart(String stateMachineId, StateContext stateContext) { List> handlersList = new ArrayList>(); String cacheKey = OnStateMachineStart.class.getName() + stateMachineId; - List list = cache.get(cacheKey); + List list = getCacheEntries(cacheKey); if (list == null) { return; } @@ -234,7 +241,7 @@ public class StateMachineHandlerCallHelper implements InitializingBean, Be public void callOnStateMachineStop(String stateMachineId, StateContext stateContext) { List> handlersList = new ArrayList>(); String cacheKey = OnStateMachineStop.class.getName() + stateMachineId; - List list = cache.get(cacheKey); + List list = getCacheEntries(cacheKey); if (list == null) { return; } @@ -247,7 +254,7 @@ public class StateMachineHandlerCallHelper implements InitializingBean, Be public void callOnStateMachineError(String stateMachineId, StateContext stateContext) { List> handlersList = new ArrayList>(); String cacheKey = OnStateMachineError.class.getName() + stateMachineId; - List list = cache.get(cacheKey); + List list = getCacheEntries(cacheKey); if (list == null) { return; } @@ -260,7 +267,7 @@ public class StateMachineHandlerCallHelper implements InitializingBean, Be public void callOnExtendedStateChanged(String stateMachineId, Object key, Object value, StateContext stateContext) { List> handlersList = new ArrayList>(); String cacheKey = OnExtendedStateChanged.class.getName() + stateMachineId; - List list = cache.get(cacheKey); + List list = getCacheEntries(cacheKey); if (list == null) { return; } @@ -272,6 +279,24 @@ public class StateMachineHandlerCallHelper implements InitializingBean, Be getStateMachineHandlerResults(handlersList, stateContext); } + private synchronized List getCacheEntries(String cacheKey) { + if (stateMachineHandlerApplicationListener != null) { + Long l = stateMachineHandlerApplicationListener.getLastRefreshTime(); + if (l != null && l < System.currentTimeMillis() ) { + if (last != l) { + cache.clear(); + try { + afterPropertiesSet(); + } catch (Exception e) { + log.error("Unable to update handler cache", e); + } + last = l; + } + } + } + return cache.get(cacheKey); + } + private boolean annotationHandlerVariableMatch(Annotation annotation, Object key) { boolean handle = false; Map annotationAttributes = AnnotationUtils.getAnnotationAttributes(annotation); diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/processor/AnnotatedMethodTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/processor/AnnotatedMethodTests.java index ef3101e3..654e7687 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/processor/AnnotatedMethodTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/processor/AnnotatedMethodTests.java @@ -38,6 +38,7 @@ 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.StateMachineConfigurationConfigurer; import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; @@ -175,6 +176,24 @@ public class AnnotatedMethodTests extends AbstractStateMachineTests { assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates.S3)); } + @Test + @SuppressWarnings("unchecked") + public void testBeansCreatedAfterMachine() throws Exception { + // autostart is causing lifecycle before beans from BeanConfig1 + // are created. + context.register(Config7.class, BeanConfig1.class); + context.refresh(); + ObjectStateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); + Bean1 bean1 = context.getBean(Bean1.class); + machine.start(); + // S1 is transitioned during lifecycle start which happens + // before all beans are started, so onMethod0Latch is not called + assertThat(bean1.onMethod0Latch.await(2, TimeUnit.SECONDS), is(false)); + machine.sendEvent(TestEvents.E1); + assertThat(bean1.onMethod1Latch.await(2, TimeUnit.SECONDS), is(true)); + } + @WithStateMachine static class Bean1 { @@ -584,4 +603,35 @@ public class AnnotatedMethodTests extends AbstractStateMachineTests { } + @Configuration + @EnableStateMachine + static class Config7 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure( + StateMachineConfigurationConfigurer config) + throws Exception { + config + .withConfiguration() + .autoStartup(true); + } + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(TestStates.S1) + .states(EnumSet.allOf(TestStates.class)); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source(TestStates.S1) + .target(TestStates.S2) + .event(TestEvents.E1); + } + + } }