From 862c9db611d50cc4379b39b1ec67e8af1d2f073d Mon Sep 17 00:00:00 2001 From: Donovan Muller Date: Tue, 3 May 2016 20:36:27 +0200 Subject: [PATCH] Add @RemoteApplicationEventScan support for custom events (subclasses of RemoteApplicationEvent) Fixes gh-34 --- .../jackson/BusJacksonAutoConfiguration.java | 63 ++++++-------- .../RemoteApplicationEventRegistrar.java | 57 +++++++++++++ .../jackson/RemoteApplicationEventScan.java | 26 ++++++ .../RemoteApplicationEventScanTests.java | 85 +++++++++++++++++++ .../cloud/bus/jackson/SubtypeModuleTests.java | 6 +- 5 files changed, 197 insertions(+), 40 deletions(-) create mode 100644 spring-cloud-bus/src/main/java/org/springframework/cloud/bus/jackson/RemoteApplicationEventRegistrar.java create mode 100644 spring-cloud-bus/src/main/java/org/springframework/cloud/bus/jackson/RemoteApplicationEventScan.java create mode 100644 spring-cloud-bus/src/test/java/org/springframework/cloud/bus/jackson/RemoteApplicationEventScanTests.java diff --git a/spring-cloud-bus/src/main/java/org/springframework/cloud/bus/jackson/BusJacksonAutoConfiguration.java b/spring-cloud-bus/src/main/java/org/springframework/cloud/bus/jackson/BusJacksonAutoConfiguration.java index 56ff0a5..0901502 100644 --- a/spring-cloud-bus/src/main/java/org/springframework/cloud/bus/jackson/BusJacksonAutoConfiguration.java +++ b/spring-cloud-bus/src/main/java/org/springframework/cloud/bus/jackson/BusJacksonAutoConfiguration.java @@ -1,12 +1,15 @@ package org.springframework.cloud.bus.jackson; -import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Set; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.cloud.bus.BusAutoConfiguration; import org.springframework.cloud.bus.ConditionalOnBusEnabled; import org.springframework.cloud.bus.endpoint.RefreshBusEndpoint; @@ -14,13 +17,9 @@ import org.springframework.cloud.bus.event.RemoteApplicationEvent; import org.springframework.cloud.stream.converter.AbstractFromMessageConverter; import org.springframework.cloud.stream.converter.MessageConverterUtils; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; import org.springframework.context.annotation.Configuration; -import org.springframework.core.io.Resource; -import org.springframework.core.io.support.PathMatchingResourcePatternResolver; -import org.springframework.core.io.support.ResourcePatternResolver; -import org.springframework.core.type.classreading.CachingMetadataReaderFactory; -import org.springframework.core.type.classreading.MetadataReader; -import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.filter.AssignableTypeFilter; import org.springframework.messaging.Message; import org.springframework.util.ClassUtils; import org.springframework.util.MimeTypeUtils; @@ -31,6 +30,7 @@ import com.fasterxml.jackson.databind.SerializationFeature; /** * @author Spencer Gibb * @author Dave Syer + * @author Donovan Muller */ @Configuration @ConditionalOnBusEnabled @@ -39,23 +39,20 @@ import com.fasterxml.jackson.databind.SerializationFeature; public class BusJacksonAutoConfiguration { @Bean + @ConditionalOnMissingBean(name="busJsonConverter") public BusJacksonMessageConverter busJsonConverter() { return new BusJacksonMessageConverter(); } } -class BusJacksonMessageConverter extends AbstractFromMessageConverter { +class BusJacksonMessageConverter extends AbstractFromMessageConverter implements InitializingBean { private static final String DEFAULT_PACKAGE = ClassUtils .getPackageName(RemoteApplicationEvent.class); - private static final String CLASS_RESOURCE_PATTERN = "/**/*.class"; - private final ObjectMapper mapper = new ObjectMapper(); - private ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver(); - private String[] packagesToScan = new String[] { DEFAULT_PACKAGE }; public void setPackagesToScan(String[] packagesToScan) { @@ -68,40 +65,24 @@ class BusJacksonMessageConverter extends AbstractFromMessageConverter { public BusJacksonMessageConverter() { super(MimeTypeUtils.APPLICATION_JSON, MessageConverterUtils.X_JAVA_OBJECT); - this.mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); - this.mapper.registerModule(new SubtypeModule(findSubTypes())); } private Class[] findSubTypes() { List> types = new ArrayList<>(); if (this.packagesToScan != null) { for (String pkg : this.packagesToScan) { - try { - String pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX - + ClassUtils.convertClassNameToResourcePath(pkg) - + CLASS_RESOURCE_PATTERN; - Resource[] resources = this.resourcePatternResolver - .getResources(pattern); - MetadataReaderFactory readerFactory = new CachingMetadataReaderFactory( - this.resourcePatternResolver); - for (Resource resource : resources) { - if (resource.isReadable()) { - MetadataReader reader = readerFactory - .getMetadataReader(resource); - String className = reader.getClassMetadata().getClassName(); - try { - Class type = ClassUtils.forName(className, null); - types.add(type); - } - catch (Exception e) { - } - } + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); + provider.addIncludeFilter(new AssignableTypeFilter(RemoteApplicationEvent.class)); + + Set components = provider.findCandidateComponents(pkg); + for (BeanDefinition component : components) { + try { + types.add(Class.forName(component.getBeanClassName())); + } catch (ClassNotFoundException e) { + throw new IllegalStateException( + "Failed to scan classpath for remote event classes", e); } } - catch (IOException ex) { - throw new IllegalStateException( - "Failed to scan classpath for remote event classes", ex); - } } } return types.toArray(new Class[0]); @@ -137,4 +118,10 @@ class BusJacksonMessageConverter extends AbstractFromMessageConverter { } return result; } + + @Override + public void afterPropertiesSet() throws Exception { + this.mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + this.mapper.registerModule(new SubtypeModule(findSubTypes())); + } } diff --git a/spring-cloud-bus/src/main/java/org/springframework/cloud/bus/jackson/RemoteApplicationEventRegistrar.java b/spring-cloud-bus/src/main/java/org/springframework/cloud/bus/jackson/RemoteApplicationEventRegistrar.java new file mode 100644 index 0000000..f209530 --- /dev/null +++ b/spring-cloud-bus/src/main/java/org/springframework/cloud/bus/jackson/RemoteApplicationEventRegistrar.java @@ -0,0 +1,57 @@ +package org.springframework.cloud.bus.jackson; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * @author Donovan Muller + */ +public class RemoteApplicationEventRegistrar implements ImportBeanDefinitionRegistrar { + + // patterned after Spring Integration IntegrationComponentScanRegistrar + + @Override + public void registerBeanDefinitions(final AnnotationMetadata importingClassMetadata, + final BeanDefinitionRegistry registry) { + + Map componentScan = importingClassMetadata + .getAnnotationAttributes(RemoteApplicationEventScan.class.getName(), false); + + Set basePackages = new HashSet<>(); + for (String pkg : (String[]) componentScan.get("value")) { + if (StringUtils.hasText(pkg)) { + basePackages.add(pkg); + } + } + for (String pkg : (String[]) componentScan.get("basePackages")) { + if (StringUtils.hasText(pkg)) { + basePackages.add(pkg); + } + } + for (Class clazz : (Class[]) componentScan.get("basePackageClasses")) { + basePackages.add(ClassUtils.getPackageName(clazz)); + } + + if (basePackages.isEmpty()) { + basePackages.add(ClassUtils.getPackageName(importingClassMetadata.getClassName())); + } + + BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(BusJacksonMessageConverter.class); + beanDefinitionBuilder.addPropertyValue("packagesToScan", basePackages.toArray(new String[basePackages.size()])); + AbstractBeanDefinition beanDefinition = beanDefinitionBuilder.getBeanDefinition(); + + BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, "busJsonConverter"); + BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry); + } +} diff --git a/spring-cloud-bus/src/main/java/org/springframework/cloud/bus/jackson/RemoteApplicationEventScan.java b/spring-cloud-bus/src/main/java/org/springframework/cloud/bus/jackson/RemoteApplicationEventScan.java new file mode 100644 index 0000000..c99ff89 --- /dev/null +++ b/spring-cloud-bus/src/main/java/org/springframework/cloud/bus/jackson/RemoteApplicationEventScan.java @@ -0,0 +1,26 @@ +package org.springframework.cloud.bus.jackson; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Import; +import org.springframework.core.annotation.AliasFor; + +/** + * @author Donovan Muller + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Documented +@Import(RemoteApplicationEventRegistrar.class) +public @interface RemoteApplicationEventScan { + + String[] value() default {}; + + String[] basePackages() default {}; + + Class[] basePackageClasses() default {}; +} diff --git a/spring-cloud-bus/src/test/java/org/springframework/cloud/bus/jackson/RemoteApplicationEventScanTests.java b/spring-cloud-bus/src/test/java/org/springframework/cloud/bus/jackson/RemoteApplicationEventScanTests.java new file mode 100644 index 0000000..8897a64 --- /dev/null +++ b/spring-cloud-bus/src/test/java/org/springframework/cloud/bus/jackson/RemoteApplicationEventScanTests.java @@ -0,0 +1,85 @@ +package org.springframework.cloud.bus.jackson; + +import static org.junit.Assert.assertArrayEquals; + +import org.junit.Test; +import org.springframework.boot.Banner; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.bus.event.test.TestRemoteApplicationEvent; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.util.ReflectionTestUtils; + +public class RemoteApplicationEventScanTests { + + private BusJacksonMessageConverter converter; + + @Test + public void importingClassMetadataPackageRegistered() { + converter = createTestContext(DefaultConfig.class) + .getBean(BusJacksonMessageConverter.class); + + assertArrayEquals("RemoteApplicationEvent packages not registered", + (String[]) ReflectionTestUtils.getField(converter, "packagesToScan"), + new String[]{"org.springframework.cloud.bus.jackson", + "org.springframework.cloud.bus.event"}); + } + + @Test + public void annotationValuePackagesRegistered() { + converter = createTestContext(ValueConfig.class) + .getBean(BusJacksonMessageConverter.class); + + assertArrayEquals("RemoteApplicationEvent packages not registered", + (String[]) ReflectionTestUtils.getField(converter, "packagesToScan"), + new String[]{"foo.bar", "com.acme", "org.springframework.cloud.bus.event"}); + } + + @Test + public void annotationValueBasePackagesRegistered() { + converter = createTestContext(BasePackagesConfig.class) + .getBean(BusJacksonMessageConverter.class); + + assertArrayEquals("RemoteApplicationEvent packages not registered", + (String[]) ReflectionTestUtils.getField(converter, "packagesToScan"), + new String[]{"foo.bar", "fizz.buzz", "com.acme", "org.springframework.cloud.bus.event"}); + } + + @Test + public void annotationBasePackagesRegistered() { + converter = createTestContext(BasePackageClassesConfig.class) + .getBean(BusJacksonMessageConverter.class); + + assertArrayEquals("RemoteApplicationEvent packages not registered", + (String[]) ReflectionTestUtils.getField(converter, "packagesToScan"), + new String[]{"org.springframework.cloud.bus.event.test", + "org.springframework.cloud.bus.event"}); + } + + private ConfigurableApplicationContext createTestContext(Class configuration) { + return new SpringApplicationBuilder(configuration) + .web(false) + .bannerMode(Banner.Mode.OFF) + .run(); + } + + @Configuration + @RemoteApplicationEventScan + static class DefaultConfig { + } + + @Configuration + @RemoteApplicationEventScan({"com.acme", "foo.bar"}) + static class ValueConfig { + } + + @Configuration + @RemoteApplicationEventScan(basePackages = {"com.acme", "foo.bar", "fizz.buzz"}) + static class BasePackagesConfig { + } + + @Configuration + @RemoteApplicationEventScan(basePackageClasses = TestRemoteApplicationEvent.class) + static class BasePackageClassesConfig { + } +} diff --git a/spring-cloud-bus/src/test/java/org/springframework/cloud/bus/jackson/SubtypeModuleTests.java b/spring-cloud-bus/src/test/java/org/springframework/cloud/bus/jackson/SubtypeModuleTests.java index 229d790..122d0d6 100644 --- a/spring-cloud-bus/src/test/java/org/springframework/cloud/bus/jackson/SubtypeModuleTests.java +++ b/spring-cloud-bus/src/test/java/org/springframework/cloud/bus/jackson/SubtypeModuleTests.java @@ -44,7 +44,8 @@ public class SubtypeModuleTests { @Test public void testDeserializeWithMessageConverter() throws Exception { - MessageConverter converter = new BusJacksonAutoConfiguration().busJsonConverter(); + BusJacksonMessageConverter converter = new BusJacksonAutoConfiguration().busJsonConverter(); + converter.afterPropertiesSet(); Object event = converter.fromMessage( MessageBuilder.withPayload("{\"type\":\"TestRemoteApplicationEvent\"}").build(), RemoteApplicationEvent.class); @@ -53,7 +54,8 @@ public class SubtypeModuleTests { @Test public void testDeserializeJsonTypeWithMessageConverter() throws Exception { - MessageConverter converter = new BusJacksonAutoConfiguration().busJsonConverter(); + BusJacksonMessageConverter converter = new BusJacksonAutoConfiguration().busJsonConverter(); + converter.afterPropertiesSet(); Object event = converter.fromMessage( MessageBuilder.withPayload("{\"type\":\"typed\"}").build(), RemoteApplicationEvent.class);