Binding error channel

- bind Spring Integration `errorChannel` as a message producer when the property `spring.cloud.stream.bindings.error.destination` is set
    - The error channel will use this property value as the destination name
  - Add test

This resolves #329

Rename NoopBindable -> BindableAdapter

Return error channel name for getOutputs

Fix review comments

  - Modified integration test to use `Sink` interface
  - Add unit test
This commit is contained in:
Ilayaperumal Gopinathan
2016-02-19 18:00:00 +05:30
committed by Marius Bogoevici
parent d2edea84ee
commit cae56385e0
15 changed files with 248 additions and 17 deletions

View File

@@ -0,0 +1,107 @@
/*
* Copyright 2016 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.config;
import static org.junit.Assert.assertTrue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.cloud.stream.annotation.Bindings;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.binder.redis.config.RedisMessageChannelBinderConfiguration;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.cloud.stream.test.junit.redis.RedisTestSupport;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.PropertySource;
import org.springframework.integration.annotation.InboundChannelAdapter;
import org.springframework.integration.annotation.Poller;
import org.springframework.integration.core.MessageSource;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessagingException;
import org.springframework.messaging.support.ErrorMessage;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.util.Assert;
/**
* @author Ilayaperumal Gopinathan
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration({ErrorChannelTests.TestSource.class, ErrorChannelTests.TestErrorSink.class})
public class ErrorChannelTests {
@Rule
public RedisTestSupport redisTestSupport = new RedisTestSupport();
@Autowired
@Bindings(TestErrorSink.class)
private Sink testErrorSink;
@Test
public void testErrorChannelBinding() throws Exception {
final CountDownLatch latch = new CountDownLatch(1);
MessageHandler errorMessageHandler = new MessageHandler() {
@Override
public void handleMessage(Message<?> message) throws MessagingException {
Assert.isTrue(message instanceof ErrorMessage, "Message should be an instance of ErrorMessage");
Assert.isTrue(message.getPayload() instanceof MessagingException, "Message payload should be an instance" +
"of MessagingException");
Assert.isTrue(message.getPayload().toString()
.equals("org.springframework.messaging.MessagingException: test"));
latch.countDown();
}
};
testErrorSink.input().subscribe(errorMessageHandler);
assertTrue(latch.await(10, TimeUnit.SECONDS));
}
@EnableBinding(Source.class)
@EnableAutoConfiguration
@Import(RedisMessageChannelBinderConfiguration.class)
@PropertySource("classpath:/org/springframework/cloud/stream/config/errorchannel/source-channel.properties")
public static class TestSource {
@Bean
@InboundChannelAdapter(value = Source.OUTPUT, poller = @Poller(fixedDelay = "5000", maxMessagesPerPoll = "1"))
public MessageSource<String> timerMessageSource() {
return new MessageSource<String>() {
@Override
public Message<String> receive() {
throw new MessagingException("test");
}
};
}
}
@EnableBinding(Sink.class)
@EnableAutoConfiguration
@Import(RedisMessageChannelBinderConfiguration.class)
@PropertySource("classpath:/org/springframework/cloud/stream/config/errorchannel/errorsink-channel.properties")
public static class TestErrorSink {
}
}

View File

@@ -117,7 +117,7 @@ public class MessageChannelConfigurerTests {
@EnableBinding(Source.class)
@EnableAutoConfiguration
@Import(RedisMessageChannelBinderConfiguration.class)
@PropertySource("classpath:/org/springframework/cloud/stream/config/source-channel-configurers.properties")
@PropertySource("classpath:/org/springframework/cloud/stream/config/channel/source-channel-configurers.properties")
public static class TestSource {
}
@@ -125,7 +125,7 @@ public class MessageChannelConfigurerTests {
@EnableBinding(Sink.class)
@EnableAutoConfiguration
@Import(RedisMessageChannelBinderConfiguration.class)
@PropertySource("classpath:/org/springframework/cloud/stream/config/sink-channel-configurers.properties")
@PropertySource("classpath:/org/springframework/cloud/stream/config/channel/sink-channel-configurers.properties")
public static class TestSink {
}

View File

@@ -0,0 +1 @@
spring.cloud.stream.bindings.input.destination=errorchannel-test

View File

@@ -0,0 +1,2 @@
spring.cloud.stream.bindings.output.destination=source-output
spring.cloud.stream.bindings.error.destination=errorchannel-test

View File

@@ -31,7 +31,7 @@ import org.springframework.context.annotation.Import;
import org.springframework.integration.config.EnableIntegration;
/**
* Enables the binding of inputs and outputs to a broker, according to the list
* Enables the binding of {@link Input} and {@link Output} - annotated components to a broker, according to the list
* of interfaces passed as value to the annotation.
*
* @author Dave Syer

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2016 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.binding;
import java.util.Collections;
import java.util.Set;
/**
* Default adapter implementation for {@Bindable}.
*
* @author Ilayaperumal Gopinathan
*/
public class BindableAdapter implements Bindable {
@Override
public void bindInputs(ChannelBindingService adapter) {
}
@Override
public void bindOutputs(ChannelBindingService adapter) {
}
@Override
public void unbindInputs(ChannelBindingService adapter) {
}
@Override
public void unbindOutputs(ChannelBindingService adapter) {
}
@Override
public Set<String> getInputs() {
return Collections.unmodifiableSet(Collections.EMPTY_SET);
}
@Override
public Set<String> getOutputs() {
return Collections.unmodifiableSet(Collections.EMPTY_SET);
}
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2016 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.binding;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.springframework.integration.channel.PublishSubscribeChannel;
/**
* A {@link Bindable} component that represents an error channel.
*
* @author Ilayaperumal Gopinathan
*/
public class BindableErrorChannel extends BindableAdapter {
private final String name;
private final PublishSubscribeChannel errorChannel;
public BindableErrorChannel(String name, PublishSubscribeChannel errorChannel) {
this.name = name;
this.errorChannel = errorChannel;
}
@Override
public void bindOutputs(ChannelBindingService adapter) {
adapter.bindProducer(errorChannel, name);
}
@Override
public void unbindOutputs(ChannelBindingService adapter) {
adapter.unbindProducers(name);
}
@Override
public Set<String> getOutputs() {
return Collections.unmodifiableSet(new HashSet<String>(Arrays.asList(name)));
}
}

View File

@@ -17,7 +17,6 @@
package org.springframework.cloud.stream.binding;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;

View File

@@ -24,13 +24,16 @@ import java.util.Map;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationPropertiesBinding;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.stream.binder.BinderFactory;
import org.springframework.cloud.stream.binding.BindableChannelFactory;
import org.springframework.cloud.stream.binding.BindableErrorChannel;
import org.springframework.cloud.stream.binding.BinderAwareChannelResolver;
import org.springframework.cloud.stream.binding.BinderAwareRouterBeanPostProcessor;
import org.springframework.cloud.stream.binding.ChannelBindingService;
@@ -47,6 +50,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.core.convert.converter.Converter;
import org.springframework.expression.PropertyAccessor;
import org.springframework.integration.channel.PublishSubscribeChannel;
import org.springframework.integration.config.IntegrationEvaluationContextFactoryBean;
import org.springframework.integration.context.IntegrationContextUtils;
import org.springframework.integration.json.JsonPropertyAccessor;
@@ -69,6 +73,8 @@ import org.springframework.tuple.spel.TuplePropertyAccessor;
@EnableConfigurationProperties(ChannelBindingServiceProperties.class)
public class ChannelBindingServiceConfiguration {
private static final String ERROR_CHANNEL_NAME = "error";
@Autowired
MessageBuilderFactory messageBuilderFactory;
@@ -201,4 +207,11 @@ public class ChannelBindingServiceConfiguration {
}
}
@Bean
@ConditionalOnProperty("spring.cloud.stream.bindings." + ERROR_CHANNEL_NAME + ".destination")
public BindableErrorChannel bindableErrorChannel(
@Qualifier(IntegrationContextUtils.ERROR_CHANNEL_BEAN_NAME) PublishSubscribeChannel errorChannel) {
return new BindableErrorChannel(ERROR_CHANNEL_NAME, errorChannel);
}
}

View File

@@ -27,6 +27,7 @@ import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.cloud.stream.annotation.Bindings;
@@ -35,10 +36,13 @@ import org.springframework.cloud.stream.messaging.Source;
import org.springframework.cloud.stream.utils.MockBinderRegistryConfiguration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.PropertySource;
import org.springframework.integration.channel.PublishSubscribeChannel;
import org.springframework.integration.context.IntegrationContextUtils;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @author Marius Bogoevici
* @author Ilayaperumal Gopinathan
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(SourceBindingTestsWithBindingTargets.TestSource.class)
@@ -51,10 +55,16 @@ public class SourceBindingTestsWithBindingTargets {
@Autowired @Bindings(TestSource.class)
private Source testSource;
@Autowired
@Qualifier(IntegrationContextUtils.ERROR_CHANNEL_BEAN_NAME)
private PublishSubscribeChannel errorChannel;
@SuppressWarnings("unchecked")
@Test
public void testSourceOutputChannelBound() {
verify(binder).bindProducer(eq("testtock"), eq(testSource.output()), Mockito.<Properties>any());
//Check error channel binding
verify(binder).bindProducer(eq("error-test"), eq(errorChannel), Mockito.<Properties>any());
verifyNoMoreInteractions(binder);
}

View File

@@ -27,7 +27,6 @@ import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatcher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
@@ -77,14 +76,4 @@ public class PartitionedProducerTest {
}
class PropertiesArgumentMatcher extends ArgumentMatcher<Properties> {
@Override
public boolean matches(Object argument) {
if (!(argument instanceof Properties)) {
return false;
}
return true;
}
}
}

View File

@@ -1 +1 @@
spring.cloud.stream.bindings.input=testtock
spring.cloud.stream.bindings.input.destination=testtock

View File

@@ -1 +1,2 @@
spring.cloud.stream.bindings.output=testtock
spring.cloud.stream.bindings.output.destination=testtock
spring.cloud.stream.bindings.error.destination=error-test