Add @RemoteApplicationEventScan support for custom events
(subclasses of RemoteApplicationEvent) Fixes gh-34
This commit is contained in:
committed by
Dave Syer
parent
623108ab8f
commit
862c9db611
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 {};
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user