Add @RemoteApplicationEventScan support for custom events

(subclasses of RemoteApplicationEvent)

Fixes gh-34
This commit is contained in:
Donovan Muller
2016-05-03 20:36:27 +02:00
committed by Dave Syer
parent 623108ab8f
commit 862c9db611
5 changed files with 197 additions and 40 deletions

View File

@@ -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<Class<?>> 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<BeanDefinition> 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()));
}
}

View File

@@ -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<String, Object> componentScan = importingClassMetadata
.getAnnotationAttributes(RemoteApplicationEventScan.class.getName(), false);
Set<String> 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);
}
}

View File

@@ -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 {};
}

View File

@@ -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 {
}
}

View File

@@ -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);