From 13893609501b7579c7633e4eea8c87ba95543db5 Mon Sep 17 00:00:00 2001 From: Janne Valkealahti Date: Fri, 10 Feb 2017 11:50:28 +0000 Subject: [PATCH] Better generics handling with adapters - Modify StateMachineConfiguration and StateMachineFactoryConfiguration so that if you have different states and events classes with multiple machines or factories, you can autowire by types without adding qualifiers. - Manually try to discover generic types from enclosing adapther class and if succesfull pass that information into bean definition as ResolvableType. - Fixes #306 --- .../StateMachineConfiguration.java | 27 +- .../StateMachineFactoryConfiguration.java | 33 ++- .../config/MachineTypedTests.java | 255 ++++++++++++++++++ 3 files changed, 311 insertions(+), 4 deletions(-) create mode 100644 spring-statemachine-core/src/test/java/org/springframework/statemachine/config/MachineTypedTests.java diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configuration/StateMachineConfiguration.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configuration/StateMachineConfiguration.java index ba1b36b5..8e8396b9 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configuration/StateMachineConfiguration.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configuration/StateMachineConfiguration.java @@ -24,10 +24,13 @@ import org.springframework.beans.factory.BeanNameAware; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.SmartLifecycle; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Scope; +import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.type.AnnotationMetadata; import org.springframework.statemachine.StateMachine; @@ -39,8 +42,8 @@ import org.springframework.statemachine.config.builders.StateMachineConfigBuilde import org.springframework.statemachine.config.builders.StateMachineConfigurer; import org.springframework.statemachine.config.common.annotation.AbstractImportingAnnotationConfiguration; import org.springframework.statemachine.config.common.annotation.AnnotationConfigurer; -import org.springframework.statemachine.config.model.DefaultStateMachineModel; import org.springframework.statemachine.config.model.ConfigurationData; +import org.springframework.statemachine.config.model.DefaultStateMachineModel; import org.springframework.statemachine.config.model.StatesData; import org.springframework.statemachine.config.model.TransitionsData; import org.springframework.statemachine.monitor.StateMachineMonitor; @@ -97,7 +100,15 @@ public class StateMachineConfiguration extends beanDefinitionBuilder.addConstructorArgValue(StateMachine.class); beanDefinitionBuilder.addConstructorArgValue(importingClassMetadata.getClassName()); beanDefinitionBuilder.addConstructorArgValue(contextEvents); - return beanDefinitionBuilder.getBeanDefinition(); + + AbstractBeanDefinition beanDefinition = beanDefinitionBuilder.getBeanDefinition(); + + // try to add more info about generics + ResolvableType type = resolveFactoryObjectType(enableStateMachineEnclosingClass); + if (type != null && beanDefinition instanceof RootBeanDefinition) { + ((RootBeanDefinition)beanDefinition).setTargetType(type); + } + return beanDefinition; } @Override @@ -107,6 +118,18 @@ public class StateMachineConfiguration extends return types; } + private ResolvableType resolveFactoryObjectType(Class enableStateMachineEnclosingClass) { + ResolvableType type = null; + try { + Class[] generics = ResolvableType.forClass(enableStateMachineEnclosingClass).getSuperType().resolveGenerics(); + if (generics != null && generics.length == 2) { + type = ResolvableType.forClassWithGenerics(StateMachine.class, generics); + } + } catch (Exception e) { + } + return type; + } + private static class StateMachineDelegatingFactoryBean extends BeanDelegatingFactoryBean,StateMachineConfigBuilder,StateMachineConfig> implements SmartLifecycle, BeanNameAware { diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configuration/StateMachineFactoryConfiguration.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configuration/StateMachineFactoryConfiguration.java index dd2683a3..f3cb0953 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configuration/StateMachineFactoryConfiguration.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configuration/StateMachineFactoryConfiguration.java @@ -27,8 +27,11 @@ import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.annotation.Configuration; +import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.type.AnnotationMetadata; import org.springframework.statemachine.config.EnableStateMachineFactory; @@ -39,8 +42,8 @@ import org.springframework.statemachine.config.StateMachineFactory; import org.springframework.statemachine.config.builders.StateMachineConfigBuilder; import org.springframework.statemachine.config.common.annotation.AbstractImportingAnnotationConfiguration; import org.springframework.statemachine.config.common.annotation.AnnotationConfigurer; -import org.springframework.statemachine.config.model.DefaultStateMachineModel; import org.springframework.statemachine.config.model.ConfigurationData; +import org.springframework.statemachine.config.model.DefaultStateMachineModel; import org.springframework.statemachine.config.model.StatesData; import org.springframework.statemachine.config.model.TransitionsData; import org.springframework.util.ClassUtils; @@ -64,6 +67,12 @@ public class StateMachineFactoryConfiguration extends @Override protected BeanDefinition buildBeanDefinition(AnnotationMetadata importingClassMetadata, Class namedAnnotation) throws Exception { + + String enableStateMachineEnclosingClassName = importingClassMetadata.getClassName(); + // for below classloader, see gh122 + Class enableStateMachineEnclosingClass = ClassUtils.forName(enableStateMachineEnclosingClassName, + ClassUtils.getDefaultClassLoader()); + BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder .rootBeanDefinition(StateMachineFactoryDelegatingFactoryBean.class); AnnotationAttributes attributes = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes( @@ -72,9 +81,29 @@ public class StateMachineFactoryConfiguration extends beanDefinitionBuilder.addConstructorArgValue(builder); beanDefinitionBuilder.addConstructorArgValue(importingClassMetadata.getClassName()); beanDefinitionBuilder.addConstructorArgValue(contextEvents); - return beanDefinitionBuilder.getBeanDefinition(); + + AbstractBeanDefinition beanDefinition = beanDefinitionBuilder.getBeanDefinition(); + + // try to add more info about generics + ResolvableType type = resolveFactoryObjectType(enableStateMachineEnclosingClass); + if (type != null && beanDefinition instanceof RootBeanDefinition) { + ((RootBeanDefinition)beanDefinition).setTargetType(type); + } + + return beanDefinition; } + private ResolvableType resolveFactoryObjectType(Class enableStateMachineEnclosingClass) { + ResolvableType type = null; + try { + Class[] generics = ResolvableType.forClass(enableStateMachineEnclosingClass).getSuperType().resolveGenerics(); + if (generics != null && generics.length == 2) { + type = ResolvableType.forClassWithGenerics(StateMachineFactory.class, generics); + } + } catch (Exception e) { + } + return type; + } @Override protected List> getAnnotations() { diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/config/MachineTypedTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/config/MachineTypedTests.java new file mode 100644 index 00000000..9d679488 --- /dev/null +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/config/MachineTypedTests.java @@ -0,0 +1,255 @@ +/* + * Copyright 2017 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; + +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.Matchers.sameInstance; +import static org.junit.Assert.assertThat; + +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.statemachine.AbstractStateMachineTests; +import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; +import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; + +public class MachineTypedTests extends AbstractStateMachineTests { + + @Override + protected AnnotationConfigApplicationContext buildContext() { + return new AnnotationConfigApplicationContext(); + } + + @Test + public void testAutowireMachineByTypeNameMatches() { + context.register(Config1.class, Config2.class, MyBean1Config.class); + context.refresh(); + + MyBean1 myBean1 = context.getBean(MyBean1.class); + assertThat(myBean1.machine1, notNullValue()); + assertThat(myBean1.machine2, notNullValue()); + assertThat(myBean1.machine1, not(sameInstance(myBean1.machine2))); + } + + @Test + public void testAutowireMachineByTypeNameNotMatches() { + context.register(Config1.class, Config2.class, MyBean2Config.class); + context.refresh(); + + MyBean2 myBean2 = context.getBean(MyBean2.class); + assertThat(myBean2.someMachine1, notNullValue()); + assertThat(myBean2.someMachine2, notNullValue()); + assertThat(myBean2.someMachine1, not(sameInstance(myBean2.someMachine2))); + } + + @Test + public void testAutowireMachineFactoryByTypeNameMatches() { + context.register(Config3.class, Config4.class, MyBean3Config.class); + context.refresh(); + + MyBean3 myBean3 = context.getBean(MyBean3.class); + assertThat(myBean3.machinefactory1, notNullValue()); + assertThat(myBean3.machinefactory2, notNullValue()); + assertThat(myBean3.machinefactory1, not(sameInstance(myBean3.machinefactory2))); + } + + @Test + public void testAutowireMachineFactoryByTypeNameNotMatches() { + context.register(Config3.class, Config4.class, MyBean4Config.class); + context.refresh(); + + MyBean4 myBean4 = context.getBean(MyBean4.class); + assertThat(myBean4.someMachineFactory1, notNullValue()); + assertThat(myBean4.someMachineFactory2, notNullValue()); + assertThat(myBean4.someMachineFactory1, not(sameInstance(myBean4.someMachineFactory2))); + } + + @Configuration + public static class MyBean1Config { + + @Bean + public MyBean1 myBean1() { + return new MyBean1(); + } + } + + @Configuration + public static class MyBean2Config { + + @Bean + public MyBean2 myBean2() { + return new MyBean2(); + } + } + + @Configuration + public static class MyBean3Config { + + @Bean + public MyBean3 myBean3() { + return new MyBean3(); + } + } + + @Configuration + public static class MyBean4Config { + + @Bean + public MyBean4 myBean4() { + return new MyBean4(); + } + } + + public static class MyBean1 { + + @Autowired + StateMachine machine1; + + @Autowired + StateMachine machine2; + } + + public static class MyBean2 { + + @Autowired + StateMachine someMachine1; + + @Autowired + StateMachine someMachine2; + } + + public static class MyBean3 { + + @Autowired + StateMachineFactory machinefactory1; + + @Autowired + StateMachineFactory machinefactory2; + } + + public static class MyBean4 { + + @Autowired + StateMachineFactory someMachineFactory1; + + @Autowired + StateMachineFactory someMachineFactory2; + } + + @Configuration + @EnableStateMachine(name = "machine1") + public static class Config1 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(MyTestStates1.S1) + .state(MyTestStates1.S2); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source(MyTestStates1.S1).target(MyTestStates1.S2) + .event(MyTestEvents1.E1); + } + } + + @Configuration + @EnableStateMachine(name = "machine2") + public static class Config2 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(MyTestStates2.S1) + .state(MyTestStates2.S2); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source(MyTestStates2.S1).target(MyTestStates2.S2) + .event(MyTestEvents2.E1); + } + } + + @Configuration + @EnableStateMachineFactory(name = "machinefactory1") + public static class Config3 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(MyTestStates1.S1) + .state(MyTestStates1.S2); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source(MyTestStates1.S1).target(MyTestStates1.S2) + .event(MyTestEvents1.E1); + } + } + + @Configuration + @EnableStateMachineFactory(name = "machinefactory2") + public static class Config4 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(MyTestStates2.S1) + .state(MyTestStates2.S2); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source(MyTestStates2.S1).target(MyTestStates2.S2) + .event(MyTestEvents2.E1); + } + } + + public enum MyTestStates1 { + S1, S2; + } + + public enum MyTestEvents1 { + E1; + } + + public enum MyTestStates2 { + S1, S2; + } + + public enum MyTestEvents2 { + E1; + } +}