diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/processor/StateMachineAnnotationPostProcessor.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/processor/StateMachineAnnotationPostProcessor.java index a316f495..f4ea3852 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/processor/StateMachineAnnotationPostProcessor.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/processor/StateMachineAnnotationPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-2018 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. @@ -17,9 +17,13 @@ package org.springframework.statemachine.processor; import java.lang.annotation.Annotation; import java.lang.reflect.Method; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import org.apache.commons.logging.Log; @@ -37,6 +41,7 @@ import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; import org.springframework.context.Lifecycle; import org.springframework.context.SmartLifecycle; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.statemachine.annotation.OnEventNotAccepted; import org.springframework.statemachine.annotation.OnExtendedStateChanged; @@ -140,53 +145,69 @@ public class StateMachineAnnotationPostProcessor implements BeanPostProcessor, B return bean; } - ReflectionUtils.doWithMethods(beanClass, new ReflectionUtils.MethodCallback() { @SuppressWarnings({ "unchecked", "rawtypes" }) public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { - for (Class ppa : postProcessors.keySet()) { - Annotation metaAnnotation = AnnotationUtils.findAnnotation(method, ppa); - if (metaAnnotation == null) { - continue; - } - for (Annotation a : AnnotationUtils.getAnnotations(method)) { - MethodAnnotationPostProcessor postProcessor = metaAnnotation != null ? postProcessors - .get(metaAnnotation.annotationType()) : null; - if (postProcessor != null && shouldCreateHandler(a)) { - Object result = postProcessor.postProcess(beanClass, bean, beanName, method, metaAnnotation, a); - if (result != null && result instanceof StateMachineHandler) { - String endpointBeanName = generateBeanName(beanName, method, a.annotationType()); - if (result instanceof BeanNameAware) { - ((BeanNameAware) result).setBeanName(endpointBeanName); + Map, List> annotationChains = new HashMap<>(); + for (Class annotationType : postProcessors.keySet()) { + if (AnnotatedElementUtils.isAnnotated(method, annotationType.getName())) { + List annotationChain = getAnnotationChain(method, annotationType); + if (annotationChain.size() > 0) { + annotationChains.put(annotationType, annotationChain); + } + } + } + + for (Entry, List> entry : annotationChains.entrySet()) { + Class annotationType = entry.getKey(); + List annotations = entry.getValue(); + Annotation metaAnnotation = null; + Annotation annotation = null; + if (annotations.size() == 2) { + annotation = annotations.get(0); + metaAnnotation = annotations.get(1); + } else if (annotations.size() == 1) { + annotation = annotations.get(0); + metaAnnotation = annotations.get(0); + } + + MethodAnnotationPostProcessor postProcessor = metaAnnotation != null ? postProcessors.get(annotationType) : null; + if (postProcessor != null) { + // TODO: should change post processor to handle annotation list + Object result = postProcessor.postProcess(beanClass, bean, beanName, method, metaAnnotation, annotation); + if (result != null && result instanceof StateMachineHandler) { + String endpointBeanName = generateBeanName(beanName, method, annotation.annotationType()); + + if (result instanceof BeanNameAware) { + ((BeanNameAware) result).setBeanName(endpointBeanName); + } + beanFactory.registerSingleton(endpointBeanName, result); + if (result instanceof BeanFactoryAware) { + ((BeanFactoryAware) result).setBeanFactory(beanFactory); + } + if (result instanceof InitializingBean) { + try { + ((InitializingBean) result).afterPropertiesSet(); + } catch (Exception e) { + throw new BeanInitializationException("failed to initialize annotated component", e); } - beanFactory.registerSingleton(endpointBeanName, result); - if (result instanceof BeanFactoryAware) { - ((BeanFactoryAware) result).setBeanFactory(beanFactory); - } - if (result instanceof InitializingBean) { - try { - ((InitializingBean) result).afterPropertiesSet(); - } catch (Exception e) { - throw new BeanInitializationException("failed to initialize annotated component", e); - } - } - if (result instanceof Lifecycle) { - lifecycles.add((Lifecycle) result); - if (result instanceof SmartLifecycle && ((SmartLifecycle) result).isAutoStartup()) { - ((SmartLifecycle) result).start(); - } - } - if (result instanceof ApplicationListener) { - listeners.add((ApplicationListener) result); + } + if (result instanceof Lifecycle) { + lifecycles.add((Lifecycle) result); + if (result instanceof SmartLifecycle && ((SmartLifecycle) result).isAutoStartup()) { + ((SmartLifecycle) result).start(); } } + if (result instanceof ApplicationListener) { + listeners.add((ApplicationListener) result); + } } } } } - }); + }, ReflectionUtils.USER_DECLARED_METHODS); return bean; } @@ -229,10 +250,6 @@ public class StateMachineAnnotationPostProcessor implements BeanPostProcessor, B this.running = false; } - private boolean shouldCreateHandler(Annotation annotation) { - return true; - } - /** * Gets the bean class. Will check if bean is a proxy and * find a class from there as target class, otherwise @@ -256,4 +273,38 @@ public class StateMachineAnnotationPostProcessor implements BeanPostProcessor, B return name; } + + private List getAnnotationChain(Method method, Class annotationType) { + Annotation[] annotations = AnnotationUtils.getAnnotations(method); + List annotationChain = new LinkedList(); + Set visited = new HashSet(); + for (Annotation ann : annotations) { + this.recursiveFindAnnotation(annotationType, ann, annotationChain, visited); + if (annotationChain.size() > 0) { + Collections.reverse(annotationChain); + return annotationChain; + } + } + return annotationChain; + } + + private boolean recursiveFindAnnotation(Class annotationType, Annotation ann, + List annotationChain, Set visited) { + if (ann.annotationType().equals(annotationType)) { + annotationChain.add(ann); + return true; + } + for (Annotation metaAnn : ann.annotationType().getAnnotations()) { + if (!ann.equals(metaAnn) && !visited.contains(metaAnn) + && !(metaAnn.annotationType().getPackage().getName().startsWith("java.lang"))) { + visited.add(metaAnn); // prevent infinite recursion if the same + // annotation is found again + if (this.recursiveFindAnnotation(annotationType, metaAnn, annotationChain, visited)) { + annotationChain.add(ann); + return true; + } + } + } + return false; + } } 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 654e7687..9f2b0714 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 @@ -1,5 +1,5 @@ /* - * Copyright 2015-2016 the original author or authors. + * Copyright 2015-2018 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. @@ -41,6 +41,7 @@ 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; +import org.springframework.transaction.annotation.Transactional; public class AnnotatedMethodTests extends AbstractStateMachineTests { @@ -83,6 +84,8 @@ public class AnnotatedMethodTests extends AbstractStateMachineTests { assertThat(bean1.onMethod5Latch.await(2, TimeUnit.SECONDS), is(true)); assertThat(bean1.onMethod7Latch.await(2, TimeUnit.SECONDS), is(true)); assertThat(bean1.onMethod9Latch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(bean1.onMethod10Latch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(bean1.onMethod10Count, is(1)); } @Test @@ -105,6 +108,8 @@ public class AnnotatedMethodTests extends AbstractStateMachineTests { assertThat(bean1.onMethod5Latch.await(2, TimeUnit.SECONDS), is(true)); assertThat(bean1.onMethod7Latch.await(2, TimeUnit.SECONDS), is(true)); assertThat(bean1.onMethod9Latch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(bean1.onMethod10Latch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(bean1.onMethod10Count, is(1)); } @Test @@ -127,6 +132,8 @@ public class AnnotatedMethodTests extends AbstractStateMachineTests { assertThat(bean1.onMethod5Latch.await(2, TimeUnit.SECONDS), is(true)); assertThat(bean1.onMethod7Latch.await(2, TimeUnit.SECONDS), is(true)); assertThat(bean1.onMethod9Latch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(bean1.onMethod10Latch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(bean1.onMethod10Count, is(1)); } @Test @@ -143,6 +150,8 @@ public class AnnotatedMethodTests extends AbstractStateMachineTests { assertThat(bean1.onMethod8Latch.await(2, TimeUnit.SECONDS), is(true)); machine.sendEvent(TestEvents.E3); assertThat(bean1.onMethod9Latch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(bean1.onMethod10Latch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(bean1.onMethod10Count, is(1)); } @Test @@ -193,7 +202,7 @@ public class AnnotatedMethodTests extends AbstractStateMachineTests { machine.sendEvent(TestEvents.E1); assertThat(bean1.onMethod1Latch.await(2, TimeUnit.SECONDS), is(true)); } - + @WithStateMachine static class Bean1 { @@ -207,6 +216,8 @@ public class AnnotatedMethodTests extends AbstractStateMachineTests { CountDownLatch onMethod7Latch = new CountDownLatch(1); CountDownLatch onMethod8Latch = new CountDownLatch(1); CountDownLatch onMethod9Latch = new CountDownLatch(1); + CountDownLatch onMethod10Latch = new CountDownLatch(1); + volatile int onMethod10Count; CountDownLatch onOnTransitionFromS2ToS3Latch = new CountDownLatch(1); @OnTransition(target = "S1") @@ -259,6 +270,13 @@ public class AnnotatedMethodTests extends AbstractStateMachineTests { onMethod9Latch.countDown(); } + @StatesOnTransition(target = TestStates.S30) + @Transactional + public void method10() { + onMethod10Count++; + onMethod10Latch.countDown(); + } + @OnTransition public void onTransitionFromS2ToS3() { onOnTransitionFromS2ToS3Latch.countDown(); @@ -615,7 +633,7 @@ public class AnnotatedMethodTests extends AbstractStateMachineTests { .withConfiguration() .autoStartup(true); } - + @Override public void configure(StateMachineStateConfigurer states) throws Exception { states diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/processor/StateMachineAnnotationPostProcessorTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/processor/StateMachineAnnotationPostProcessorTests.java index b7887dd5..33d793d4 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/processor/StateMachineAnnotationPostProcessorTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/processor/StateMachineAnnotationPostProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-2018 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. @@ -15,6 +15,9 @@ */ package org.springframework.statemachine.processor; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + import java.util.EnumSet; import java.util.concurrent.CountDownLatch; @@ -30,6 +33,7 @@ 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; +import org.springframework.transaction.annotation.Transactional; public class StateMachineAnnotationPostProcessorTests extends AbstractStateMachineTests { @@ -39,9 +43,17 @@ public class StateMachineAnnotationPostProcessorTests extends AbstractStateMachi } @Test - public void testWithOtherAnnotations() { + public void testWithNormalAnnotation() { context.register(Config1.class, BeanConfig1.class); context.refresh(); + assertThat(context.getBeansOfType(StateMachineHandler.class).size(), is(2)); + } + + @Test + public void testWithNormalAnnotationWithTransactional() { + context.register(Config1.class, BeanConfig2.class); + context.refresh(); + assertThat(context.getBeansOfType(StateMachineHandler.class).size(), is(1)); } @WithStateMachine @@ -67,6 +79,25 @@ public class StateMachineAnnotationPostProcessorTests extends AbstractStateMachi } + @WithStateMachine + static class Bean2 { + + CountDownLatch onMethod1Latch = new CountDownLatch(1); + CountDownLatch onOnTransitionFromS2ToS3Latch = new CountDownLatch(1); + + @OnTransition(source = "S1", target = "S2") + @Transactional + public void method1() { + onMethod1Latch.countDown(); + } + + @Bean + public String dummy() { + return "dummy"; + } + + } + @Configuration static class BeanConfig1 { @@ -77,6 +108,16 @@ public class StateMachineAnnotationPostProcessorTests extends AbstractStateMachi } + @Configuration + static class BeanConfig2 { + + @Bean + public Bean2 bean2() { + return new Bean2(); + } + + } + @Configuration @EnableStateMachine(name = {StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, "fooMachine"}) static class Config1 extends EnumStateMachineConfigurerAdapter { @@ -99,5 +140,4 @@ public class StateMachineAnnotationPostProcessorTests extends AbstractStateMachi } } - }