INT-3327: Add IntegrationConverter Annotation

JIRA: https://jira.spring.io/browse/INT-3327

INT-3327: Addressing PR comments

Polishing
This commit is contained in:
Artem Bilan
2014-03-31 16:38:28 +03:00
committed by Gary Russell
parent d74b4dba73
commit 969310b453
14 changed files with 317 additions and 138 deletions

View File

@@ -27,8 +27,10 @@ import java.lang.annotation.Target;
* annotation will be applied as global channel interceptors
* using the provided {@code patterns} to match channel names.
* <p>
* The annotation can be used at the {@code class} level for {@link org.springframework.stereotype.Component} beans
* This annotation can be used at the {@code class} level for {@link org.springframework.stereotype.Component} beans
* and on methods with {@link org.springframework.context.annotation.Bean}.
* <p>
* This annotation is an analogue of {@code <int:channel-interceptor/>}.
*
* @author Artem Bilan
* @since 4.0

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2014 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.integration.config;
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;
/**
* A marker annotation (an analogue of {@code <int:converter/>}) to register
* {@link org.springframework.core.convert.converter.Converter},
* {@link org.springframework.core.convert.converter.GenericConverter} or
* {@link org.springframework.core.convert.converter.ConverterFactory} beans for the {@code integrationConversionService}.
* <p>
* This annotation can be used at the {@code class} level for {@link org.springframework.stereotype.Component} beans
* and on methods with {@link org.springframework.context.annotation.Bean}.
*
* @author Artem Bilan
* @since 4.0
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface IntegrationConverter {
}

View File

@@ -0,0 +1,93 @@
/*
* Copyright 2014 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.integration.config;
import java.util.Set;
import org.springframework.beans.BeanMetadataElement;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.ManagedSet;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.MethodMetadata;
import org.springframework.integration.context.IntegrationContextUtils;
/**
* @author Artem Bilan
* @since 4.0
*/
public class IntegrationConverterInitializer implements IntegrationConfigurationInitializer {
private static final String CONTEXT_PACKAGE = "org.springframework.integration.context.";
@Override
public void initialize(ConfigurableListableBeanFactory beanFactory) throws BeansException {
BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
for (String beanName : registry.getBeanDefinitionNames()) {
BeanDefinition beanDefinition = registry.getBeanDefinition(beanName);
if (beanDefinition instanceof AnnotatedBeanDefinition) {
AnnotationMetadata metadata = ((AnnotatedBeanDefinition) beanDefinition).getMetadata();
boolean hasIntegrationConverter = metadata.hasAnnotation(IntegrationConverter.class.getName());
if (!hasIntegrationConverter && beanDefinition.getSource() instanceof MethodMetadata) {
MethodMetadata beanMethod = (MethodMetadata) beanDefinition.getSource();
hasIntegrationConverter = beanMethod.isAnnotated(IntegrationConverter.class.getName());
}
if (hasIntegrationConverter) {
this.registerConverter(registry, new RuntimeBeanReference(beanName));
}
}
}
}
@SuppressWarnings("unchecked")
public void registerConverter(BeanDefinitionRegistry registry, BeanMetadataElement converterBeanDefinition) {
Set<BeanMetadataElement> converters = new ManagedSet<BeanMetadataElement>();
if (!registry.containsBeanDefinition(IntegrationContextUtils.CONVERTER_REGISTRAR_BEAN_NAME)) {
BeanDefinitionBuilder converterRegistrarBuilder = BeanDefinitionBuilder.genericBeanDefinition(
CONTEXT_PACKAGE + "ConverterRegistrar").addConstructorArgValue(converters);
registry.registerBeanDefinition(IntegrationContextUtils.CONVERTER_REGISTRAR_BEAN_NAME,
converterRegistrarBuilder.getBeanDefinition());
if (!registry.containsBeanDefinition(IntegrationContextUtils.INTEGRATION_CONVERSION_SERVICE_BEAN_NAME)) {
registry.registerBeanDefinition(IntegrationContextUtils.INTEGRATION_CONVERSION_SERVICE_BEAN_NAME,
new RootBeanDefinition(CONTEXT_PACKAGE + "CustomConversionServiceFactoryBean"));
}
}
else {
BeanDefinition converterRegistrarBeanDefinition = registry
.getBeanDefinition(IntegrationContextUtils.CONVERTER_REGISTRAR_BEAN_NAME);
converters = (Set<BeanMetadataElement>) converterRegistrarBeanDefinition
.getConstructorArgumentValues()
.getIndexedArgumentValues()
.values()
.iterator()
.next()
.getValue();
}
converters.add(converterBeanDefinition);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2010 the original author or authors.
* Copyright 2002-2014 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.
@@ -21,56 +21,37 @@ import org.w3c.dom.Element;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.parsing.BeanComponentDefinition;
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.ManagedSet;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.integration.config.IntegrationConverterInitializer;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* @author Oleg Zhurakousky
* @author Mark Fisher
* @author Artem Bilan
* @since 2.0
*/
public class ConverterParser extends AbstractBeanDefinitionParser {
private final ManagedSet<Object> converters = new ManagedSet<Object>();
private volatile boolean initialized;
private final Object initializationMonitor = new Object();
private final static IntegrationConverterInitializer INTEGRATION_CONVERTER_INITIALIZER = new IntegrationConverterInitializer();
@Override
protected AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) {
this.initializeConversionServiceInfrastructureIfNecessary(parserContext);
BeanDefinitionRegistry registry = parserContext.getRegistry();
BeanComponentDefinition converterDefinition = IntegrationNamespaceUtils.parseInnerHandlerDefinition(element, parserContext);
if (converterDefinition != null) {
this.converters.add(converterDefinition);
INTEGRATION_CONVERTER_INITIALIZER.registerConverter(registry, converterDefinition);
}
else {
String beanName = element.getAttribute("ref");
Assert.isTrue(StringUtils.hasText(beanName),
"Either a 'ref' attribute pointing to a Converter or a <bean> sub-element defining a Converter is required.");
this.converters.add(new RuntimeBeanReference(beanName));
INTEGRATION_CONVERTER_INITIALIZER.registerConverter(registry, new RuntimeBeanReference(beanName));
}
return null;
}
private void initializeConversionServiceInfrastructureIfNecessary(ParserContext parserContext) {
synchronized (this.initializationMonitor) {
if (!this.initialized) {
String contextPackage = "org.springframework.integration.context.";
BeanDefinitionBuilder creatorBuilder = BeanDefinitionBuilder.rootBeanDefinition(contextPackage + "ConversionServiceCreator");
BeanDefinitionReaderUtils.registerWithGeneratedName(creatorBuilder.getBeanDefinition(), parserContext.getRegistry());
BeanDefinitionBuilder conversionServiceBuilder = BeanDefinitionBuilder.rootBeanDefinition(contextPackage + "ConverterRegistrar");
conversionServiceBuilder.addConstructorArgValue(converters);
BeanDefinitionReaderUtils.registerWithGeneratedName(conversionServiceBuilder.getBeanDefinition(), parserContext.getRegistry());
this.initialized = true;
}
}
}
}

View File

@@ -1,72 +0,0 @@
/*
* Copyright 2002-2010 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.integration.context;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
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.support.ConversionServiceFactoryBean;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.GenericConversionService;
/**
* @author Oleg Zhurakousky
* @author Mark Fisher
* @since 2.0
*/
class ConversionServiceCreator implements BeanFactoryPostProcessor {
private final Log logger = LogFactory.getLog(this.getClass());
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
if (!beanFactory.containsBean(IntegrationContextUtils.INTEGRATION_CONVERSION_SERVICE_BEAN_NAME)) {
if (beanFactory instanceof BeanDefinitionRegistry) {
BeanDefinitionBuilder conversionServiceBuilder = BeanDefinitionBuilder.rootBeanDefinition(CustomConversionServiceFactoryBean.class);
BeanDefinitionHolder beanDefinitionHolder = new BeanDefinitionHolder(
conversionServiceBuilder.getBeanDefinition(), IntegrationContextUtils.INTEGRATION_CONVERSION_SERVICE_BEAN_NAME);
BeanDefinitionReaderUtils.registerBeanDefinition(beanDefinitionHolder, (BeanDefinitionRegistry) beanFactory);
}
else if (logger.isWarnEnabled()) {
logger.warn("BeanFactory is not a BeanDefinitionRegistry implementation. Cannot register a default ConversionService.");
}
}
}
/**
* This is a workaround until we depend on Spring 3.1 and specifically when SPR-8818 is resolved.
* See INT-2259 and INT-1893 for more detail.
*/
static class CustomConversionServiceFactoryBean extends ConversionServiceFactoryBean {
@Override
public ConversionService getObject() {
ConversionService service = super.getObject();
if (service instanceof GenericConversionService) {
((GenericConversionService) service).removeConvertible(Object.class, Object.class);
}
return service;
}
}
}

View File

@@ -22,7 +22,6 @@ import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.support.ConversionServiceFactory;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.util.Assert;
@@ -30,19 +29,19 @@ import org.springframework.util.Assert;
/**
* Utility class that keeps track of a set of Converters in order to register
* them with the "integrationConversionService" upon initialization.
*
*
* @author Oleg Zhurakousky
* @author Mark Fisher
* @since 2.0
*/
class ConverterRegistrar implements InitializingBean, BeanFactoryAware {
private final Set<Converter<?, ?>> converters;
private final Set<?> converters;
private BeanFactory beanFactory;
public ConverterRegistrar(Set<Converter<?, ?>> converters) {
public ConverterRegistrar(Set<?> converters) {
this.converters = converters;
}
@@ -55,7 +54,7 @@ class ConverterRegistrar implements InitializingBean, BeanFactoryAware {
Assert.notNull(beanFactory, "BeanFactory is required");
ConversionService conversionService = IntegrationContextUtils.getConversionService(beanFactory);
if (conversionService instanceof GenericConversionService) {
ConversionServiceFactory.registerConverters(converters, (GenericConversionService) conversionService);
ConversionServiceFactory.registerConverters(converters, (GenericConversionService) conversionService);
}
else {
Assert.notNull(conversionService, "Failed to locate '" + IntegrationContextUtils.INTEGRATION_CONVERSION_SERVICE_BEAN_NAME + "'");

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2014 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.integration.context;
import org.springframework.context.support.ConversionServiceFactoryBean;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.GenericConversionService;
/**
* This is a workaround until SPR-8818 will be resolved.
* See INT-2259 and INT-1893 for more detail.
*
* @author Oleg Zhurakousky
* @author Mark Fisher
* @since 2.0
*/
class CustomConversionServiceFactoryBean extends ConversionServiceFactoryBean {
@Override
public ConversionService getObject() {
ConversionService service = super.getObject();
if (service instanceof GenericConversionService) {
((GenericConversionService) service).removeConvertible(Object.class, Object.class);
}
return service;
}
}

View File

@@ -52,6 +52,8 @@ public abstract class IntegrationContextUtils {
public static final String METADATA_STORE_BEAN_NAME = "metadataStore";
public static final String CONVERTER_REGISTRAR_BEAN_NAME = "converterRegistrar";
public static final String INTEGRATION_CONVERSION_SERVICE_BEAN_NAME = "integrationConversionService";
public static final String INTEGRATION_EVALUATION_CONTEXT_BEAN_NAME = "integrationEvaluationContext";

View File

@@ -1,4 +1,5 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.integration.config.boot.IntegrationAutoConfiguration
org.springframework.integration.config.IntegrationConfigurationInitializer=\
org.springframework.integration.config.GlobalChannelInterceptorInitializer
org.springframework.integration.config.GlobalChannelInterceptorInitializer,\
org.springframework.integration.config.IntegrationConverterInitializer

View File

@@ -42,6 +42,8 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
import org.springframework.context.annotation.PropertySource;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.serializer.support.SerializingConverter;
import org.springframework.integration.annotation.Gateway;
import org.springframework.integration.annotation.GatewayHeader;
import org.springframework.integration.annotation.IntegrationComponentScan;
@@ -53,6 +55,7 @@ import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.annotation.Transformer;
import org.springframework.integration.channel.AbstractMessageChannel;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.channel.FixedSubscriberChannel;
import org.springframework.integration.channel.NullChannel;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.integration.channel.interceptor.WireTap;
@@ -60,8 +63,10 @@ import org.springframework.integration.config.EnableIntegration;
import org.springframework.integration.config.EnableMessageHistory;
import org.springframework.integration.config.EnablePublisher;
import org.springframework.integration.config.GlobalChannelInterceptor;
import org.springframework.integration.config.IntegrationConverter;
import org.springframework.integration.history.MessageHistory;
import org.springframework.integration.history.MessageHistoryConfigurer;
import org.springframework.integration.message.MutableMessage;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.messaging.Message;
@@ -69,6 +74,7 @@ import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.PollableChannel;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.ChannelInterceptorAdapter;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.stereotype.Component;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
@@ -110,6 +116,15 @@ public class EnableIntegrationTests {
@Autowired
private AtomicInteger fbInterceptorCounter;
@Autowired
private MessageChannel numberChannel;
@Autowired
private TestConverter testConverter;
@Autowired
private MessageChannel bytesChannel;
@Test
public void testAnnotatedServiceActivator() {
this.input.send(MessageBuilder.withPayload("Foo").build());
@@ -192,6 +207,17 @@ public class EnableIntegrationTests {
child.close();
}
@Test
public void testIntegrationConverter() {
this.numberChannel.send(new GenericMessage<Integer>(10));
this.numberChannel.send(new GenericMessage<Boolean>(true));
assertThat(this.testConverter.getInvoked(), Matchers.greaterThan(0));
assertTrue(this.bytesChannel.send(new GenericMessage<byte[]>("foo".getBytes())));
assertTrue(this.bytesChannel.send(new GenericMessage<Message<?>>(new MutableMessage<Object>(""))));
}
@Configuration
@ComponentScan
@IntegrationComponentScan
@@ -271,6 +297,23 @@ public class EnableIntegrationTests {
}
@Component
@IntegrationConverter
public static class TestConverter implements Converter<Boolean, Number> {
private final AtomicInteger invoked = new AtomicInteger();
@Override
public Number convert(Boolean source) {
this.invoked.incrementAndGet();
return source ? 1 : 0;
}
public Integer getInvoked() {
return invoked.get();
}
}
@Configuration
@EnableIntegration
@ImportResource("classpath:org/springframework/integration/configuration/EnableIntegrationTests-context.xml")
@@ -293,6 +336,26 @@ public class EnableIntegrationTests {
return new DirectChannel();
}
@Bean
public QueueChannel numberChannel() {
QueueChannel channel = new QueueChannel();
channel.setDatatypes(Number.class);
return channel;
}
@Bean
public QueueChannel bytesChannel() {
QueueChannel channel = new QueueChannel();
channel.setDatatypes(byte[].class);
return channel;
}
@Bean
@IntegrationConverter
public SerializingConverter serializingConverter() {
return new SerializingConverter();
}
}
@Configuration

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-2014 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.
@@ -16,29 +16,25 @@
package org.springframework.integration.transformer;
import java.lang.reflect.Constructor;
import java.security.AccessController;
import java.security.PrivilegedExceptionAction;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import java.util.HashMap;
import java.util.Map;
import org.junit.Test;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.context.support.StaticApplicationContext;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterRegistry;
import org.springframework.messaging.Message;
import org.springframework.integration.context.IntegrationContextUtils;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.util.ClassUtils;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import org.springframework.messaging.Message;
/**
@@ -123,22 +119,8 @@ public class MapToObjectTransformerTests {
private BeanFactory getBeanFactory() {
GenericApplicationContext ctx = TestUtils.createTestApplicationContext();
Constructor<?> constructorToUse = null;
try {
// Add the integrationConversionService (reflection needed because of package protection)
final Class<?> conversionServiceCreatorClass = ClassUtils.forName("org.springframework.integration.context.ConversionServiceCreator",
ClassUtils.getDefaultClassLoader());
constructorToUse = AccessController.doPrivileged(new PrivilegedExceptionAction<Constructor<?>>() {
public Constructor<?> run() throws Exception {
return conversionServiceCreatorClass.getDeclaredConstructor((Class[]) null);
}
});
}
catch (Exception e) {
throw new RuntimeException("Unexpected Privilege Exception: ", e);
}
ctx.addBeanFactoryPostProcessor((BeanFactoryPostProcessor) BeanUtils.instantiateClass(constructorToUse));
ctx.registerBeanDefinition(IntegrationContextUtils.INTEGRATION_CONVERSION_SERVICE_BEAN_NAME,
new RootBeanDefinition("org.springframework.integration.context.CustomConversionServiceFactoryBean"));
ctx.refresh();
return ctx;
}