StateMachineAnnotationPostProcessor should extract correct annotated method

- Fix processing so that only expected annotations are
  processed and handlers created.
- Fixes #510
This commit is contained in:
jvalkeal
2018-02-21 15:17:17 +02:00
parent 08e1138aab
commit 3ce942fce3
3 changed files with 155 additions and 46 deletions

View File

@@ -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<? extends Annotation> 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<Class<? extends Annotation>, List<Annotation>> annotationChains = new HashMap<>();
for (Class<? extends Annotation> annotationType : postProcessors.keySet()) {
if (AnnotatedElementUtils.isAnnotated(method, annotationType.getName())) {
List<Annotation> annotationChain = getAnnotationChain(method, annotationType);
if (annotationChain.size() > 0) {
annotationChains.put(annotationType, annotationChain);
}
}
}
for (Entry<Class<? extends Annotation>, List<Annotation>> entry : annotationChains.entrySet()) {
Class<? extends Annotation> annotationType = entry.getKey();
List<Annotation> 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<Annotation> getAnnotationChain(Method method, Class<? extends Annotation> annotationType) {
Annotation[] annotations = AnnotationUtils.getAnnotations(method);
List<Annotation> annotationChain = new LinkedList<Annotation>();
Set<Annotation> visited = new HashSet<Annotation>();
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<? extends Annotation> annotationType, Annotation ann,
List<Annotation> annotationChain, Set<Annotation> 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;
}
}

View File

@@ -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<TestStates, TestEvents> states) throws Exception {
states

View File

@@ -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<TestStates, TestEvents> {
@@ -99,5 +140,4 @@ public class StateMachineAnnotationPostProcessorTests extends AbstractStateMachi
}
}
}