GH-1127: Add stub Binder backed by SI

Resolves: spring-cloud/spring-cloud-stream#1127

- Added initial implementation of SI-backed stub binder to facilitate
 more consistent testing
- Improved BinderAwareChannelResolverTests and ExtendedPropertiesBinderAwareChannelResolverTests
to make use of it
- Added sample Application that demonstrates the binder usage

Addressed PR comments
This commit is contained in:
Oleg Zhurakousky
2017-11-14 12:39:36 -05:00
committed by Artem Bilan
parent 024e3cffcc
commit 21a2a84d86
9 changed files with 766 additions and 274 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2016 the original author or authors.
* Copyright 2013-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.
@@ -20,7 +20,6 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -28,25 +27,19 @@ import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.stream.binder.integration.SpringIntegrationBinderConfiguration;
import org.springframework.cloud.stream.binding.AbstractBindingTargetFactory;
import org.springframework.cloud.stream.binding.Bindable;
import org.springframework.cloud.stream.binding.BinderAwareChannelResolver;
import org.springframework.cloud.stream.binding.BindingService;
import org.springframework.cloud.stream.binding.DynamicDestinationsBindable;
import org.springframework.cloud.stream.binding.InputBindingLifecycle;
import org.springframework.cloud.stream.binding.MessageConverterConfigurer;
import org.springframework.cloud.stream.binding.OutputBindingLifecycle;
import org.springframework.cloud.stream.binding.SubscribableChannelBindingTargetFactory;
import org.springframework.cloud.stream.config.BindingProperties;
import org.springframework.cloud.stream.config.BindingServiceProperties;
import org.springframework.cloud.stream.converter.CompositeMessageConverterFactory;
import org.springframework.context.support.StaticApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.support.DefaultMessageBuilderFactory;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.support.utils.IntegrationUtils;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
@@ -54,10 +47,11 @@ import org.springframework.messaging.MessagingException;
import org.springframework.messaging.SubscribableChannel;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.matches;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.matches;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -66,10 +60,11 @@ import static org.mockito.Mockito.when;
* @author Mark Fisher
* @author Gary Russell
* @author Ilayaperumal Gopinathan
* @author Oleg Zhurakousky
*/
public class BinderAwareChannelResolverTests {
protected final StaticApplicationContext context = new StaticApplicationContext();
protected ConfigurableApplicationContext context;
protected volatile BinderAwareChannelResolver resolver;
@@ -81,68 +76,43 @@ public class BinderAwareChannelResolverTests {
protected volatile DynamicDestinationsBindable dynamicDestinationsBindable;
private volatile List<TestBinder.TestBinding> producerBindings;
@Before
public void setupContext() throws Exception {
producerBindings = new ArrayList<>();
this.binder = new TestBinder();
BinderFactory binderFactory = new BinderFactory() {
@Override
public <T> Binder<T, ? extends ConsumerProperties, ? extends ProducerProperties> getBinder(
String configurationName, Class<? extends T> bindableType) {
return (Binder<T, ? extends ConsumerProperties, ? extends ProducerProperties>) binder;
}
};
this.bindingServiceProperties = new BindingServiceProperties();
Map<String, BindingProperties> bindings = new HashMap<>();
BindingProperties bindingProperties = new BindingProperties();
bindingProperties.setContentType("text/plain");
bindings.put("foo", bindingProperties);
this.bindingServiceProperties.setBindings(bindings);
BindingService bindingService = new BindingService(bindingServiceProperties,
binderFactory);
MessageConverterConfigurer messageConverterConfigurer = new MessageConverterConfigurer(
this.bindingServiceProperties,
new CompositeMessageConverterFactory());
messageConverterConfigurer.setBeanFactory(Mockito.mock(ConfigurableListableBeanFactory.class));
messageConverterConfigurer.afterPropertiesSet();
this.bindingTargetFactory = new SubscribableChannelBindingTargetFactory(messageConverterConfigurer);
dynamicDestinationsBindable = new DynamicDestinationsBindable();
this.resolver = new BinderAwareChannelResolver(bindingService, this.bindingTargetFactory,
dynamicDestinationsBindable);
this.resolver.setBeanFactory(context.getBeanFactory());
context.getBeanFactory().registerSingleton("channelResolver", this.resolver);
context.getBeanFactory().registerSingleton("dynamicDestinationBindable", this.dynamicDestinationsBindable);
context.registerSingleton("other", DirectChannel.class);
context.registerSingleton(IntegrationUtils.INTEGRATION_MESSAGE_BUILDER_FACTORY_BEAN_NAME,
DefaultMessageBuilderFactory.class);
context.getBeanFactory().registerSingleton("bindingService", bindingService);
context.registerSingleton("inputBindingLifecycle", InputBindingLifecycle.class);
context.registerSingleton("outputBindingLifecycle", OutputBindingLifecycle.class);
context.refresh();
this.context = new SpringApplicationBuilder(SpringIntegrationBinderConfiguration.getCompleteConfiguration()).web(WebApplicationType.NONE).run();
this.resolver = context.getBean(BinderAwareChannelResolver.class);
this.binder = context.getBean(Binder.class);
this.bindingServiceProperties = context.getBean(BindingServiceProperties.class);
this.bindingTargetFactory = context.getBean(AbstractBindingTargetFactory.class);
}
@Test
public void resolveChannel() {
assertThat(producerBindings).hasSize(0);
Map<String, Bindable> bindables = context.getBeansOfType(Bindable.class);
assertThat(bindables).hasSize(1);
for (Bindable bindable : bindables.values()) {
assertEquals(0, bindable.getInputs().size()); // producer
assertEquals(0, bindable.getOutputs().size());// consumer
}
MessageChannel registered = resolver.resolveDestination("foo");
assertThat(producerBindings).hasSize(1);
TestBinder.TestBinding binding = producerBindings.get(0);
assertThat(binding.isBound()).describedAs("Must be bound");
bindables = context.getBeansOfType(Bindable.class);
assertThat(bindables).hasSize(1);
for (Bindable bindable : bindables.values()) {
assertEquals(0, bindable.getInputs().size()); // producer
assertEquals(1, bindable.getOutputs().size());// consumer
}
DirectChannel testChannel = new DirectChannel();
testChannel.setComponentName("INPUT");
final CountDownLatch latch = new CountDownLatch(1);
final List<Message<?>> received = new ArrayList<>();
testChannel.subscribe(new MessageHandler() {
@Override
public void handleMessage(Message<?> message) throws MessagingException {
received.add(message);
latch.countDown();
}
});
binder.bindConsumer("foo", null, testChannel, new ConsumerProperties());
this.binder.bindConsumer("foo", null, testChannel, new ConsumerProperties());
assertThat(received).hasSize(0);
registered.send(MessageBuilder.withPayload("hello").build());
try {
@@ -154,15 +124,18 @@ public class BinderAwareChannelResolverTests {
}
assertThat(received).hasSize(1);
assertThat(new String((byte[])received.get(0).getPayload())).isEqualTo("hello");
context.close();
assertThat(producerBindings).hasSize(1);
assertThat(binding.isBound()).isFalse().describedAs("Must not be bound");
this.context.close();
for (Bindable bindable : bindables.values()) {
assertEquals(0, bindable.getInputs().size());
assertEquals(0, bindable.getOutputs().size());//Must not be bound"
}
}
@Test
public void resolveNonRegisteredChannel() {
MessageChannel other = resolver.resolveDestination("other");
assertThat(context.getBean("other")).isSameAs(other);
this.context.close();
}
@Test
@@ -173,7 +146,6 @@ public class BinderAwareChannelResolverTests {
genericProperties.setContentType("text/plain");
bindings.put("foo", genericProperties);
this.bindingServiceProperties.setBindings(bindings);
@SuppressWarnings("unchecked")
Binder binder = mock(Binder.class);
Binder binder2 = mock(Binder.class);
BinderFactory mockBinderFactory = Mockito.mock(BinderFactory.class);
@@ -187,74 +159,12 @@ public class BinderAwareChannelResolverTests {
when(mockBinderFactory.getBinder("someTransport", DirectChannel.class)).thenReturn(binder2);
BindingService bindingService = new BindingService(bindingServiceProperties,
mockBinderFactory);
@SuppressWarnings("unchecked")
BinderAwareChannelResolver resolver = new BinderAwareChannelResolver(bindingService, this.bindingTargetFactory,
new DynamicDestinationsBindable());
BeanFactory beanFactory = new DefaultListableBeanFactory();
resolver.setBeanFactory(beanFactory);
resolver.setBeanFactory(context.getBeanFactory());
SubscribableChannel resolved = (SubscribableChannel) resolver.resolveDestination("foo");
verify(binder).bindProducer(eq("foo"), any(MessageChannel.class), any(ProducerProperties.class));
assertThat(resolved).isSameAs(beanFactory.getBean("foo"));
}
/**
* A simple test binder that creates queues for the destinations. Ignores groups.
*/
private class TestBinder implements Binder<MessageChannel, ConsumerProperties, ProducerProperties> {
private final Map<String, DirectChannel> destinations = new ConcurrentHashMap<>();
@Override
public Binding<MessageChannel> bindConsumer(String name, String group,
MessageChannel inboundBindTarget, ConsumerProperties properties) {
synchronized (destinations) {
if (!destinations.containsKey(name)) {
destinations.put(name, new DirectChannel());
}
}
DirectHandler directHandler = new DirectHandler(inboundBindTarget);
destinations.get(name).subscribe(directHandler);
return new TestBinding(name, directHandler);
}
@Override
public Binding<MessageChannel> bindProducer(String name,
MessageChannel outboundBindTarget, ProducerProperties properties) {
synchronized (destinations) {
if (!destinations.containsKey(name)) {
destinations.put(name, new DirectChannel());
}
}
DirectHandler directHandler = new DirectHandler(destinations.get(name));
// for test purposes we can assume it is a SubscribableChannel
((SubscribableChannel) outboundBindTarget).subscribe(directHandler);
TestBinding binding = new TestBinding(name, directHandler);
producerBindings.add(binding);
return binding;
}
private final class TestBinding implements Binding<MessageChannel> {
private final String name;
private final DirectHandler directHandler;
private boolean bound = true;
private TestBinding(String name, DirectHandler directHandler) {
this.name = name;
this.directHandler = directHandler;
}
@Override
public void unbind() {
bound = false;
destinations.get(name).unsubscribe(directHandler);
}
public boolean isBound() {
return bound;
}
}
assertThat(resolved).isSameAs(context.getBean("foo"));
this.context.close();
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2016 the original author or authors.
* Copyright 2013-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.
@@ -17,102 +17,49 @@
package org.springframework.cloud.stream.binder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.cloud.stream.binding.BinderAwareChannelResolver;
import org.springframework.cloud.stream.binding.BindingService;
import org.springframework.cloud.stream.binding.DynamicDestinationsBindable;
import org.springframework.cloud.stream.binding.InputBindingLifecycle;
import org.springframework.cloud.stream.binding.MessageConverterConfigurer;
import org.springframework.cloud.stream.binding.OutputBindingLifecycle;
import org.springframework.cloud.stream.binding.SubscribableChannelBindingTargetFactory;
import org.springframework.cloud.stream.config.BindingProperties;
import org.springframework.cloud.stream.config.BindingServiceProperties;
import org.springframework.cloud.stream.converter.CompositeMessageConverterFactory;
import org.springframework.cloud.stream.binding.Bindable;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.support.DefaultMessageBuilderFactory;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.support.utils.IntegrationUtils;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessagingException;
import org.springframework.messaging.SubscribableChannel;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
/**
* @author Mark Fisher
* @author Gary Russell
* @author Ilayaperumal Gopinathan
* @author Oleg Zhurakousky
*/
public class ExtendedPropertiesBinderAwareChannelResolverTests extends BinderAwareChannelResolverTests {
private volatile ExtendedPropertiesBinder<MessageChannel, ExtendedConsumerProperties, ExtendedProducerProperties> binder;
private volatile List<TestBinder.TestBinding> producerBindings;
@Before
@Override
public void setupContext() throws Exception {
producerBindings = new ArrayList<>();
this.binder = new TestBinder();
BinderFactory binderFactory = new BinderFactory() {
@Override
public <T> Binder<T, ? extends ConsumerProperties, ? extends ProducerProperties> getBinder(
String configurationName, Class<? extends T> bindableType) {
return (Binder<T, ? extends ConsumerProperties, ? extends ProducerProperties>) binder;
}
};
this.bindingServiceProperties = new BindingServiceProperties();
Map<String, BindingProperties> bindings = new HashMap<String, BindingProperties>();
BindingProperties bindingProperties = new BindingProperties();
bindingProperties.setContentType("text/plain");
bindings.put("foo", bindingProperties);
this.bindingServiceProperties.setBindings(bindings);
MessageConverterConfigurer messageConverterConfigurer = new MessageConverterConfigurer(
this.bindingServiceProperties,
new CompositeMessageConverterFactory());
messageConverterConfigurer.setBeanFactory(Mockito.mock(ConfigurableListableBeanFactory.class));
messageConverterConfigurer.afterPropertiesSet();
this.bindingTargetFactory = new SubscribableChannelBindingTargetFactory(messageConverterConfigurer);
dynamicDestinationsBindable = new DynamicDestinationsBindable();
BindingService bindingService = new BindingService(bindingServiceProperties,
binderFactory);
this.resolver = new BinderAwareChannelResolver(bindingService, this.bindingTargetFactory,
dynamicDestinationsBindable);
this.resolver.setBeanFactory(context.getBeanFactory());
context.getBeanFactory().registerSingleton("channelResolver", this.resolver);
context.getBeanFactory().registerSingleton("dynamicDestinationBindable", this.dynamicDestinationsBindable);
context.registerSingleton("other", DirectChannel.class);
context.registerSingleton(IntegrationUtils.INTEGRATION_MESSAGE_BUILDER_FACTORY_BEAN_NAME,
DefaultMessageBuilderFactory.class);
context.getBeanFactory().registerSingleton("bindingService", bindingService);
context.registerSingleton("inputBindingLifecycle", InputBindingLifecycle.class);
context.registerSingleton("outputBindingLifecycle", OutputBindingLifecycle.class);
context.refresh();
}
@Test
@Override
public void resolveChannel() {
assertThat(producerBindings).hasSize(0);
Map<String, Bindable> bindables = context.getBeansOfType(Bindable.class);
assertThat(bindables).hasSize(1);
for (Bindable bindable : bindables.values()) {
assertEquals(0, bindable.getInputs().size()); // producer
assertEquals(0, bindable.getOutputs().size());// consumer
}
MessageChannel registered = resolver.resolveDestination("foo");
assertThat(producerBindings).hasSize(1);
TestBinder.TestBinding binding = producerBindings.get(0);
assertThat(binding.isBound()).describedAs("Must be bound");
bindables = context.getBeansOfType(Bindable.class);
assertThat(bindables).hasSize(1);
for (Bindable bindable : bindables.values()) {
assertEquals(0, bindable.getInputs().size()); // producer
assertEquals(1, bindable.getOutputs().size());// consumer
}
DirectChannel testChannel = new DirectChannel();
final CountDownLatch latch = new CountDownLatch(1);
final List<Message<?>> received = new ArrayList<>();
@@ -124,7 +71,7 @@ public class ExtendedPropertiesBinderAwareChannelResolverTests extends BinderAwa
latch.countDown();
}
});
binder.bindConsumer("foo", null, testChannel, new ExtendedConsumerProperties(new ConsumerProperties()));
binder.bindConsumer("foo", null, testChannel, new ExtendedConsumerProperties<ConsumerProperties>(new ConsumerProperties()));
assertThat(received).hasSize(0);
registered.send(MessageBuilder.withPayload("hello").build());
try {
@@ -137,79 +84,9 @@ public class ExtendedPropertiesBinderAwareChannelResolverTests extends BinderAwa
assertThat(received).hasSize(1);
assertThat(new String((byte[])received.get(0).getPayload())).isEqualTo("hello");
context.close();
assertThat(producerBindings).hasSize(1);
assertThat(binding.isBound()).isFalse().describedAs("Must not be bound");
}
/**
* A simple test binder that creates queues for the destinations. Ignores groups.
*/
class TestBinder implements
ExtendedPropertiesBinder<MessageChannel, ExtendedConsumerProperties, ExtendedProducerProperties> {
private final Map<String, DirectChannel> destinations = new ConcurrentHashMap<>();
@Override
public Binding<MessageChannel> bindConsumer(String name, String group,
MessageChannel inboundBindTarget, ExtendedConsumerProperties properties) {
synchronized (destinations) {
if (!destinations.containsKey(name)) {
destinations.put(name, new DirectChannel());
}
}
DirectHandler directHandler = new DirectHandler(inboundBindTarget);
destinations.get(name).subscribe(directHandler);
return new TestBinding(name, directHandler);
}
@Override
public Binding<MessageChannel> bindProducer(String name,
MessageChannel outboundBindTarget, ExtendedProducerProperties properties) {
synchronized (destinations) {
if (!destinations.containsKey(name)) {
destinations.put(name, new DirectChannel());
}
}
DirectHandler directHandler = new DirectHandler(destinations.get(name));
// for test purposes we can assume it is a SubscribableChannel
((SubscribableChannel) outboundBindTarget).subscribe(directHandler);
TestBinding binding = new TestBinding(name, directHandler);
producerBindings.add(binding);
return binding;
}
@Override
public ExtendedConsumerProperties getExtendedConsumerProperties(String channelName) {
return new ExtendedConsumerProperties(new ConsumerProperties());
}
@Override
public ExtendedProducerProperties getExtendedProducerProperties(String channelName) {
return new ExtendedProducerProperties(new ProducerProperties());
}
private final class TestBinding implements Binding<MessageChannel> {
private final String name;
private final DirectHandler directHandler;
private boolean bound = true;
private TestBinding(String name, DirectHandler directHandler) {
this.name = name;
this.directHandler = directHandler;
}
@Override
public void unbind() {
bound = false;
destinations.get(name).unsubscribe(directHandler);
}
public boolean isBound() {
return bound;
}
for (Bindable bindable : bindables.values()) {
assertEquals(0, bindable.getInputs().size());
assertEquals(0, bindable.getOutputs().size());//Must not be bound"
}
}
}

View File

@@ -0,0 +1,41 @@
/*
* 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.cloud.stream.binder.integration;
import org.springframework.messaging.SubscribableChannel;
/**
* @author Oleg Zhurakousky
*
*/
abstract class AbstractDestination {
private SubscribableChannel channel;
SubscribableChannel getChannel() {
return channel;
}
void setChannel(SubscribableChannel channel) {
this.channel = channel;
this.afterChannelIsSet();
}
void afterChannelIsSet() {
// noop
}
}

View File

@@ -0,0 +1,71 @@
/*
* 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.cloud.stream.binder.integration;
import java.nio.charset.StandardCharsets;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Processor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Import;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.support.GenericMessage;
import static org.junit.Assert.assertEquals;
/**
* Sample spring cloud stream application that demonstrates the usage of {@link SpringIntegrationChannelBinder}.
*
* @author Oleg Zhurakousky
*
*/
@SpringBootApplication
@EnableBinding(Processor.class)
@Import(SpringIntegrationBinderConfiguration.class)
public class SampleStreamApp {
public static void main(String[] args) {
ApplicationContext context = new SpringApplicationBuilder(SampleStreamApp.class).web(WebApplicationType.NONE)
.run("--server.port=0");
SourceDestination source = context.getBean(SourceDestination.class);
TargetDestination target = context.getBean(TargetDestination.class);
source.send(new GenericMessage<byte[]>("Hello".getBytes()));
Message<?> message = target.receive();
assertEquals("Hello", new String((byte[])message.getPayload(), StandardCharsets.UTF_8));
}
@StreamListener(Processor.INPUT)
@SendTo(Processor.OUTPUT)
public String receive(String value) {
System.out.println("Handling payload: " + value);
return value;
}
@ServiceActivator(inputChannel="input.anonymous.errors")
public void error(String value) {
System.out.println("Handling ERROR payload: " + value);
}
}

View File

@@ -0,0 +1,40 @@
/*
* 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.cloud.stream.binder.integration;
import org.springframework.messaging.Message;
/**
* Implementation of binder endpoint that represents the source destination
* (e.g., destination from which messages will be received by Processor.INPUT).
* <br>
* You can interact with it by calling {@link #send(Message)} operation.
*
* @author Oleg Zhurakousky
*
*/
public class SourceDestination extends AbstractDestination {
/**
* Allows the {@link Message} to be sent to a Binder to be delegated
* to binder's input destination (e.g., Processor.INPUT).
*
*/
public void send(Message<?> message) {
this.getChannel().send(message);
}
}

View File

@@ -0,0 +1,99 @@
/*
* 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.cloud.stream.binder.integration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.binder.Binder;
import org.springframework.cloud.stream.binder.BinderType;
import org.springframework.cloud.stream.binder.BinderTypeRegistry;
import org.springframework.cloud.stream.binder.ConsumerProperties;
import org.springframework.cloud.stream.binder.DefaultBinderTypeRegistry;
import org.springframework.cloud.stream.binder.ProducerProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.integration.config.EnableIntegration;
/**
* {@link Binder} configuration backed by Spring Integration.
*
* Please see {@link SpringIntegrationChannelBinder} for more details.
*
* @author Oleg Zhurakousky
*
* @see SpringIntegrationChannelBinder
*/
@Configuration
@ConditionalOnMissingBean(Binder.class)
@EnableIntegration
public class SpringIntegrationBinderConfiguration<T> {
public static final String NAME = "integration";
/**
* Utility operation to return an array of configuration classes
* defined in {@link EnableBinding} annotation.
* Typically used for tests that do not rely on creating an SCSt boot
* application annotated with {@link EnableBinding}, yet require
* full {@link Binder} configuration.
*/
public static Class<?>[] getCompleteConfiguration() {
List<Class<?>> configClasses = new ArrayList<>();
configClasses.add(SpringIntegrationBinderConfiguration.class);
Import annotation = AnnotationUtils.getAnnotation(EnableBinding.class, Import.class);
Map<String, Object> annotationAttributes = AnnotationUtils.getAnnotationAttributes(annotation);
configClasses.addAll(Arrays.asList((Class<?>[])annotationAttributes.get("value")));
return configClasses.toArray(new Class<?>[] {});
}
@Bean
public BinderTypeRegistry binderTypeRegistry() {
BinderType binderType = new BinderType(NAME, new Class[] {SpringIntegrationBinderConfiguration.class});
BinderTypeRegistry btr = new DefaultBinderTypeRegistry(Collections.singletonMap(NAME, binderType));
return btr;
}
@Bean
public SourceDestination sourceDestination() {
return new SourceDestination();
}
@Bean
public TargetDestination targetDestination() {
return new TargetDestination();
}
@SuppressWarnings("unchecked")
@Bean
public Binder<T, ? extends ConsumerProperties, ? extends ProducerProperties> springIntegrationChannelBinder(SpringIntegrationProvisioner provisioner) {
return (Binder<T, ? extends ConsumerProperties, ? extends ProducerProperties>) new SpringIntegrationChannelBinder(provisioner);
}
@Bean
public SpringIntegrationProvisioner springIntegrationProvisioner() {
return new SpringIntegrationProvisioner();
}
}

View File

@@ -0,0 +1,257 @@
/*
* Copyright 2015-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.cloud.stream.binder.integration;
import java.util.function.Consumer;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.stream.binder.AbstractMessageChannelBinder;
import org.springframework.cloud.stream.binder.Binder;
import org.springframework.cloud.stream.binder.ConsumerProperties;
import org.springframework.cloud.stream.binder.ProducerProperties;
import org.springframework.cloud.stream.binder.integration.SpringIntegrationProvisioner.SpringIntegrationConsumerDestination;
import org.springframework.cloud.stream.binder.integration.SpringIntegrationProvisioner.SpringIntegrationProducerDestination;
import org.springframework.cloud.stream.provisioning.ConsumerDestination;
import org.springframework.cloud.stream.provisioning.ProducerDestination;
import org.springframework.core.AttributeAccessor;
import org.springframework.integration.core.MessageProducer;
import org.springframework.integration.endpoint.MessageProducerSupport;
import org.springframework.integration.handler.BridgeHandler;
import org.springframework.integration.support.DefaultErrorMessageStrategy;
import org.springframework.integration.support.ErrorMessageStrategy;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessagingException;
import org.springframework.messaging.SubscribableChannel;
import org.springframework.retry.RecoveryCallback;
import org.springframework.retry.RetryCallback;
import org.springframework.retry.RetryContext;
import org.springframework.retry.RetryListener;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Implementation of {@link Binder} backed by Spring Integration framework.
* It is useful for localized demos and testing.
* <p>
* This binder extends from the same base class ({@link AbstractMessageChannelBinder}) as
* other binders (i.e., Rabbit, Kafka etc). Interaction with this binder is done via source
* and target destination which emulate real binder's destinations (i.e., Kafka topic)
* <br>
* The destination classes are
* <ul>
* <li>{@link SourceDestination}</li>
* <li>{@link TargetDestination}</li>
* </ul>
* Simply autowire them in your your application and send/receive messages.
* </p>
* You must also add {@link SpringIntegrationBinderConfiguration} to your configuration.
* Below is the example using Spring Boot test.
* <pre class="code">
*
* &#064;RunWith(SpringJUnit4ClassRunner.class)
* &#064;SpringBootTest(classes = {SpringIntegrationBinderConfiguration.class, TestWithSIBinder.MyProcessor.class})
* public class TestWithSIBinder {
* &#064;Autowired
* private SourceDestination sourceDestination;
*
* &#064;Autowired
* private TargetDestination targetDestination;
*
* &#064;Test
* public void testWiring() {
* sourceDestination.send(new GenericMessage<String>("Hello"));
* assertEquals("Hello world", new String((byte[])targetDestination.receive().getPayload(), StandardCharsets.UTF_8));
* }
*
* &#064;SpringBootApplication
* &#064;EnableBinding(Processor.class)
* public static class MyProcessor {
* &#064;StreamListener(Processor.INPUT)
* &#064;SendTo(Processor.OUTPUT)
* public String transform(String in) {
* return in + " world";
* }
* }
* }
* </pre>
*
* @author Oleg Zhurakousky
*/
class SpringIntegrationChannelBinder extends AbstractMessageChannelBinder<ConsumerProperties,
ProducerProperties, SpringIntegrationProvisioner> {
@Autowired
private BeanFactory beanFactory;
SpringIntegrationChannelBinder(SpringIntegrationProvisioner provisioningProvider) {
super(new String[] {}, provisioningProvider);
}
@Override
protected MessageHandler createProducerMessageHandler(ProducerDestination destination,
ProducerProperties producerProperties, MessageChannel errorChannel) throws Exception {
BridgeHandler handler = new BridgeHandler();
handler.setBeanFactory(this.beanFactory);
handler.setOutputChannel(((SpringIntegrationProducerDestination)destination).getChannel());
return handler;
}
@Override
protected MessageProducer createConsumerEndpoint(ConsumerDestination destination, String group, ConsumerProperties properties)
throws Exception {
ErrorMessageStrategy errorMessageStrategy = new DefaultErrorMessageStrategy();
SubscribableChannel siBinderInputChannel = ((SpringIntegrationConsumerDestination)destination).getChannel();
IntegrationMessageListeningContainer messageListenerContainer = new IntegrationMessageListeningContainer();
IntegrationBinderInboundChannelAdapter adapter = new IntegrationBinderInboundChannelAdapter(messageListenerContainer);
String groupName = StringUtils.hasText(group) ? group : "anonymous";
ErrorInfrastructure errorInfrastructure = registerErrorInfrastructure(destination, groupName, properties);
if (properties.getMaxAttempts() > 1) {
adapter.setRetryTemplate(buildRetryTemplate(properties));
}
else {
adapter.setErrorMessageStrategy(errorMessageStrategy);
adapter.setErrorChannel(errorInfrastructure.getErrorChannel());
}
siBinderInputChannel.subscribe(messageListenerContainer);
return adapter;
}
/**
* Implementation of simple message listener container modeled after AMQP SimpleMessageListenerContainer
*/
private static class IntegrationMessageListeningContainer implements MessageHandler {
private Consumer<Message<?>> listener;
@Override
public void handleMessage(Message<?> message) throws MessagingException {
this.listener.accept(message);
}
public void setMessageListener(Consumer<Message<?>> listener) {
this.listener = listener;
}
}
/**
* Implementation of inbound channel adapter modeled after AmqpInboundChannelAdapter
*/
private static class IntegrationBinderInboundChannelAdapter extends MessageProducerSupport {
private static final ThreadLocal<AttributeAccessor> attributesHolder = new ThreadLocal<AttributeAccessor>();
private final IntegrationMessageListeningContainer listenerContainer;
private RetryTemplate retryTemplate;
private RecoveryCallback<? extends Object> recoveryCallback;
IntegrationBinderInboundChannelAdapter(IntegrationMessageListeningContainer listenerContainer) {
this.listenerContainer = listenerContainer;
}
@SuppressWarnings("unused")
// Temporarily unused until DLQ strategy for this binder becomes a requirement
public void setRecoveryCallback(RecoveryCallback<? extends Object> recoveryCallback) {
this.recoveryCallback = recoveryCallback;
}
public void setRetryTemplate(RetryTemplate retryTemplate) {
this.retryTemplate = retryTemplate;
}
@Override
protected void onInit() {
if (this.retryTemplate != null) {
Assert.state(getErrorChannel() == null, "Cannot have an 'errorChannel' property when a 'RetryTemplate' is "
+ "provided; use an 'ErrorMessageSendingRecoverer' in the 'recoveryCallback' property to "
+ "send an error message when retries are exhausted");
}
Listener messageListener = new Listener();
if (this.retryTemplate != null) {
this.retryTemplate.registerListener(messageListener);
}
this.listenerContainer.setMessageListener(messageListener);
}
protected class Listener implements RetryListener, Consumer<Message<?>> {
@Override
@SuppressWarnings("unchecked")
public void accept(Message<?> message) {
try {
if (IntegrationBinderInboundChannelAdapter.this.retryTemplate == null) {
try {
processMessage(message);
}
finally {
attributesHolder.remove();
}
}
else {
IntegrationBinderInboundChannelAdapter.this.retryTemplate.execute(context -> {
processMessage(message);
return null;
},
(RecoveryCallback<Object>) IntegrationBinderInboundChannelAdapter.this.recoveryCallback);
}
}
catch (RuntimeException e) {
if (getErrorChannel() != null) {
getMessagingTemplate().send(getErrorChannel(), buildErrorMessage(null,
new IllegalStateException("Message conversion failed: " + message, e)));
}
else {
throw e;
}
}
}
private void processMessage(Message<?> message) {
sendMessage(message);
}
@Override
public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {
if (IntegrationBinderInboundChannelAdapter.this.recoveryCallback != null) {
attributesHolder.set(context);
}
return true;
}
@Override
public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback,
Throwable throwable) {
attributesHolder.remove();
}
@Override
public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback,
Throwable throwable) {
// Empty
}
}
}
}

View File

@@ -0,0 +1,133 @@
/*
* 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.cloud.stream.binder.integration;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.stream.binder.Binder;
import org.springframework.cloud.stream.binder.ConsumerProperties;
import org.springframework.cloud.stream.binder.ProducerProperties;
import org.springframework.cloud.stream.provisioning.ConsumerDestination;
import org.springframework.cloud.stream.provisioning.ProducerDestination;
import org.springframework.cloud.stream.provisioning.ProvisioningException;
import org.springframework.cloud.stream.provisioning.ProvisioningProvider;
import org.springframework.integration.channel.AbstractMessageChannel;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.channel.PublishSubscribeChannel;
import org.springframework.messaging.Message;
import org.springframework.messaging.SubscribableChannel;
/**
* {@link ProvisioningProvider} to support {@link SpringIntegrationChannelBinder}. It
* exists primarily to support {@link AbstractMessageChannel} semantics for creating
* {@link ConsumerDestination} and {@link ProducerDestination}, to interact with this
* {@link Binder}.
*
* @author Oleg Zhurakousky
*
*/
class SpringIntegrationProvisioner implements ProvisioningProvider<ConsumerProperties, ProducerProperties> {
private final Map<String, SubscribableChannel> provisionedDestinations = new HashMap<>();
@Autowired
private SourceDestination source;
@Autowired
private TargetDestination target;
/**
* Will provision producer destination as an SI {@link PublishSubscribeChannel}.
* <br>
* This provides convenience of registering additional subscriber (handler in the test method)
* along side of being able to call {@link TargetDestination#receive()} to get a
* {@link Message} for additional assertions.
*/
@Override
public ProducerDestination provisionProducerDestination(String name, ProducerProperties properties) throws ProvisioningException {
SubscribableChannel destination = this.provisionDestination(name, true);
this.target.setChannel(destination);
return new SpringIntegrationProducerDestination(name, destination);
}
/**
* Will provision consumer destination as SI {@link DirectChannel}
*/
@Override
public ConsumerDestination provisionConsumerDestination(String name, String group, ConsumerProperties properties) throws ProvisioningException {
SubscribableChannel destination = this.provisionDestination(name, false);
this.source.setChannel(destination);
return new SpringIntegrationConsumerDestination(name, destination);
}
private SubscribableChannel provisionDestination(String name, boolean pubSub) {
String destinationName = name + ".destination";
SubscribableChannel destination = this.provisionedDestinations.get(destinationName);
if (destination == null) {
destination = pubSub ? new PublishSubscribeChannel() : new DirectChannel();
((AbstractMessageChannel)destination).setBeanName(destinationName);
((AbstractMessageChannel)destination).setComponentName(destinationName);
this.provisionedDestinations.put(destinationName, destination);
}
return destination;
}
class SpringIntegrationConsumerDestination implements ConsumerDestination {
private final String name;
private final SubscribableChannel channel;
SpringIntegrationConsumerDestination(String name, SubscribableChannel channel) {
this.name = name;
this.channel = channel;
}
public SubscribableChannel getChannel() {
return this.channel;
}
@Override
public String getName() {
return this.name;
}
}
class SpringIntegrationProducerDestination implements ProducerDestination {
private final String name;
private final SubscribableChannel channel;
SpringIntegrationProducerDestination(String name, SubscribableChannel channel) {
this.name = name;
this.channel = channel;
}
@Override
public String getNameForPartition(int partition) {
return this.getName() + partition;
}
public SubscribableChannel getChannel() {
return this.channel;
}
@Override
public String getName() {
return this.name;
}
}
}

View File

@@ -0,0 +1,64 @@
/*
* 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.cloud.stream.binder.integration;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.TimeUnit;
import org.springframework.messaging.Message;
/**
* Implementation of binder endpoint that represents the target destination
* (e.g., destination which receives messages sent to Processor.OUTPUT)
* <br>
* You can interact with it by calling {@link #receive()} operation.
*
* @author Oleg Zhurakousky
*
*/
public class TargetDestination extends AbstractDestination {
private BlockingQueue<Message<?>> messages;
/**
* Allows to access {@link Message}s received by this {@link TargetDestination}.
* @param timeout how long to wait before giving up
*/
public Message<?> receive(long timeout) {
try {
return this.messages.poll(timeout, TimeUnit.MILLISECONDS);
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return null;
}
/**
* Allows to access {@link Message}s received by this {@link TargetDestination}.
*/
public Message<?> receive() {
return this.receive(0);
}
@Override
void afterChannelIsSet() {
this.messages = new LinkedTransferQueue<>();
this.getChannel().subscribe(message -> messages.offer(message));
}
}