diff --git a/spring-cloud-stream-binders/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/KafkaMessageChannelBinder.java b/spring-cloud-stream-binders/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/KafkaMessageChannelBinder.java index 7e689b392..249f7f13c 100644 --- a/spring-cloud-stream-binders/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/KafkaMessageChannelBinder.java +++ b/spring-cloud-stream-binders/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/KafkaMessageChannelBinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-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. @@ -117,11 +117,13 @@ import scala.collection.Seq; * that better be greater than number of containers * * + * * @author Eric Bottard * @author Marius Bogoevici * @author Ilayaperumal Gopinathan * @author David Turanski * @author Gary Russell + * @author Mark Fisher */ public class KafkaMessageChannelBinder extends MessageChannelBinderSupport { @@ -184,12 +186,6 @@ public class KafkaMessageChannelBinder extends MessageChannelBinderSupport { KafkaMessageChannelBinder.COMPRESSION_CODEC, })); - /** - * The consumer group to use when achieving point to point semantics (that consumer group name is static and hence - * shared by all containers). - */ - private static final String POINT_TO_POINT_SEMANTICS_CONSUMER_GROUP = "springXD"; - private static final Set KAFKA_CONSUMER_PROPERTIES = new SetBuilder() .add(BinderPropertyKeys.MIN_PARTITION_COUNT) .build(); @@ -210,25 +206,12 @@ public class KafkaMessageChannelBinder extends MessageChannelBinderSupport { .add(BinderPropertyKeys.MIN_PARTITION_COUNT) .build(); - /** - * Basic + concurrency. - */ - private static final Set SUPPORTED_NAMED_CONSUMER_PROPERTIES = new SetBuilder() - .addAll(CONSUMER_STANDARD_PROPERTIES) - .build(); - - private static final Set SUPPORTED_NAMED_PRODUCER_PROPERTIES = new SetBuilder() - .addAll(PRODUCER_STANDARD_PROPERTIES) - .addAll(PRODUCER_BATCHING_BASIC_PROPERTIES) - .build(); - /** * Partitioning + kafka producer properties. */ private static final Set SUPPORTED_PRODUCER_PROPERTIES = new SetBuilder() .addAll(PRODUCER_PARTITIONING_PROPERTIES) .addAll(PRODUCER_STANDARD_PROPERTIES) - .add(BinderPropertyKeys.DIRECT_BINDING_ALLOWED) .addAll(KAFKA_PRODUCER_PROPERTIES) .addAll(PRODUCER_BATCHING_BASIC_PROPERTIES) .addAll(PRODUCER_COMPRESSION_PROPERTIES) @@ -246,6 +229,7 @@ public class KafkaMessageChannelBinder extends MessageChannelBinderSupport { private final String zkAddress; // -------- Default values for properties ------- + private int defaultReplicationFactor = 1; private String defaultCompressionCodec = DEFAULT_COMPRESSION_CODEC; @@ -306,7 +290,6 @@ public class KafkaMessageChannelBinder extends MessageChannelBinderSupport { else { this.headersToMap = BinderHeaders.STANDARD_HEADERS; } - } public void setOffsetStoreTopic(String offsetStoreTopic) { @@ -333,7 +316,6 @@ public class KafkaMessageChannelBinder extends MessageChannelBinderSupport { this.offsetStoreMaxFetchSize = offsetStoreMaxFetchSize; } - public void setOffsetUpdateTimeWindow(int offsetUpdateTimeWindow) { this.offsetUpdateTimeWindow = offsetUpdateTimeWindow; } @@ -460,99 +442,66 @@ public class KafkaMessageChannelBinder extends MessageChannelBinderSupport { } @Override - public void bindConsumer(String name, final MessageChannel moduleInputChannel, Properties properties) { - // Point-to-point consumers reset at the earliest time, which allows them to catch up with all messages - createKafkaConsumer(name, moduleInputChannel, properties, POINT_TO_POINT_SEMANTICS_CONSUMER_GROUP, - OffsetRequest.EarliestTime()); - bindExistingProducerDirectlyIfPossible(name, moduleInputChannel); - } - - @Override - public void bindPubSubConsumer(String name, MessageChannel inputChannel, String group, Properties properties) { + protected Binding doBindConsumer(String name, String group, MessageChannel inputChannel, Properties properties) { // If the caller provides a group, use it; otherwise // usage of a different consumer group each time achieves pub-sub // but multiple instances of this binding will each get all messages // PubSub consumers reset at the latest time, which allows them to receive only messages sent after // they've been bound String consumerGroup = group == null ? UUID.randomUUID().toString() : group; - createKafkaConsumer(name, inputChannel, properties, consumerGroup, OffsetRequest.LatestTime()); + return createKafkaConsumer(name, inputChannel, properties, consumerGroup, OffsetRequest.LatestTime()); } @Override - public void bindProducer(final String name, MessageChannel moduleOutputChannel, Properties properties) { - + public Binding bindProducer(final String name, MessageChannel moduleOutputChannel, Properties properties) { Assert.isInstanceOf(SubscribableChannel.class, moduleOutputChannel); KafkaPropertiesAccessor producerPropertiesAccessor = new KafkaPropertiesAccessor(properties); - if (name.startsWith(P2P_NAMED_CHANNEL_TYPE_PREFIX)) { - validateProducerProperties(name, properties, SUPPORTED_NAMED_PRODUCER_PROPERTIES); - } - else { - validateProducerProperties(name, properties, SUPPORTED_PRODUCER_PROPERTIES); - } - if (!bindNewProducerDirectlyIfPossible(name, (SubscribableChannel) moduleOutputChannel, - producerPropertiesAccessor)) { - if (logger.isInfoEnabled()) { - logger.info("Using kafka topic for outbound: " + name); - } - - final String topicName = escapeTopicName(name); - - int numPartitions = producerPropertiesAccessor.getNumberOfKafkaPartitionsForProducer(); - - Collection partitions = ensureTopicCreated(topicName, numPartitions, defaultReplicationFactor); - - ProducerMetadata producerMetadata = new ProducerMetadata<>( - topicName, byte[].class, byte[].class, BYTE_ARRAY_SERIALIZER, BYTE_ARRAY_SERIALIZER); - producerMetadata.setCompressionType(ProducerMetadata.CompressionType.valueOf( - producerPropertiesAccessor.getCompressionCodec(this.defaultCompressionCodec))); - producerMetadata.setBatchBytes(producerPropertiesAccessor.getBatchSize(this.defaultBatchSize)); - Properties additionalProps = new Properties(); - additionalProps.put(ProducerConfig.ACKS_CONFIG, - String.valueOf(producerPropertiesAccessor.getRequiredAcks(this - .defaultRequiredAcks))); - additionalProps.put(ProducerConfig.LINGER_MS_CONFIG, - String.valueOf(producerPropertiesAccessor.getBatchTimeout(this - .defaultBatchTimeout))); - ProducerFactoryBean producerFB = - new ProducerFactoryBean<>(producerMetadata, brokers, additionalProps); - - try { - final ProducerConfiguration producerConfiguration = new ProducerConfiguration<>( - producerMetadata, producerFB.getObject()); - - MessageHandler handler = new SendingHandler(topicName, producerPropertiesAccessor, - partitions.size(), producerConfiguration); - EventDrivenConsumer consumer = new EventDrivenConsumer((SubscribableChannel) moduleOutputChannel, - handler); - consumer.setBeanFactory(this.getBeanFactory()); - consumer.setBeanName("outbound." + name); - consumer.afterPropertiesSet(); - Binding producerBinding = Binding.forProducer(name, moduleOutputChannel, consumer, - producerPropertiesAccessor); - addBinding(producerBinding); - producerBinding.start(); - } - catch (Exception e) { - throw new RuntimeException(e); - } - + validateProducerProperties(name, properties, SUPPORTED_PRODUCER_PROPERTIES); + if (logger.isInfoEnabled()) { + logger.info("Using kafka topic for outbound: " + name); } - } + final String topicName = escapeTopicName(name); - @Override - public void bindPubSubProducer(String name, MessageChannel outputChannel, Properties properties) { - bindProducer(name, outputChannel, properties); - } + int numPartitions = producerPropertiesAccessor.getNumberOfKafkaPartitionsForProducer(); - @Override - public void bindRequestor(String name, MessageChannel requests, MessageChannel replies, Properties properties) { - throw new UnsupportedOperationException("requestor binding is not supported by this binder"); - } + Collection partitions = ensureTopicCreated(topicName, numPartitions, defaultReplicationFactor); - @Override - public void bindReplier(String name, MessageChannel requests, MessageChannel replies, Properties properties) { - throw new UnsupportedOperationException("replier binding is not supported by this binder"); + ProducerMetadata producerMetadata = new ProducerMetadata<>( + topicName, byte[].class, byte[].class, BYTE_ARRAY_SERIALIZER, BYTE_ARRAY_SERIALIZER); + producerMetadata.setCompressionType(ProducerMetadata.CompressionType.valueOf( + producerPropertiesAccessor.getCompressionCodec(this.defaultCompressionCodec))); + producerMetadata.setBatchBytes(producerPropertiesAccessor.getBatchSize(this.defaultBatchSize)); + Properties additionalProps = new Properties(); + additionalProps.put(ProducerConfig.ACKS_CONFIG, + String.valueOf(producerPropertiesAccessor.getRequiredAcks(this + .defaultRequiredAcks))); + additionalProps.put(ProducerConfig.LINGER_MS_CONFIG, + String.valueOf(producerPropertiesAccessor.getBatchTimeout(this + .defaultBatchTimeout))); + ProducerFactoryBean producerFB = + new ProducerFactoryBean<>(producerMetadata, brokers, additionalProps); + + try { + final ProducerConfiguration producerConfiguration = new ProducerConfiguration<>( + producerMetadata, producerFB.getObject()); + + MessageHandler handler = new SendingHandler(topicName, producerPropertiesAccessor, + partitions.size(), producerConfiguration); + EventDrivenConsumer consumer = new EventDrivenConsumer((SubscribableChannel) moduleOutputChannel, + handler); + consumer.setBeanFactory(this.getBeanFactory()); + consumer.setBeanName("outbound." + name); + consumer.afterPropertiesSet(); + Binding producerBinding = Binding.forProducer(name, moduleOutputChannel, consumer, + producerPropertiesAccessor); + addBinding(producerBinding); + producerBinding.start(); + return producerBinding; + } + catch (Exception e) { + throw new RuntimeException(e); + } } /** @@ -610,15 +559,10 @@ public class KafkaMessageChannelBinder extends MessageChannelBinderSupport { } } - private void createKafkaConsumer(String name, final MessageChannel moduleInputChannel, Properties properties, + private Binding createKafkaConsumer(String name, final MessageChannel moduleInputChannel, Properties properties, String group, long referencePoint) { - if (name.startsWith(P2P_NAMED_CHANNEL_TYPE_PREFIX)) { - validateConsumerProperties(name, properties, SUPPORTED_NAMED_CONSUMER_PROPERTIES); - } - else { - validateConsumerProperties(name, properties, SUPPORTED_CONSUMER_PROPERTIES); - } + validateConsumerProperties(groupedName(name, group), properties, SUPPORTED_CONSUMER_PROPERTIES); KafkaPropertiesAccessor accessor = new KafkaPropertiesAccessor(properties); int maxConcurrency = accessor.getConcurrency(defaultConcurrency); @@ -702,12 +646,13 @@ public class KafkaMessageChannelBinder extends MessageChannelBinderSupport { super.doStop(); } }; - edc.setBeanName("inbound." + name); + String groupedName = groupedName(name, group); + edc.setBeanName("inbound." + groupedName); - Binding consumerBinding = Binding.forConsumer(name, edc, moduleInputChannel, accessor); + Binding consumerBinding = Binding.forConsumer(name, group, edc, moduleInputChannel, accessor); addBinding(consumerBinding); consumerBinding.start(); - + return consumerBinding; } public KafkaMessageListenerContainer createMessageListenerContainer(Properties properties, String group, diff --git a/spring-cloud-stream-binders/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/KafkaBinderTests.java b/spring-cloud-stream-binders/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/KafkaBinderTests.java index c092ad7cd..0873b027e 100644 --- a/spring-cloud-stream-binders/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/KafkaBinderTests.java +++ b/spring-cloud-stream-binders/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/KafkaBinderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-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. @@ -29,13 +29,12 @@ import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.TimeUnit; -import kafka.api.OffsetRequest; import org.junit.ClassRule; -import org.junit.Ignore; import org.junit.Test; import org.springframework.cloud.stream.binder.Binder; import org.springframework.cloud.stream.binder.BinderPropertyKeys; +import org.springframework.cloud.stream.binder.Binding; import org.springframework.cloud.stream.binder.PartitionCapableBinderTests; import org.springframework.cloud.stream.binder.Spy; import org.springframework.cloud.stream.test.junit.kafka.KafkaTestSupport; @@ -48,12 +47,15 @@ import org.springframework.integration.kafka.listener.MessageListener; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; +import kafka.api.OffsetRequest; + /** * Integration tests for the {@link KafkaMessageChannelBinder}. * * @author Eric Bottard * @author Marius Bogoevici + * @author Mark Fisher */ public class KafkaBinderTests extends PartitionCapableBinderTests { @@ -137,8 +139,8 @@ public class KafkaBinderTests extends PartitionCapableBinderTests { if (codec != null) { props.put(KafkaMessageChannelBinder.COMPRESSION_CODEC, codec); } - binder.bindProducer("foo.0", moduleOutputChannel, props); - binder.bindConsumer("foo.0", moduleInputChannel, null); + Binding producerBinding = binder.bindProducer("foo.0", moduleOutputChannel, props); + Binding consumerBinding = binder.bindConsumer("foo.0", "test", moduleInputChannel, null); Message message = org.springframework.integration.support.MessageBuilder.withPayload(ratherBigPayload).build(); // Let the consumer actually bind to the producer before sending a msg binderBindUnbindLatency(); @@ -146,8 +148,8 @@ public class KafkaBinderTests extends PartitionCapableBinderTests { Message inbound = moduleInputChannel.receive(2000); assertNotNull(inbound); assertArrayEquals(ratherBigPayload, (byte[]) inbound.getPayload()); - binder.unbindProducers("foo.0"); - binder.unbindConsumers("foo.0"); + binder.unbind(producerBinding); + binder.unbind(consumerBinding); } } @@ -166,8 +168,8 @@ public class KafkaBinderTests extends PartitionCapableBinderTests { Properties consumerProperties = new Properties(); consumerProperties.put(BinderPropertyKeys.MIN_PARTITION_COUNT, "10"); long uniqueBindingId = System.currentTimeMillis(); - binder.bindProducer("foo" + uniqueBindingId + ".0", moduleOutputChannel, producerProperties); - binder.bindConsumer("foo" + uniqueBindingId + ".0", moduleInputChannel, consumerProperties); + Binding producerBinding = binder.bindProducer("foo" + uniqueBindingId + ".0", moduleOutputChannel, producerProperties); + Binding consumerBinding = binder.bindConsumer("foo" + uniqueBindingId + ".0", null, moduleInputChannel, consumerProperties); Message message = org.springframework.integration.support.MessageBuilder.withPayload(ratherBigPayload).build(); // Let the consumer actually bind to the producer before sending a msg binderBindUnbindLatency(); @@ -178,8 +180,8 @@ public class KafkaBinderTests extends PartitionCapableBinderTests { Collection partitions = binder.getCoreBinder().getConnectionFactory().getPartitions( "foo" + uniqueBindingId + ".0"); assertThat(partitions, hasSize(10)); - binder.unbindProducers("foo" + uniqueBindingId + ".0"); - binder.unbindConsumers("foo" + uniqueBindingId + ".0"); + binder.unbind(producerBinding); + binder.unbind(consumerBinding); } @Test @@ -199,8 +201,8 @@ public class KafkaBinderTests extends PartitionCapableBinderTests { consumerProps.put(BinderPropertyKeys.MIN_PARTITION_COUNT, "5"); consumerProps.put(BinderPropertyKeys.CONCURRENCY, "6"); long uniqueBindingId = System.currentTimeMillis(); - binder.bindProducer("foo" + uniqueBindingId + ".0", moduleOutputChannel, producerProps); - binder.bindConsumer("foo" + uniqueBindingId + ".0", moduleInputChannel, consumerProps); + Binding producerBinding = binder.bindProducer("foo" + uniqueBindingId + ".0", moduleOutputChannel, producerProps); + Binding consumerBinding = binder.bindConsumer("foo" + uniqueBindingId + ".0", null, moduleInputChannel, consumerProps); Message message = org.springframework.integration.support.MessageBuilder.withPayload(ratherBigPayload).build(); // Let the consumer actually bind to the producer before sending a msg binderBindUnbindLatency(); @@ -211,8 +213,8 @@ public class KafkaBinderTests extends PartitionCapableBinderTests { Collection partitions = binder.getCoreBinder().getConnectionFactory().getPartitions( "foo" + uniqueBindingId + ".0"); assertThat(partitions, hasSize(6)); - binder.unbindProducers("foo" + uniqueBindingId + ".0"); - binder.unbindConsumers("foo" + uniqueBindingId + ".0"); + binder.unbind(producerBinding); + binder.unbind(consumerBinding); } @Test @@ -231,8 +233,8 @@ public class KafkaBinderTests extends PartitionCapableBinderTests { consumerProps.put(BinderPropertyKeys.MIN_PARTITION_COUNT, "6"); consumerProps.put(BinderPropertyKeys.CONCURRENCY, "5"); long uniqueBindingId = System.currentTimeMillis(); - binder.bindProducer("foo" + uniqueBindingId + ".0", moduleOutputChannel, producerProps); - binder.bindConsumer("foo" + uniqueBindingId + ".0", moduleInputChannel, consumerProps); + Binding producerBinding = binder.bindProducer("foo" + uniqueBindingId + ".0", moduleOutputChannel, producerProps); + Binding consumerBinding = binder.bindConsumer("foo" + uniqueBindingId + ".0", null, moduleInputChannel, consumerProps); Message message = org.springframework.integration.support.MessageBuilder.withPayload(ratherBigPayload).build(); // Let the consumer actually bind to the producer before sending a msg binderBindUnbindLatency(); @@ -243,8 +245,8 @@ public class KafkaBinderTests extends PartitionCapableBinderTests { Collection partitions = binder.getCoreBinder().getConnectionFactory().getPartitions( "foo" + uniqueBindingId + ".0"); assertThat(partitions, hasSize(6)); - binder.unbindProducers("foo" + uniqueBindingId + ".0"); - binder.unbindConsumers("foo" + uniqueBindingId + ".0"); + binder.unbind(producerBinding); + binder.unbind(consumerBinding); } @Test @@ -263,8 +265,8 @@ public class KafkaBinderTests extends PartitionCapableBinderTests { Properties consumerProperties = new Properties(); consumerProperties.put(BinderPropertyKeys.MIN_PARTITION_COUNT, "3"); long uniqueBindingId = System.currentTimeMillis(); - binder.bindProducer("foo" + uniqueBindingId + ".0", moduleOutputChannel, producerProperties); - binder.bindConsumer("foo" + uniqueBindingId + ".0", moduleInputChannel, consumerProperties); + Binding producerBinding = binder.bindProducer("foo" + uniqueBindingId + ".0", moduleOutputChannel, producerProperties); + Binding consumerBinding = binder.bindConsumer("foo" + uniqueBindingId + ".0", null, moduleInputChannel, consumerProperties); Message message = org.springframework.integration.support.MessageBuilder.withPayload(ratherBigPayload).build(); // Let the consumer actually bind to the producer before sending a msg binderBindUnbindLatency(); @@ -275,8 +277,8 @@ public class KafkaBinderTests extends PartitionCapableBinderTests { Collection partitions = binder.getCoreBinder().getConnectionFactory().getPartitions( "foo" + uniqueBindingId + ".0"); assertThat(partitions, hasSize(5)); - binder.unbindProducers("foo" + uniqueBindingId + ".0"); - binder.unbindConsumers("foo" + uniqueBindingId + ".0"); + binder.unbind(producerBinding); + binder.unbind(consumerBinding); } @Test @@ -295,8 +297,8 @@ public class KafkaBinderTests extends PartitionCapableBinderTests { Properties consumerProperties = new Properties(); consumerProperties.put(BinderPropertyKeys.MIN_PARTITION_COUNT, "5"); long uniqueBindingId = System.currentTimeMillis(); - binder.bindProducer("foo" + uniqueBindingId + ".0", moduleOutputChannel, producerProperties); - binder.bindConsumer("foo" + uniqueBindingId + ".0", moduleInputChannel, consumerProperties); + Binding producerBinding = binder.bindProducer("foo" + uniqueBindingId + ".0", moduleOutputChannel, producerProperties); + Binding consumerBinding = binder.bindConsumer("foo" + uniqueBindingId + ".0", null, moduleInputChannel, consumerProperties); Message message = org.springframework.integration.support.MessageBuilder.withPayload(ratherBigPayload).build(); // Let the consumer actually bind to the producer before sending a msg binderBindUnbindLatency(); @@ -307,22 +309,8 @@ public class KafkaBinderTests extends PartitionCapableBinderTests { Collection partitions = binder.getCoreBinder().getConnectionFactory().getPartitions( "foo" + uniqueBindingId + ".0"); assertThat(partitions, hasSize(5)); - binder.unbindProducers("foo" + uniqueBindingId + ".0"); - binder.unbindConsumers("foo" + uniqueBindingId + ".0"); + binder.unbind(producerBinding); + binder.unbind(consumerBinding); } - @Override @Ignore("https://github.com/spring-cloud/spring-cloud-stream/issues/243") - public void testSendAndReceivePubSub() throws Exception { - } - - @Override @Ignore("https://github.com/spring-cloud/spring-cloud-stream/issues/243") - public void createInboundPubSubBeforeOutboundPubSub() throws Exception { - } - - @Test - @Ignore("Kafka binder does not support direct binding") - @Override - public void testDirectBinding() throws Exception { - - } } diff --git a/spring-cloud-stream-binders/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/RawModeKafkaBinderTests.java b/spring-cloud-stream-binders/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/RawModeKafkaBinderTests.java index 38d0cd092..394c0f06d 100644 --- a/spring-cloud-stream-binders/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/RawModeKafkaBinderTests.java +++ b/spring-cloud-stream-binders/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/RawModeKafkaBinderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-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. @@ -33,28 +33,24 @@ import org.junit.Ignore; import org.junit.Test; import org.springframework.cloud.stream.binder.Binder; -import org.springframework.cloud.stream.binder.BinderHeaders; import org.springframework.cloud.stream.binder.BinderPropertyKeys; import org.springframework.cloud.stream.binder.Binding; import org.springframework.cloud.stream.binder.TestUtils; import org.springframework.integration.IntegrationMessageHeaderAccessor; import org.springframework.integration.channel.DirectChannel; import org.springframework.integration.channel.QueueChannel; -import org.springframework.integration.channel.interceptor.WireTap; import org.springframework.integration.endpoint.AbstractEndpoint; import org.springframework.integration.support.MessageBuilder; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.support.GenericMessage; /** * @author Marius Bogoevici * @author David Turanski * @author Gary Russell + * @author Mark Fisher */ - -@Ignore public class RawModeKafkaBinderTests extends KafkaBinderTests { @Override @@ -74,9 +70,9 @@ public class RawModeKafkaBinderTests extends KafkaBinderTests { DirectChannel output = new DirectChannel(); output.setBeanName("test.output"); - binder.bindProducer("partJ.0", output, properties); + Binding outputBinding = binder.bindProducer("partJ.0", output, properties); @SuppressWarnings("unchecked") - List bindings = TestUtils.getPropertyValue(binder, "binder.bindings", List.class); + List> bindings = TestUtils.getPropertyValue(binder, "binder.bindings", List.class); assertEquals(1, bindings.size()); properties.clear(); @@ -85,15 +81,15 @@ public class RawModeKafkaBinderTests extends KafkaBinderTests { properties.put("partitionIndex", "0"); QueueChannel input0 = new QueueChannel(); input0.setBeanName("test.input0J"); - binder.bindConsumer("partJ.0", input0, properties); + Binding input0Binding = binder.bindConsumer("partJ.0", "test", input0, properties); properties.put("partitionIndex", "1"); QueueChannel input1 = new QueueChannel(); input1.setBeanName("test.input1J"); - binder.bindConsumer("partJ.0", input1, properties); + Binding input1Binding = binder.bindConsumer("partJ.0", "test", input1, properties); properties.put("partitionIndex", "2"); QueueChannel input2 = new QueueChannel(); input2.setBeanName("test.input2J"); - binder.bindConsumer("partJ.0", input2, properties); + Binding input2Binding = binder.bindConsumer("partJ.0", "test", input2, properties); output.send(new GenericMessage<>(new byte[]{(byte)0})); output.send(new GenericMessage<>(new byte[]{(byte)1})); @@ -112,8 +108,10 @@ public class RawModeKafkaBinderTests extends KafkaBinderTests { ((byte[]) receive2.getPayload())[0]), containsInAnyOrder((byte)0, (byte)1, (byte)2)); - binder.unbindConsumers("partJ.0"); - binder.unbindProducers("partJ.0"); + binder.unbind(input0Binding); + binder.unbind(input1Binding); + binder.unbind(input2Binding); + binder.unbind(outputBinding); } @Test @@ -128,9 +126,9 @@ public class RawModeKafkaBinderTests extends KafkaBinderTests { DirectChannel output = new DirectChannel(); output.setBeanName("test.output"); - binder.bindProducer("part.0", output, properties); + Binding outputBinding = binder.bindProducer("part.0", output, properties); @SuppressWarnings("unchecked") - List bindings = TestUtils.getPropertyValue(binder, "binder.bindings", List.class); + List> bindings = TestUtils.getPropertyValue(binder, "binder.bindings", List.class); assertEquals(1, bindings.size()); try { AbstractEndpoint endpoint = bindings.get(0).getEndpoint(); @@ -146,15 +144,15 @@ public class RawModeKafkaBinderTests extends KafkaBinderTests { properties.put("count","3"); QueueChannel input0 = new QueueChannel(); input0.setBeanName("test.input0S"); - binder.bindConsumer("part.0", input0, properties); + Binding input0Binding = binder.bindConsumer("part.0", "test", input0, properties); properties.put("partitionIndex", "1"); QueueChannel input1 = new QueueChannel(); input1.setBeanName("test.input1S"); - binder.bindConsumer("part.0", input1, properties); + Binding input1Binding = binder.bindConsumer("part.0", "test", input1, properties); properties.put("partitionIndex", "2"); QueueChannel input2 = new QueueChannel(); input2.setBeanName("test.input2S"); - binder.bindConsumer("part.0", input2, properties); + Binding input2Binding = binder.bindConsumer("part.0", "test", input2, properties); Message message2 = MessageBuilder.withPayload(new byte[]{2}) .setHeader(IntegrationMessageHeaderAccessor.CORRELATION_ID, "foo") @@ -180,74 +178,10 @@ public class RawModeKafkaBinderTests extends KafkaBinderTests { ((byte[]) receive2.getPayload())[0]), containsInAnyOrder((byte)0, (byte)1, (byte)2)); - binder.unbindConsumers("part.0"); - binder.unbindProducers("part.0"); - } - - @Test - @Override - @Ignore - public void createInboundPubSubBeforeOutboundPubSub() throws Exception { - Binder binder = getBinder(); - DirectChannel moduleOutputChannel = new DirectChannel(); - // Test pub/sub by emulating how StreamPlugin handles taps - DirectChannel tapChannel = new DirectChannel(); - QueueChannel moduleInputChannel = new QueueChannel(); - QueueChannel module2InputChannel = new QueueChannel(); - QueueChannel module3InputChannel = new QueueChannel(); - // Create the tap first - String fooTapName = "baz.0"; - binder.bindPubSubConsumer(fooTapName, module2InputChannel, null, null); - - // Then create the stream - binder.bindProducer("baz.0", moduleOutputChannel, null); - binder.bindConsumer("baz.0", moduleInputChannel, null); - moduleOutputChannel.addInterceptor(new WireTap(tapChannel)); - binder.bindPubSubProducer(fooTapName, tapChannel, null); - - // Another new module is using tap as an input channel - String barTapName = "baz.0"; - binder.bindPubSubConsumer(barTapName, module3InputChannel, null, null); - Message message = MessageBuilder.withPayload("foo".getBytes()).setHeader(MessageHeaders.CONTENT_TYPE, "foo/bar").build(); - boolean success = false; - boolean retried = false; - while (!success) { - moduleOutputChannel.send(message); - Message inbound = moduleInputChannel.receive(5000); - assertNotNull(inbound); - assertEquals("foo", new String((byte[])inbound.getPayload())); - Message tapped1 = module2InputChannel.receive(5000); - Message tapped2 = module3InputChannel.receive(5000); - if (tapped1 == null || tapped2 == null) { - // listener may not have started - assertFalse("Failed to receive tap after retry", retried); - retried = true; - continue; - } - success = true; - assertEquals("foo", new String((byte[]) tapped1.getPayload())); - assertNull(tapped1.getHeaders().get(BinderHeaders.BINDER_ORIGINAL_CONTENT_TYPE)); - assertEquals("foo", new String((byte[])tapped2.getPayload())); - } - // delete one tap stream is deleted - binder.unbindConsumer(barTapName, module3InputChannel); - Message message2 = MessageBuilder.withPayload("bar".getBytes()).setHeader(MessageHeaders.CONTENT_TYPE, "foo/bar").build(); - moduleOutputChannel.send(message2); - - // other tap still receives messages - Message tapped = module2InputChannel.receive(5000); - assertNotNull(tapped); - - // Removed tap does not - assertNull(module3InputChannel.receive(1000)); - - // when other tap stream is deleted - binder.unbindConsumer(fooTapName, module2InputChannel); - // Clean up as StreamPlugin would - binder.unbindConsumer("baz.0", moduleInputChannel); - binder.unbindProducers("baz.0"); - binder.unbindConsumers("baz.0"); - assertTrue(getBindings(binder).isEmpty()); + binder.unbind(input0Binding); + binder.unbind(input1Binding); + binder.unbind(input2Binding); + binder.unbind(outputBinding); } @Test @@ -256,8 +190,8 @@ public class RawModeKafkaBinderTests extends KafkaBinderTests { Binder binder = getBinder(); DirectChannel moduleOutputChannel = new DirectChannel(); QueueChannel moduleInputChannel = new QueueChannel(); - binder.bindProducer("foo.0", moduleOutputChannel, null); - binder.bindConsumer("foo.0", moduleInputChannel, null); + Binding producerBinding = binder.bindProducer("foo.0", moduleOutputChannel, null); + Binding consumerBinding = binder.bindConsumer("foo.0", "test", moduleInputChannel, null); Message message = MessageBuilder.withPayload("foo".getBytes()).build(); // Let the consumer actually bind to the producer before sending a msg binderBindUnbindLatency(); @@ -265,8 +199,8 @@ public class RawModeKafkaBinderTests extends KafkaBinderTests { Message inbound = moduleInputChannel.receive(5000); assertNotNull(inbound); assertEquals("foo", new String((byte[])inbound.getPayload())); - binder.unbindProducers("foo.0"); - binder.unbindConsumers("foo.0"); + binder.unbind(producerBinding); + binder.unbind(consumerBinding); } // Ignored, since raw mode does not support headers @@ -277,32 +211,29 @@ public class RawModeKafkaBinderTests extends KafkaBinderTests { } - @Override @Test - public void testSendAndReceivePubSub() throws Exception { + public void testSendAndReceiveWithExplicitConsumerGroup() { Binder binder = getBinder(); DirectChannel moduleOutputChannel = new DirectChannel(); // Test pub/sub by emulating how StreamPlugin handles taps - DirectChannel tapChannel = new DirectChannel(); - QueueChannel moduleInputChannel = new QueueChannel(); + QueueChannel module1InputChannel = new QueueChannel(); QueueChannel module2InputChannel = new QueueChannel(); QueueChannel module3InputChannel = new QueueChannel(); - binder.bindProducer("baz.0", moduleOutputChannel, null); - binder.bindConsumer("baz.0", moduleInputChannel, null); - moduleOutputChannel.addInterceptor(new WireTap(tapChannel)); + Binding producerBinding = binder.bindProducer("baz.0", moduleOutputChannel, null); + Binding input1Binding = binder.bindConsumer("baz.0", "test", module1InputChannel, null); // A new module is using the tap as an input channel String fooTapName = "baz.0"; - binder.bindPubSubProducer(fooTapName, tapChannel, null); - binder.bindPubSubConsumer(fooTapName, module2InputChannel, null, null); + Binding input2Binding = binder.bindConsumer(fooTapName, "tap1", module2InputChannel, null); // Another new module is using tap as an input channel String barTapName = "baz.0"; - binder.bindPubSubConsumer(barTapName, module3InputChannel, null, null); + Binding input3Binding = binder.bindConsumer(barTapName, "tap2", module3InputChannel, null); + Message message = MessageBuilder.withPayload("foo".getBytes()).build(); boolean success = false; boolean retried = false; while (!success) { moduleOutputChannel.send(message); - Message inbound = moduleInputChannel.receive(5000); + Message inbound = module1InputChannel.receive(5000); assertNotNull(inbound); assertEquals("foo", new String((byte[])inbound.getPayload())); @@ -315,11 +246,11 @@ public class RawModeKafkaBinderTests extends KafkaBinderTests { continue; } success = true; - assertEquals("foo", new String((byte[])tapped1.getPayload())); - assertEquals("foo", new String((byte[])tapped2.getPayload())); + assertEquals("foo", new String((byte[]) tapped1.getPayload())); + assertEquals("foo", new String((byte[]) tapped2.getPayload())); } // delete one tap stream is deleted - binder.unbindConsumer(barTapName, module3InputChannel); + binder.unbind(input3Binding); Message message2 = MessageBuilder.withPayload("bar".getBytes()).build(); moduleOutputChannel.send(message2); @@ -327,15 +258,18 @@ public class RawModeKafkaBinderTests extends KafkaBinderTests { Message tapped = module2InputChannel.receive(5000); assertNotNull(tapped); - // Removed tap does not + // removed tap does not assertNull(module3InputChannel.receive(1000)); - // when other tap stream is deleted - binder.unbindConsumer(fooTapName, module2InputChannel); - // Clean up as StreamPlugin would - binder.unbindConsumer("baz.0", moduleInputChannel); - binder.unbindProducers("baz.0"); - binder.unbindConsumers("baz.0"); + // re-subscribed tap does receive the message + input3Binding = binder.bindConsumer(barTapName, "tap2", module3InputChannel, null); + assertNotNull(module3InputChannel.receive(1000)); + + // clean up + binder.unbind(input1Binding); + binder.unbind(input2Binding); + binder.unbind(input3Binding); + binder.unbind(producerBinding); assertTrue(getBindings(binder).isEmpty()); } diff --git a/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/RabbitBindingCleaner.java b/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/RabbitBindingCleaner.java index ee01466ed..d1ded2561 100644 --- a/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/RabbitBindingCleaner.java +++ b/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/RabbitBindingCleaner.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-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. @@ -25,7 +25,6 @@ import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.cloud.stream.binder.BinderUtils; import org.springframework.cloud.stream.binder.BindingCleaner; import org.springframework.cloud.stream.binder.MessageChannelBinderSupport; import org.springframework.web.client.RestTemplate; @@ -42,7 +41,9 @@ public class RabbitBindingCleaner implements BindingCleaner { private final static Logger logger = LoggerFactory.getLogger(RabbitBindingCleaner.class); - public static final String BINDER_PREFIX = "binder."; + private static final String PREFIX_DELIMITER = "."; + + public static final String BINDER_PREFIX = "binder" + PREFIX_DELIMITER; @Override public Map> clean(String entity, boolean isJob) { @@ -128,7 +129,7 @@ public class RabbitBindingCleaner implements BindingCleaner { return prefix.substring(0, prefix.length() - 1); } else { - return prefix + BinderUtils.GROUP_INDEX_DELIMITER; + return prefix + PREFIX_DELIMITER; } } diff --git a/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/RabbitMessageChannelBinder.java b/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/RabbitMessageChannelBinder.java index adb0119b4..864d4c7bf 100644 --- a/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/RabbitMessageChannelBinder.java +++ b/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/RabbitMessageChannelBinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2015 the original author or authors. + * Copyright 2013-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. @@ -62,7 +62,6 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.cloud.stream.binder.AbstractBindingPropertiesAccessor; import org.springframework.cloud.stream.binder.BinderPropertyKeys; -import org.springframework.cloud.stream.binder.BinderUtils; import org.springframework.cloud.stream.binder.Binding; import org.springframework.cloud.stream.binder.MessageChannelBinderSupport; import org.springframework.cloud.stream.binder.MessageValues; @@ -98,6 +97,7 @@ import com.rabbitmq.client.Envelope; /** * A {@link org.springframework.cloud.stream.binder.Binder} implementation backed by RabbitMQ. + * * @author Mark Fisher * @author Gary Russell * @author Jennifer Hickey @@ -150,40 +150,16 @@ public class RabbitMessageChannelBinder extends MessageChannelBinderSupport impl .addAll(RABBIT_CONSUMER_PROPERTIES) .build(); - private static final Set SUPPORTED_PUBSUB_CONSUMER_PROPERTIES = new SetBuilder() - .addAll(SUPPORTED_BASIC_CONSUMER_PROPERTIES) - .add(BinderPropertyKeys.DURABLE) - .build(); - /** - * Basic + concurrency. - */ - private static final Set SUPPORTED_NAMED_CONSUMER_PROPERTIES = new SetBuilder() - .addAll(SUPPORTED_BASIC_CONSUMER_PROPERTIES) - .add(BinderPropertyKeys.CONCURRENCY) - .build(); - - /** - * Basic + concurrency + partitioning. + * Basic + durable + concurrency + partitioning. */ private static final Set SUPPORTED_CONSUMER_PROPERTIES = new SetBuilder() .addAll(SUPPORTED_BASIC_CONSUMER_PROPERTIES) + .add(BinderPropertyKeys.DURABLE) .add(BinderPropertyKeys.CONCURRENCY) .add(BinderPropertyKeys.PARTITION_INDEX) .build(); - /** - * Basic + concurrency + reply headers + delivery mode (reply). - */ - private static final Set SUPPORTED_REPLYING_CONSUMER_PROPERTIES = new SetBuilder() - // request - .addAll(SUPPORTED_BASIC_CONSUMER_PROPERTIES) - .add(BinderPropertyKeys.CONCURRENCY) - // reply - .add(RabbitPropertiesAccessor.REPLY_HEADER_PATTERNS) - .add(RabbitPropertiesAccessor.DELIVERY_MODE) - .build(); - /** * Rabbit producer properties. */ @@ -195,39 +171,15 @@ public class RabbitMessageChannelBinder extends MessageChannelBinderSupport impl .add(BinderPropertyKeys.COMPRESS) .build(); - private static final Set SUPPORTED_PUBSUB_PRODUCER_PROPERTIES = new SetBuilder() - .addAll(SUPPORTED_BASIC_PRODUCER_PROPERTIES) - .addAll(PRODUCER_BATCHING_BASIC_PROPERTIES) - .addAll(PRODUCER_BATCHING_ADVANCED_PROPERTIES) - .build(); - - private static final Set SUPPORTED_NAMED_PRODUCER_PROPERTIES = new SetBuilder() - .addAll(SUPPORTED_BASIC_PRODUCER_PROPERTIES) - .addAll(PRODUCER_BATCHING_BASIC_PROPERTIES) - .addAll(PRODUCER_BATCHING_ADVANCED_PROPERTIES) - .build(); - /** * Partitioning + rabbit producer properties. */ private static final Set SUPPORTED_PRODUCER_PROPERTIES = new SetBuilder() .addAll(PRODUCER_PARTITIONING_PROPERTIES) .addAll(SUPPORTED_BASIC_PRODUCER_PROPERTIES) - .add(BinderPropertyKeys.DIRECT_BINDING_ALLOWED) .addAll(PRODUCER_BATCHING_BASIC_PROPERTIES) .addAll(PRODUCER_BATCHING_ADVANCED_PROPERTIES) - .build(); - - /** - * Basic producer + basic consumer + concurrency + reply headers. - */ - private static final Set SUPPORTED_REQUESTING_PRODUCER_PROPERTIES = new SetBuilder() - // request - .addAll(SUPPORTED_BASIC_PRODUCER_PROPERTIES) - // reply - .addAll(SUPPORTED_BASIC_CONSUMER_PROPERTIES) - .add(BinderPropertyKeys.CONCURRENCY) - .add(RabbitPropertiesAccessor.REPLY_HEADER_PATTERNS) + .add(RabbitPropertiesAccessor.AUTO_BIND_DLQ) .build(); private static final MessagePropertiesConverter inboundMessagePropertiesConverter = @@ -436,51 +388,27 @@ public class RabbitMessageChannelBinder extends MessageChannelBinderSupport impl } @Override - public void bindConsumer(final String name, MessageChannel moduleInputChannel, Properties properties) { + public Binding doBindConsumer(String name, String group, MessageChannel inputChannel, Properties properties) { + String baseQueueName = groupedName(name, group); if (this.logger.isInfoEnabled()) { - this.logger.info("declaring queue for inbound: " + name); - } - if (name.startsWith(P2P_NAMED_CHANNEL_TYPE_PREFIX)) { - validateConsumerProperties(name, properties, SUPPORTED_NAMED_CONSUMER_PROPERTIES); - } - else { - validateConsumerProperties(name, properties, SUPPORTED_CONSUMER_PROPERTIES); + this.logger.info("declaring queue for inbound: " + baseQueueName + ", bound to: " + name); } RabbitPropertiesAccessor accessor = new RabbitPropertiesAccessor(properties); - String queueName = applyPrefix(accessor.getPrefix(this.defaultPrefix), name); - TopicExchange exchange = new TopicExchange(queueName); - declareExchange(queueName, exchange); + validateConsumerProperties(baseQueueName, properties, SUPPORTED_CONSUMER_PROPERTIES); + String prefix = accessor.getPrefix(this.defaultPrefix); + String exchangeName = applyPrefix(prefix, name); + TopicExchange exchange = new TopicExchange(exchangeName); + declareExchange(exchangeName, exchange); + + String queueName = applyPrefix(prefix, baseQueueName); int partitionIndex = accessor.getPartitionIndex(); - String dlqNameRoot = name; if (partitionIndex >= 0) { String partitionSuffix = "-" + partitionIndex; queueName += partitionSuffix; - dlqNameRoot += partitionSuffix; } - Queue queue = new Queue(queueName, true, false, false, queueArgs(accessor, queueName)); - declareQueue(queueName, queue); - autoBindDLQ(dlqNameRoot, accessor); - org.springframework.amqp.core.Binding binding = BindingBuilder.bind(queue).to(exchange).with(queueName); - declareBinding(queueName, binding); - doRegisterConsumer(name, moduleInputChannel, queue, accessor, false); - bindExistingProducerDirectlyIfPossible(name, moduleInputChannel); - } - @Override - public void bindPubSubConsumer(String exchangeName, MessageChannel moduleInputChannel, String group, - Properties properties) { - String name = BinderUtils.groupedName(exchangeName, group); - if (this.logger.isInfoEnabled()) { - this.logger.info("declaring pubsub for inbound: " + name + ", bound to: " + exchangeName); - } - RabbitPropertiesAccessor accessor = new RabbitPropertiesAccessor(properties); - validateConsumerProperties(name, properties, SUPPORTED_PUBSUB_CONSUMER_PROPERTIES); - String prefix = accessor.getPrefix(this.defaultPrefix); - TopicExchange exchange = new TopicExchange(applyPrefix(prefix, exchangeName)); - declareExchange(exchange.getName(), exchange); Queue queue; boolean durable = accessor.isDurable(this.defaultDurableSubscription); - String queueName = applyPrefix(prefix, name); if (durable) { queue = new Queue(queueName, true, false, false, queueArgs(accessor, queueName)); } @@ -488,11 +416,18 @@ public class RabbitMessageChannelBinder extends MessageChannelBinderSupport impl queue = new Queue(queueName, false, false, true); } declareQueue(queueName, queue); - declareBinding(queue.getName(), BindingBuilder.bind(queue).to(exchange).with("#")); - doRegisterConsumer(name, moduleInputChannel, queue, accessor, true); - if (durable) { - autoBindDLQ(name, accessor); + if (partitionIndex >= 0) { + String bindingKey = String.format("%s-%d", name, partitionIndex); + declareBinding(queue.getName(), BindingBuilder.bind(queue).to(exchange).with(bindingKey)); } + else { + declareBinding(queue.getName(), BindingBuilder.bind(queue).to(exchange).with("#")); + } + Binding binding = doRegisterConsumer(baseQueueName, group, inputChannel, queue, accessor); + if (durable) { + autoBindDLQ(applyPrefix(prefix, baseQueueName), queueName, accessor); + } + return binding; } private Map queueArgs(RabbitPropertiesAccessor accessor, String queueName) { @@ -504,8 +439,9 @@ public class RabbitMessageChannelBinder extends MessageChannelBinderSupport impl return args; } - private void doRegisterConsumer(String name, MessageChannel moduleInputChannel, Queue queue, - RabbitPropertiesAccessor properties, boolean isPubSub) { + private Binding doRegisterConsumer(String name, String group, MessageChannel moduleInputChannel, Queue queue, + RabbitPropertiesAccessor properties) { + Binding consumerBinding = null; // Fix for XD-2503 // Temporarily overrides the thread context classloader with the one where the SimpleMessageListenerContainer // is defined @@ -520,15 +456,15 @@ public class RabbitMessageChannelBinder extends MessageChannelBinderSupport impl listenerContainer.setChannelTransacted(properties.getTransacted(this.defaultChannelTransacted)); listenerContainer.setDefaultRequeueRejected(properties.getRequeueRejected(this .defaultDefaultRequeueRejected)); - if (!isPubSub) { - int concurrency = properties.getConcurrency(this.defaultConcurrency); - concurrency = concurrency > 0 ? concurrency : 1; - listenerContainer.setConcurrentConsumers(concurrency); - int maxConcurrency = properties.getMaxConcurrency(this.defaultMaxConcurrency); - if (maxConcurrency > concurrency) { - listenerContainer.setMaxConcurrentConsumers(maxConcurrency); - } + + int concurrency = properties.getConcurrency(this.defaultConcurrency); + concurrency = concurrency > 0 ? concurrency : 1; + listenerContainer.setConcurrentConsumers(concurrency); + int maxConcurrency = properties.getMaxConcurrency(this.defaultMaxConcurrency); + if (maxConcurrency > concurrency) { + listenerContainer.setMaxConcurrentConsumers(maxConcurrency); } + listenerContainer.setPrefetchCount(properties.getPrefetchCount(this.defaultPrefetchCount)); listenerContainer.setTxSize(properties.getTxSize(this.defaultTxSize)); listenerContainer.setTaskExecutor(new SimpleAsyncTaskExecutor(queue.getName() + "-")); @@ -559,7 +495,7 @@ public class RabbitMessageChannelBinder extends MessageChannelBinderSupport impl mapper.setReplyHeaderNames(properties.getReplyHeaderPattens(this.defaultReplyHeaderPatterns)); adapter.setHeaderMapper(mapper); adapter.afterPropertiesSet(); - Binding consumerBinding = Binding.forConsumer(name, adapter, moduleInputChannel, properties); + consumerBinding = Binding.forConsumer(name, group, adapter, moduleInputChannel, properties); addBinding(consumerBinding); ReceivingHandler convertingBridge = new ReceivingHandler(); convertingBridge.setOutputChannel(moduleInputChannel); @@ -571,6 +507,7 @@ public class RabbitMessageChannelBinder extends MessageChannelBinderSupport impl finally { Thread.currentThread().setContextClassLoader(originalClassloader); } + return consumerBinding; } private MessageRecoverer determineRecoverer(String name, RabbitPropertiesAccessor properties) { @@ -587,54 +524,36 @@ public class RabbitMessageChannelBinder extends MessageChannelBinderSupport impl } } - @Override - public void bindProducer(final String name, MessageChannel moduleOutputChannel, - Properties properties) { - Assert.isInstanceOf(SubscribableChannel.class, moduleOutputChannel); - RabbitPropertiesAccessor accessor = new RabbitPropertiesAccessor(properties); - if (name.startsWith(P2P_NAMED_CHANNEL_TYPE_PREFIX)) { - validateProducerProperties(name, properties, SUPPORTED_NAMED_PRODUCER_PROPERTIES); - } - else { - validateProducerProperties(name, properties, SUPPORTED_PRODUCER_PROPERTIES); - } - if (!bindNewProducerDirectlyIfPossible(name, (SubscribableChannel) moduleOutputChannel, accessor)) { - if (this.logger.isInfoEnabled()) { - this.logger.info("declaring queue for outbound: " + name); - } - AmqpOutboundEndpoint queue = this.buildOutboundEndpoint(name, accessor, determineRabbitTemplate(accessor)); - doRegisterProducer(name, moduleOutputChannel, queue, accessor); - } - } - private AmqpOutboundEndpoint buildOutboundEndpoint(final String name, RabbitPropertiesAccessor properties, RabbitTemplate rabbitTemplate) { String prefix = properties.getPrefix(this.defaultPrefix); - String queueName = applyPrefix(prefix, name); + String exchangeName = applyPrefix(prefix, name); String partitionKeyExtractorClass = properties.getPartitionKeyExtractorClass(); Expression partitionKeyExpression = properties.getPartitionKeyExpression(); - TopicExchange exchange = new TopicExchange(queueName); - declareExchange(queueName, exchange); + TopicExchange exchange = new TopicExchange(exchangeName); + declareExchange(exchangeName, exchange); AmqpOutboundEndpoint endpoint = new AmqpOutboundEndpoint(rabbitTemplate); endpoint.setExchangeName(exchange.getName()); + String baseQueueName = exchangeName + ".default"; if (partitionKeyExpression == null && !StringUtils.hasText(partitionKeyExtractorClass)) { - Queue queue = new Queue(queueName, true, false, false, queueArgs(properties, queueName)); - declareQueue(name, queue); - autoBindDLQ(name, properties); - endpoint.setRoutingKey(queueName); - org.springframework.amqp.core.Binding binding = BindingBuilder.bind(queue).to(exchange).with(queueName); - declareBinding(queueName, binding); + Queue queue = new Queue(baseQueueName, true, false, false, queueArgs(properties, baseQueueName)); + declareQueue(baseQueueName, queue); + autoBindDLQ(baseQueueName, baseQueueName, properties); + endpoint.setRoutingKey(name); + org.springframework.amqp.core.Binding binding = BindingBuilder.bind(queue).to(exchange).with(name); + declareBinding(baseQueueName, binding); } else { - endpoint.setExpressionRoutingKey(EXPRESSION_PARSER.parseExpression(buildPartitionRoutingExpression - (queueName))); - // if the stream is partitioned, create one queue for each target partition + endpoint.setExpressionRoutingKey(EXPRESSION_PARSER.parseExpression(buildPartitionRoutingExpression(name))); + // if the stream is partitioned, create one queue for each target partition for the default group for (int i = 0; i < properties.getNextModuleCount(); i++) { String partitionSuffix = "-" + i; - Queue queue = new Queue(queueName + partitionSuffix, true, false, false, queueArgs(properties, queueName)); + String partitionQueueName = baseQueueName + partitionSuffix; + Queue queue = new Queue(partitionQueueName, true, false, false, + queueArgs(properties, partitionQueueName)); declareQueue(queue.getName(), queue); - autoBindDLQ(name + partitionSuffix, properties); - declareBinding(queue.getName(), BindingBuilder.bind(queue).to(exchange).with(queue.getName())); + autoBindDLQ(baseQueueName, baseQueueName + partitionSuffix, properties); + declareBinding(queue.getName(), BindingBuilder.bind(queue).to(exchange).with(name + partitionSuffix)); } } configureOutboundHandler(endpoint, properties); @@ -652,18 +571,14 @@ public class RabbitMessageChannelBinder extends MessageChannelBinderSupport impl } @Override - public void bindPubSubProducer(String name, MessageChannel moduleOutputChannel, - Properties properties) { - validateProducerProperties(name, properties, SUPPORTED_PUBSUB_PRODUCER_PROPERTIES); + public Binding bindProducer(String name, MessageChannel outputChannel, Properties properties) { + validateProducerProperties(name, properties, SUPPORTED_PRODUCER_PROPERTIES); RabbitPropertiesAccessor accessor = new RabbitPropertiesAccessor(properties); String exchangeName = applyPrefix(accessor.getPrefix(this.defaultPrefix), name); TopicExchange exchange = new TopicExchange(exchangeName); declareExchange(exchangeName, exchange); - AmqpOutboundEndpoint endpoint = new AmqpOutboundEndpoint(determineRabbitTemplate(accessor)); - endpoint.setExchangeName(exchangeName); - endpoint.setRoutingKey(name); - configureOutboundHandler(endpoint, accessor); - doRegisterProducer(name, moduleOutputChannel, endpoint, accessor); + AmqpOutboundEndpoint endpoint = this.buildOutboundEndpoint(name, accessor, determineRabbitTemplate(accessor)); + return doRegisterProducer(name, outputChannel, endpoint, accessor); } private RabbitTemplate determineRabbitTemplate(RabbitPropertiesAccessor properties) { @@ -691,12 +606,12 @@ public class RabbitMessageChannelBinder extends MessageChannelBinderSupport impl return rabbitTemplate; } - private void doRegisterProducer(final String name, MessageChannel moduleOutputChannel, + private Binding doRegisterProducer(final String name, MessageChannel moduleOutputChannel, AmqpOutboundEndpoint delegate, RabbitPropertiesAccessor properties) { - this.doRegisterProducer(name, moduleOutputChannel, delegate, null, properties); + return this.doRegisterProducer(name, moduleOutputChannel, delegate, null, properties); } - private void doRegisterProducer(final String name, MessageChannel moduleOutputChannel, + private Binding doRegisterProducer(final String name, MessageChannel moduleOutputChannel, AmqpOutboundEndpoint delegate, String replyTo, RabbitPropertiesAccessor properties) { Assert.isInstanceOf(SubscribableChannel.class, moduleOutputChannel); MessageHandler handler = new SendingHandler(delegate, replyTo, properties); @@ -704,72 +619,33 @@ public class RabbitMessageChannelBinder extends MessageChannelBinderSupport impl consumer.setBeanFactory(getBeanFactory()); consumer.setBeanName("outbound." + name); consumer.afterPropertiesSet(); - Binding producerBinding = Binding.forProducer(name, moduleOutputChannel, consumer, properties); + Binding producerBinding = Binding.forProducer(name, moduleOutputChannel, consumer, properties); addBinding(producerBinding); producerBinding.start(); - } - - @Override - public void bindRequestor(String name, MessageChannel requests, MessageChannel replies, - Properties properties) { - if (this.logger.isInfoEnabled()) { - this.logger.info("binding requestor: " + name); - } - validateProducerProperties(name, properties, SUPPORTED_REQUESTING_PRODUCER_PROPERTIES); - Assert.isInstanceOf(SubscribableChannel.class, requests); - RabbitPropertiesAccessor accessor = new RabbitPropertiesAccessor(properties); - String queueName = applyRequests(name); - AmqpOutboundEndpoint queue = this.buildOutboundEndpoint(queueName, accessor, this.rabbitTemplate); - queue.setBeanFactory(this.getBeanFactory()); - - String replyQueueName = accessor.getPrefix(this.defaultPrefix) + name + ".replies." - + this.getIdGenerator().generateId(); - this.doRegisterProducer(name, requests, queue, replyQueueName, accessor); - Queue replyQueue = new Queue(replyQueueName, false, false, true); // auto-delete - declareQueue(replyQueueName, replyQueue); - this.doRegisterConsumer(name, replies, replyQueue, accessor, false); - } - - @Override - public void bindReplier(String name, MessageChannel requests, MessageChannel replies, - Properties properties) { - if (this.logger.isInfoEnabled()) { - this.logger.info("binding replier: " + name); - } - validateConsumerProperties(name, properties, SUPPORTED_REPLYING_CONSUMER_PROPERTIES); - RabbitPropertiesAccessor accessor = new RabbitPropertiesAccessor(properties); - Queue requestQueue = new Queue(applyPrefix(accessor.getPrefix(this.defaultPrefix), applyRequests(name))); - declareQueue(requestQueue.getName(), requestQueue); - this.doRegisterConsumer(name, requests, requestQueue, accessor, false); - - AmqpOutboundEndpoint replyQueue = new AmqpOutboundEndpoint(this.rabbitTemplate); - replyQueue.setExpressionRoutingKey(EXPRESSION_PARSER.parseExpression("headers['" + AmqpHeaders.REPLY_TO + - "']")); - configureOutboundHandler(replyQueue, accessor); - doRegisterProducer(name, replies, replyQueue, accessor); + return producerBinding; } /** * If so requested, declare the DLX/DLQ and bind it. The DLQ is bound to the DLX with a routing key of the original * queue name because we use default exchange routing by queue name for the original message. - * @param name The name. + * @param queueName The base name for the queue (including the binder prefix, if any). + * @param routingKey The routing key for the queue. * @param properties The properties accessor. */ - private void autoBindDLQ(final String name, RabbitPropertiesAccessor properties) { + private void autoBindDLQ(final String queueName, String routingKey, RabbitPropertiesAccessor properties) { if (this.logger.isDebugEnabled()) { this.logger.debug("autoBindDLQ=" + properties.getAutoBindDLQ(this.defaultAutoBindDLQ) - + " for: " + name); + + " for: " + queueName); } if (properties.getAutoBindDLQ(this.defaultAutoBindDLQ)) { String prefix = properties.getPrefix(this.defaultPrefix); - String queueName = applyPrefix(prefix, name); String dlqName = constructDLQName(queueName); Queue dlq = new Queue(dlqName); declareQueue(dlqName, dlq); final String dlxName = deadLetterExchangeName(prefix); final DirectExchange dlx = new DirectExchange(dlxName); declareExchange(dlxName, dlx); - declareBinding(dlqName, BindingBuilder.bind(dlq).to(dlx).with(queueName)); + declareBinding(dlqName, BindingBuilder.bind(dlq).to(dlx).with(routingKey)); } } @@ -814,21 +690,10 @@ public class RabbitMessageChannelBinder extends MessageChannelBinderSupport impl } @Override - public void unbindConsumer(String name, MessageChannel channel) { - super.unbindConsumer(name, channel); - cleanAutoDeclareContext(name); - } - - @Override - public void unbindConsumers(String name) { - super.unbindConsumers(name); - cleanAutoDeclareContext(name); - } - - @Override - public void unbindPubSubConsumers(String name, String group) { - super.unbindPubSubConsumers(name, group); - cleanAutoDeclareContext(BinderUtils.groupedName(name, group)); + protected void afterUnbind(Binding binding) { + if (Binding.Type.consumer.equals(binding.getType())) { + cleanAutoDeclareContext(groupedName(binding.getName(), binding.getGroup())); + } } private void addToAutoDeclareContext(String name, Object bean) { diff --git a/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/LocalizedQueueConnectionFactoryIntegrationTests.java b/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/LocalizedQueueConnectionFactoryIntegrationTests.java index 29f00cf48..e13160799 100644 --- a/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/LocalizedQueueConnectionFactoryIntegrationTests.java +++ b/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/LocalizedQueueConnectionFactoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-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. @@ -46,7 +46,7 @@ public class LocalizedQueueConnectionFactoryIntegrationTests { @Before public void setup() { - ConnectionFactory defaultConnectionFactory = new CachingConnectionFactory("localhost"); + ConnectionFactory defaultConnectionFactory = rabbitAvailableRule.getResource(); String[] addresses = new String[] { "localhost:9999", "localhost:5672" }; String[] adminAddresses = new String[] { "http://localhost:15672", "http://localhost:15672" }; String[] nodes = new String[] { "foo@bar", "rabbit@localhost" }; @@ -66,6 +66,8 @@ public class LocalizedQueueConnectionFactoryIntegrationTests { RabbitTemplate template = new RabbitTemplate(targetConnectionFactory); template.convertAndSend("", queue.getName(), "foo"); assertEquals("foo", template.receiveAndConvert(queue.getName())); + ((CachingConnectionFactory) targetConnectionFactory).destroy(); + admin.deleteQueue(queue.getName()); } } diff --git a/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitBinderCleanerTests.java b/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitBinderCleanerTests.java index a772e7774..68058fca9 100644 --- a/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitBinderCleanerTests.java +++ b/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitBinderCleanerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-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. @@ -69,8 +69,8 @@ public class RabbitBinderCleanerTests { CachingConnectionFactory connectionFactory = rabbitWithMgmtEnabled.getResource(); RabbitAdmin rabbitAdmin = new RabbitAdmin(connectionFactory); for (int i = 0; i < 5; i++) { - String queue1Name = MessageChannelBinderSupport.applyPrefix(BINDER_PREFIX, stream1 + "." + i); - String queue2Name = MessageChannelBinderSupport.applyPrefix(BINDER_PREFIX, stream2 + "." + i); + String queue1Name = MessageChannelBinderSupport.applyPrefix(BINDER_PREFIX, stream1 + ".default." + i); + String queue2Name = MessageChannelBinderSupport.applyPrefix(BINDER_PREFIX, stream2 + ".default." + i); if (firstQueue == null) { firstQueue = queue1Name; } @@ -110,7 +110,7 @@ public class RabbitBinderCleanerTests { @Override public Void doInRabbit(Channel channel) throws Exception { - String queueName = MessageChannelBinderSupport.applyPrefix(BINDER_PREFIX, stream1 + "." + 4); + String queueName = MessageChannelBinderSupport.applyPrefix(BINDER_PREFIX, stream1 + ".default." + 4); String consumerTag = channel.basicConsume(queueName, new DefaultConsumer(channel)); try { waitForConsumerStateNot(queueName, 0); @@ -160,8 +160,8 @@ public class RabbitBinderCleanerTests { // should *not* clean stream2 assertEquals(10, cleanedQueues.size()); for (int i = 0; i < 5; i++) { - assertEquals(BINDER_PREFIX + stream1 + "." + i, cleanedQueues.get(i * 2)); - assertEquals(BINDER_PREFIX + stream1 + "." + i + ".dlq", cleanedQueues.get(i * 2 + 1)); + assertEquals(BINDER_PREFIX + stream1 + ".default." + i, cleanedQueues.get(i * 2)); + assertEquals(BINDER_PREFIX + stream1 + ".default." + i + ".dlq", cleanedQueues.get(i * 2 + 1)); } List cleanedExchanges = cleanedMap.get("exchanges"); assertEquals(6, cleanedExchanges.size()); @@ -172,7 +172,7 @@ public class RabbitBinderCleanerTests { cleanedQueues = cleanedMap.get("queues"); assertEquals(5, cleanedQueues.size()); for (int i = 0; i < 5; i++) { - assertEquals(BINDER_PREFIX + stream2 + "." + i, cleanedQueues.get(i)); + assertEquals(BINDER_PREFIX + stream2 + ".default." + i, cleanedQueues.get(i)); } cleanedExchanges = cleanedMap.get("exchanges"); assertEquals(6, cleanedExchanges.size()); diff --git a/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitBinderTests.java b/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitBinderTests.java index 757fbcdb1..7ecff915b 100644 --- a/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitBinderTests.java +++ b/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitBinderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2015 the original author or authors. + * Copyright 2013-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. @@ -16,7 +16,6 @@ package org.springframework.cloud.stream.binder.rabbit; -import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.startsWith; @@ -26,8 +25,6 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -58,14 +55,13 @@ import org.springframework.amqp.support.AmqpHeaders; import org.springframework.amqp.support.postprocessor.DelegatingDecompressingPostProcessor; import org.springframework.amqp.utils.test.TestUtils; import org.springframework.beans.DirectFieldAccessor; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.cloud.stream.binder.AbstractTestBinder; import org.springframework.cloud.stream.binder.Binder; import org.springframework.cloud.stream.binder.BinderPropertyKeys; import org.springframework.cloud.stream.binder.Binding; import org.springframework.cloud.stream.binder.PartitionCapableBinderTests; import org.springframework.cloud.stream.binder.Spy; import org.springframework.cloud.stream.test.junit.rabbit.RabbitTestSupport; -import org.springframework.context.support.AbstractApplicationContext; import org.springframework.expression.spel.standard.SpelExpression; import org.springframework.integration.channel.DirectChannel; import org.springframework.integration.channel.QueueChannel; @@ -110,8 +106,8 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { Binder binder = getBinder(); DirectChannel moduleOutputChannel = new DirectChannel(); DirectChannel moduleInputChannel = new DirectChannel(); - binder.bindProducer("bad.0", moduleOutputChannel, null); - binder.bindConsumer("bad.0", moduleInputChannel, null); + Binding producerBinding = binder.bindProducer("bad.0", moduleOutputChannel, null); + Binding consumerBinding = binder.bindConsumer("bad.0", "test", moduleInputChannel, null); Message message = MessageBuilder.withPayload("bad").setHeader(MessageHeaders.CONTENT_TYPE, "foo/bar").build(); final CountDownLatch latch = new CountDownLatch(3); @@ -125,8 +121,8 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { }); moduleOutputChannel.send(message); assertTrue(latch.await(10, TimeUnit.SECONDS)); - binder.unbindConsumers("bad.0"); - binder.unbindProducers("bad.0"); + binder.unbind(consumerBinding); + binder.unbind(producerBinding); } @Test @@ -134,15 +130,15 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { Binder binder = getBinder(); Properties properties = new Properties(); properties.put("transacted", "true"); // test transacted with defaults; not allowed with ackmode NONE - binder.bindConsumer("props.0", new DirectChannel(), properties); + Binding consumerBinding = binder.bindConsumer("props.0", null, new DirectChannel(), properties); @SuppressWarnings("unchecked") - List bindings = TestUtils.getPropertyValue(binder, "binder.bindings", List.class); + List> bindings = TestUtils.getPropertyValue(binder, "binder.bindings", List.class); assertEquals(1, bindings.size()); AbstractEndpoint endpoint = bindings.get(0).getEndpoint(); SimpleMessageListenerContainer container = TestUtils.getPropertyValue(endpoint, "messageListenerContainer", SimpleMessageListenerContainer.class); assertEquals(AcknowledgeMode.AUTO, container.getAcknowledgeMode()); - assertEquals(RabbitMessageChannelBinder.DEFAULT_RABBIT_PREFIX + "props.0", container.getQueueNames()[0]); + assertEquals(RabbitMessageChannelBinder.DEFAULT_RABBIT_PREFIX + "props.0.default", container.getQueueNames()[0]); assertTrue(TestUtils.getPropertyValue(container, "transactional", Boolean.class)); assertEquals(1, TestUtils.getPropertyValue(container, "concurrentConsumers")); assertNull(TestUtils.getPropertyValue(container, "maxConcurrentConsumers")); @@ -154,7 +150,7 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { assertEquals(1000L, TestUtils.getPropertyValue(retry, "retryOperations.backOffPolicy.initialInterval")); assertEquals(10000L, TestUtils.getPropertyValue(retry, "retryOperations.backOffPolicy.maxInterval")); assertEquals(2.0, TestUtils.getPropertyValue(retry, "retryOperations.backOffPolicy.multiplier")); - binder.unbindConsumers("props.0"); + binder.unbind(consumerBinding); assertEquals(0, bindings.size()); properties = new Properties(); @@ -171,46 +167,26 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { properties.put("requeue", "false"); properties.put("txSize", "10"); properties.put("partitionIndex", 0); - binder.bindConsumer("props.0", new DirectChannel(), properties); + consumerBinding = binder.bindConsumer("props.0", "test", new DirectChannel(), properties); @SuppressWarnings("unchecked") - List bindingsNow = TestUtils.getPropertyValue(binder, "binder.bindings", List.class); + List> bindingsNow = TestUtils.getPropertyValue(binder, "binder.bindings", List.class); assertEquals(1, bindingsNow.size()); endpoint = bindingsNow.get(0).getEndpoint(); container = verifyContainer(endpoint); - assertEquals("foo.props.0", container.getQueueNames()[0]); + assertEquals("foo.props.0.test", container.getQueueNames()[0]); - try { - binder.bindPubSubConsumer("dummy", null, null, properties); - fail("Expected exception"); - } - catch (IllegalArgumentException e) { - assertThat(e.getMessage(), allOf( - containsString(getClassUnderTestName() + " does not support consumer properties: "), - containsString("partitionIndex"), - containsString("concurrency"), - containsString(" for dummy."))); - } - try { - binder.bindConsumer("queue:dummy", null, properties); - fail("Expected exception"); - } - catch (IllegalArgumentException e) { - assertEquals(getClassUnderTestName() + " does not support consumer property: partitionIndex for queue:dummy.", - e.getMessage()); - } - - binder.unbindConsumers("props.0"); + binder.unbind(consumerBinding); assertEquals(0, bindingsNow.size()); } @Test public void testProducerProperties() throws Exception { Binder binder = getBinder(); - binder.bindProducer("props.0", new DirectChannel(), null); + Binding producerBinding = binder.bindProducer("props.0", new DirectChannel(), null); @SuppressWarnings("unchecked") - List bindings = TestUtils.getPropertyValue(binder, "binder.bindings", List.class); + List> bindings = TestUtils.getPropertyValue(binder, "binder.bindings", List.class); assertEquals(1, bindings.size()); AbstractEndpoint endpoint = bindings.get(0).getEndpoint(); MessageDeliveryMode mode = TestUtils.getPropertyValue(endpoint, "handler.delegate.defaultDeliveryMode", @@ -219,7 +195,7 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { List requestHeaders = TestUtils.getPropertyValue(endpoint, "handler.delegate.headerMapper.requestHeaderMatcher.strategies", List.class); assertEquals(2, requestHeaders.size()); - binder.unbindProducers("props.0"); + binder.unbind(producerBinding); assertEquals(0, bindings.size()); Properties properties = new Properties(); @@ -232,11 +208,11 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { properties.put("partitionSelectorClass", "foo"); properties.put(BinderPropertyKeys.NEXT_MODULE_COUNT, "1"); - binder.bindProducer("props.0", new DirectChannel(), properties); + producerBinding = binder.bindProducer("props.0", new DirectChannel(), properties); assertEquals(1, bindings.size()); endpoint = bindings.get(0).getEndpoint(); assertEquals( - "'foo.props.0-' + headers['partition']", + "'props.0-' + headers['partition']", TestUtils.getPropertyValue(endpoint, "handler.delegate.routingKeyExpression", SpelExpression.class).getExpressionString()); mode = TestUtils.getPropertyValue(endpoint, "handler.delegate.defaultDeliveryMode", @@ -244,167 +220,7 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { assertEquals(MessageDeliveryMode.NON_PERSISTENT, mode); verifyFooRequestProducer(endpoint); - try { - binder.bindPubSubProducer("dummy", new DirectChannel(), properties); - fail("Expected exception"); - } - catch (IllegalArgumentException e) { - assertThat(e.getMessage(), allOf( - containsString(getClassUnderTestName() + " does not support producer properties: "), - containsString("partitionSelectorExpression"), - containsString("partitionKeyExtractorClass"), - containsString("partitionKeyExpression"), - containsString("partitionSelectorClass"))); - assertThat(e.getMessage(), containsString("for dummy.")); - } - try { - binder.bindProducer("queue:dummy", new DirectChannel(), properties); - fail("Expected exception"); - } - catch (IllegalArgumentException e) { - assertThat(e.getMessage(), allOf( - containsString(getClassUnderTestName() + " does not support producer properties: "), - containsString("partitionSelectorExpression"), - containsString("partitionKeyExtractorClass"), - containsString("partitionKeyExpression"), - containsString("partitionSelectorClass"))); - assertThat(e.getMessage(), containsString("for queue:dummy.")); - } - - binder.unbindProducers("props.0"); - assertEquals(0, bindings.size()); - } - - @Test - public void testRequestReplyRequestorProperties() throws Exception { - Binder binder = getBinder(); - Properties properties = new Properties(); - properties.put("prefix", "foo."); - properties.put("deliveryMode", "NON_PERSISTENT"); - - properties.put("requestHeaderPatterns", "foo"); - properties.put("replyHeaderPatterns", "bar"); - - properties.put("ackMode", "NONE"); - properties.put("backOffInitialInterval", "2000"); - properties.put("backOffMaxInterval", "20000"); - properties.put("backOffMultiplier", "5.0"); - properties.put("concurrency", "2"); - properties.put("maxAttempts", "23"); - properties.put("maxConcurrency", "3"); - properties.put("prefix", "foo."); - properties.put("prefetch", "20"); - properties.put("requeue", "false"); - properties.put("txSize", "10"); - - binder.bindRequestor("props.0", new DirectChannel(), new DirectChannel(), properties); - @SuppressWarnings("unchecked") - List bindings = TestUtils.getPropertyValue(binder, "binder.bindings", List.class); - - assertEquals(2, bindings.size()); - AbstractEndpoint endpoint = bindings.get(0).getEndpoint(); // producer - MessageDeliveryMode mode = TestUtils.getPropertyValue(endpoint, "handler.delegate.defaultDeliveryMode", - MessageDeliveryMode.class); - assertEquals(MessageDeliveryMode.NON_PERSISTENT, mode); - verifyFooRequestBarReplyProducer(endpoint); - - endpoint = bindings.get(1).getEndpoint(); // consumer - - verifyContainer(endpoint); - - verifyBarReplyConsumer(endpoint); - - properties.put("partitionKeyExpression", "'foo'"); - properties.put("partitionKeyExtractorClass", "foo"); - properties.put("partitionSelectorExpression", "0"); - properties.put("partitionSelectorClass", "foo"); - properties.put(BinderPropertyKeys.NEXT_MODULE_COUNT, "1"); - properties.put("partitionIndex", "0"); - try { - binder.bindRequestor("dummy", null, null, properties); - fail("Expected exception"); - } - catch (IllegalArgumentException e) { - assertThat(e.getMessage(), allOf( - containsString(getClassUnderTestName() + " does not support producer properties: "), - containsString("partitionSelectorExpression"), - containsString("partitionKeyExtractorClass"), - containsString("partitionKeyExpression"), - containsString("partitionSelectorClass"))); - assertThat(e.getMessage(), allOf(containsString("partitionIndex"), containsString("for dummy."))); - } - - binder.unbindConsumers("props.0"); - binder.unbindProducers("props.0"); - assertEquals(0, bindings.size()); - } - - @Test - public void testRequestReplyReplierProperties() throws Exception { - Binder binder = getBinder(); - Properties properties = new Properties(); - properties.put("prefix", "foo."); - properties.put("deliveryMode", "NON_PERSISTENT"); - - properties.put("requestHeaderPatterns", "foo"); - properties.put("replyHeaderPatterns", "bar"); - - properties.put("ackMode", "NONE"); - properties.put("backOffInitialInterval", "2000"); - properties.put("backOffMaxInterval", "20000"); - properties.put("backOffMultiplier", "5.0"); - properties.put("concurrency", "2"); - properties.put("maxAttempts", "23"); - properties.put("maxConcurrency", "3"); - properties.put("prefix", "foo."); - properties.put("prefetch", "20"); - properties.put("requeue", "false"); - properties.put("txSize", "10"); - - binder.bindReplier("props.0", new DirectChannel(), new DirectChannel(), properties); - @SuppressWarnings("unchecked") - List bindings = TestUtils.getPropertyValue(binder, "binder.bindings", List.class); - - assertEquals(2, bindings.size()); - AbstractEndpoint endpoint = bindings.get(1).getEndpoint(); // producer - assertEquals( - "headers['amqp_replyTo']", - TestUtils.getPropertyValue(endpoint, "handler.delegate.routingKeyExpression", - SpelExpression.class).getExpressionString()); - MessageDeliveryMode mode = TestUtils.getPropertyValue(endpoint, "handler.delegate.defaultDeliveryMode", - MessageDeliveryMode.class); - assertEquals(MessageDeliveryMode.NON_PERSISTENT, mode); - - verifyFooRequestBarReplyProducer(endpoint); - - endpoint = bindings.get(0).getEndpoint(); // consumer - - verifyContainer(endpoint); - - verifyBarReplyConsumer(endpoint); - - properties.put("partitionKeyExpression", "'foo'"); - properties.put("partitionKeyExtractorClass", "foo"); - properties.put("partitionSelectorExpression", "0"); - properties.put("partitionSelectorClass", "foo"); - properties.put(BinderPropertyKeys.NEXT_MODULE_COUNT, "1"); - properties.put("partitionIndex", "0"); - try { - binder.bindReplier("dummy", null, null, properties); - fail("Expected exception"); - } - catch (IllegalArgumentException e) { - assertThat(e.getMessage(), allOf( - containsString(getClassUnderTestName() + " does not support consumer properties: "), - containsString("partitionSelectorExpression"), - containsString("partitionKeyExtractorClass"), - containsString("partitionKeyExpression"), - containsString("partitionSelectorClass"))); - assertThat(e.getMessage(), allOf(containsString("partitionIndex"), containsString("for dummy."))); - } - - binder.unbindConsumers("props.0"); - binder.unbindProducers("props.0"); + binder.unbind(producerBinding); assertEquals(0, bindings.size()); } @@ -430,14 +246,14 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { } }); - binder.bindPubSubConsumer("durabletest.0", moduleInputChannel, "tgroup", properties); + Binding consumerBinding = binder.bindConsumer("durabletest.0", "tgroup", moduleInputChannel, properties); RabbitTemplate template = new RabbitTemplate(this.rabbitAvailableRule.getResource()); template.convertAndSend(TEST_PREFIX + "durabletest.0", "", "foo"); int n = 0; while (n++ < 100) { - Object deadLetter = template.receiveAndConvert(TEST_PREFIX + "tgroup.durabletest.0.dlq"); + Object deadLetter = template.receiveAndConvert(TEST_PREFIX + "durabletest.0.tgroup.dlq"); if (deadLetter != null) { assertEquals("foo", deadLetter); break; @@ -446,13 +262,8 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { } assertTrue(n < 100); - binder.unbindConsumer("durabletest.0", moduleInputChannel); - binder.unbindPubSubConsumers("durabletest.0", "tgroup"); - assertNotNull(admin.getQueueProperties(TEST_PREFIX + "tgroup.durabletest.0.dlq")); - admin.deleteQueue(TEST_PREFIX + "tgroup.durabletest.0.dlq"); - admin.deleteQueue(TEST_PREFIX + "tgroup.durabletest.0"); - admin.deleteExchange(TEST_PREFIX + "durabletest.0"); - admin.deleteExchange(TEST_PREFIX + "DLX"); + binder.unbind(consumerBinding); + assertNotNull(admin.getQueueProperties(TEST_PREFIX + "durabletest.0.tgroup.dlq")); } @Test @@ -476,18 +287,14 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { } }); - binder.bindPubSubConsumer("nondurabletest.0", moduleInputChannel, "tgroup", properties); + Binding consumerBinding = binder.bindConsumer("nondurabletest.0", "tgroup", moduleInputChannel, properties); - binder.unbindPubSubConsumers("nondurabletest.0", "tgroup"); + binder.unbind(consumerBinding); assertNull(admin.getQueueProperties(TEST_PREFIX + "nondurabletest.0.dlq")); - admin.deleteQueue(TEST_PREFIX + "tgroup.nondurabletest.0"); - admin.deleteExchange(TEST_PREFIX + "nondurabletest.0"); } @Test public void testAutoBindDLQ() throws Exception { - RabbitAdmin admin = new RabbitAdmin(this.rabbitAvailableRule.getResource()); - Binder binder = getBinder(); Properties properties = new Properties(); properties.put("prefix", TEST_PREFIX); @@ -504,14 +311,14 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { } }); - binder.bindConsumer("dlqtest", moduleInputChannel, properties); + Binding consumerBinding = binder.bindConsumer("dlqtest", null, moduleInputChannel, properties); RabbitTemplate template = new RabbitTemplate(this.rabbitAvailableRule.getResource()); - template.convertAndSend("", TEST_PREFIX + "dlqtest", "foo"); + template.convertAndSend("", TEST_PREFIX + "dlqtest.default", "foo"); int n = 0; while (n++ < 100) { - Object deadLetter = template.receiveAndConvert(TEST_PREFIX + "dlqtest.dlq"); + Object deadLetter = template.receiveAndConvert(TEST_PREFIX + "dlqtest.default.dlq"); if (deadLetter != null) { assertEquals("foo", deadLetter); break; @@ -520,10 +327,176 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { } assertTrue(n < 100); - binder.unbindConsumer("dlqtest", moduleInputChannel); - admin.deleteQueue(TEST_PREFIX + "dlqtest.dlq"); - admin.deleteQueue(TEST_PREFIX + "dlqtest"); - admin.deleteExchange(TEST_PREFIX + "DLX"); + binder.unbind(consumerBinding); + } + + @Test + public void testAutoBindDLQPartionedConsumerFirst() throws Exception { + Binder binder = getBinder(); + Properties properties = new Properties(); + properties.put("prefix", "bindertest."); + properties.put("autoBindDLQ", "true"); + properties.put("maxAttempts", "1"); // disable retry + properties.put("requeue", "false"); + properties.put("partitionIndex", "0"); + DirectChannel input0 = new DirectChannel(); + input0.setBeanName("test.input0DLQ"); + Binding input0Binding = binder.bindConsumer("partDLQ.0", "dlqPartGrp", input0, properties); + Binding defaultConsumerBinding1 = binder.bindConsumer("partDLQ.0", null, new QueueChannel(), properties); + properties.put("partitionIndex", "1"); + DirectChannel input1 = new DirectChannel(); + input1.setBeanName("test.input1DLQ"); + Binding input1Binding = binder.bindConsumer("partDLQ.0", "dlqPartGrp", input1, properties); + Binding defaultConsumerBinding2 = binder.bindConsumer("partDLQ.0", null, new QueueChannel(), properties); + + properties.clear(); + properties.put("prefix", "bindertest."); + properties.put("autoBindDLQ", "true"); + properties.put("partitionKeyExtractorClass", "org.springframework.cloud.stream.binder.PartitionTestSupport"); + properties.put("partitionSelectorClass", "org.springframework.cloud.stream.binder.PartitionTestSupport"); + properties.put(BinderPropertyKeys.NEXT_MODULE_COUNT, "2"); + DirectChannel output = new DirectChannel(); + output.setBeanName("test.output"); + Binding outputBinding = binder.bindProducer("partDLQ.0", output, properties); + + final CountDownLatch latch0 = new CountDownLatch(1); + input0.subscribe(new MessageHandler() { + + @Override + public void handleMessage(Message message) throws MessagingException { + if (latch0.getCount() <= 0) { + throw new RuntimeException("dlq"); + } + latch0.countDown(); + } + + }); + + final CountDownLatch latch1 = new CountDownLatch(1); + input1.subscribe(new MessageHandler() { + + @Override + public void handleMessage(Message message) throws MessagingException { + if (latch1.getCount() <= 0) { + throw new RuntimeException("dlq"); + } + latch1.countDown(); + } + + }); + + output.send(new GenericMessage(1)); + assertTrue(latch1.await(10, TimeUnit.SECONDS)); + + output.send(new GenericMessage(0)); + assertTrue(latch0.await(10, TimeUnit.SECONDS)); + + output.send(new GenericMessage(1)); + + RabbitTemplate template = new RabbitTemplate(this.rabbitAvailableRule.getResource()); + template.setReceiveTimeout(10000); + + String streamDLQName = "bindertest.partDLQ.0.dlqPartGrp.dlq"; + + org.springframework.amqp.core.Message received = template.receive(streamDLQName); + assertNotNull(received); + assertEquals(1, received.getMessageProperties().getHeaders().get("partition")); + + output.send(new GenericMessage(0)); + received = template.receive(streamDLQName); + assertNotNull(received); + assertEquals(0, received.getMessageProperties().getHeaders().get("partition")); + + binder.unbind(input0Binding); + binder.unbind(input1Binding); + binder.unbind(defaultConsumerBinding1); + binder.unbind(defaultConsumerBinding2); + binder.unbind(outputBinding); + } + + @Test + public void testAutoBindDLQPartionedProducerFirst() throws Exception { + Binder binder = getBinder(); + Properties properties = new Properties(); + + properties.put("prefix", "bindertest."); + properties.put("autoBindDLQ", "true"); + properties.put("partitionKeyExtractorClass", "org.springframework.cloud.stream.binder.PartitionTestSupport"); + properties.put("partitionSelectorClass", "org.springframework.cloud.stream.binder.PartitionTestSupport"); + properties.put(BinderPropertyKeys.NEXT_MODULE_COUNT, "2"); + DirectChannel output = new DirectChannel(); + output.setBeanName("test.output"); + Binding outputBinding = binder.bindProducer("partDLQ.1", output, properties); + + properties.clear(); + properties.put("prefix", "bindertest."); + properties.put("autoBindDLQ", "true"); + properties.put("maxAttempts", "1"); // disable retry + properties.put("requeue", "false"); + properties.put("partitionIndex", "0"); + DirectChannel input0 = new DirectChannel(); + input0.setBeanName("test.input0DLQ"); + Binding input0Binding = binder.bindConsumer("partDLQ.1", "dlqPartGrp", input0, properties); + Binding defaultConsumerBinding1 = binder.bindConsumer("partDLQ.1", null, new QueueChannel(), properties); + properties.put("partitionIndex", "1"); + DirectChannel input1 = new DirectChannel(); + input1.setBeanName("test.input1DLQ"); + Binding input1Binding = binder.bindConsumer("partDLQ.1", "dlqPartGrp", input1, properties); + Binding defaultConsumerBinding2 = binder.bindConsumer("partDLQ.1", null, new QueueChannel(), properties); + + final CountDownLatch latch0 = new CountDownLatch(1); + input0.subscribe(new MessageHandler() { + + @Override + public void handleMessage(Message message) throws MessagingException { + if (latch0.getCount() <= 0) { + throw new RuntimeException("dlq"); + } + latch0.countDown(); + } + + }); + + final CountDownLatch latch1 = new CountDownLatch(1); + input1.subscribe(new MessageHandler() { + + @Override + public void handleMessage(Message message) throws MessagingException { + if (latch1.getCount() <= 0) { + throw new RuntimeException("dlq"); + } + latch1.countDown(); + } + + }); + + output.send(new GenericMessage(1)); + assertTrue(latch1.await(10, TimeUnit.SECONDS)); + + output.send(new GenericMessage(0)); + assertTrue(latch0.await(10, TimeUnit.SECONDS)); + + output.send(new GenericMessage(1)); + + RabbitTemplate template = new RabbitTemplate(this.rabbitAvailableRule.getResource()); + template.setReceiveTimeout(10000); + + String streamDLQName = "bindertest.partDLQ.1.dlqPartGrp.dlq"; + + org.springframework.amqp.core.Message received = template.receive(streamDLQName); + assertNotNull(received); + assertEquals(1, received.getMessageProperties().getHeaders().get("partition")); + + output.send(new GenericMessage(0)); + received = template.receive(streamDLQName); + assertNotNull(received); + assertEquals(0, received.getMessageProperties().getHeaders().get("partition")); + + binder.unbind(input0Binding); + binder.unbind(input1Binding); + binder.unbind(defaultConsumerBinding1); + binder.unbind(defaultConsumerBinding2); + binder.unbind(outputBinding); } @Test @@ -532,8 +505,8 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { RabbitAdmin admin = new RabbitAdmin(this.rabbitAvailableRule.getResource()); Map args = new HashMap(); args.put("x-dead-letter-exchange", TEST_PREFIX + "DLX"); - args.put("x-dead-letter-routing-key", TEST_PREFIX + "dlqpubtest"); - Queue queue = new Queue(TEST_PREFIX + "dlqpubtest", true, false, false, args); + args.put("x-dead-letter-routing-key", TEST_PREFIX + "dlqpubtest.default"); + Queue queue = new Queue(TEST_PREFIX + "dlqpubtest.default", true, false, false, args); admin.declareQueue(queue); Binder binder = getBinder(); @@ -553,14 +526,14 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { } }); - binder.bindConsumer("dlqpubtest", moduleInputChannel, properties); + Binding consumerBinding = binder.bindConsumer("dlqpubtest", "default", moduleInputChannel, properties); RabbitTemplate template = new RabbitTemplate(this.rabbitAvailableRule.getResource()); - template.convertAndSend("", TEST_PREFIX + "dlqpubtest", "foo"); + template.convertAndSend("", TEST_PREFIX + "dlqpubtest.default", "foo"); int n = 0; while (n++ < 100) { - org.springframework.amqp.core.Message deadLetter = template.receive(TEST_PREFIX + "dlqpubtest.dlq"); + org.springframework.amqp.core.Message deadLetter = template.receive(TEST_PREFIX + "dlqpubtest.default.dlq"); if (deadLetter != null) { assertEquals("foo", new String(deadLetter.getBody())); assertNotNull(deadLetter.getMessageProperties().getHeaders().get("x-exception-stacktrace")); @@ -570,10 +543,7 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { } assertTrue(n < 100); - binder.unbindConsumer("dlqpubtest", moduleInputChannel); - admin.deleteQueue(TEST_PREFIX + "dlqpubtest.dlq"); - admin.deleteQueue(TEST_PREFIX + "dlqpubtest"); - admin.deleteExchange(TEST_PREFIX + "DLX"); + binder.unbind(consumerBinding); } @SuppressWarnings("unchecked") @@ -591,9 +561,9 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { DirectChannel output = new DirectChannel(); output.setBeanName("batchingProducer"); - binder.bindProducer("batching.0", output, properties); + Binding producerBinding = binder.bindProducer("batching.0", output, properties); - while (template.receive(RabbitMessageChannelBinder.DEFAULT_RABBIT_PREFIX + "batching.0") != null) { + while (template.receive(RabbitMessageChannelBinder.DEFAULT_RABBIT_PREFIX + "batching.0.default") != null) { } Log logger = spy(TestUtils.getPropertyValue(binder, "binder.compressingPostProcessor.logger", Log.class)); @@ -606,7 +576,7 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { output.send(new GenericMessage<>("foo".getBytes())); output.send(new GenericMessage<>("bar".getBytes())); - Object out = spyOn("batching.0").receive(false); + Object out = spyOn("batching.0.default").receive(false); assertThat(out, instanceOf(byte[].class)); assertEquals("\u0000\u0000\u0000\u0003foo\u0000\u0000\u0000\u0003bar", new String((byte[]) out)); @@ -616,7 +586,7 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { QueueChannel input = new QueueChannel(); input.setBeanName("batchingConsumer"); - binder.bindConsumer("batching.0", input, null); + Binding consumerBinding = binder.bindConsumer("batching.0", "test", input, null); output.send(new GenericMessage<>("foo".getBytes())); output.send(new GenericMessage<>("bar".getBytes())); @@ -629,8 +599,8 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { assertEquals("bar", new String(in.getPayload())); assertNull(in.getHeaders().get(AmqpHeaders.DELIVERY_MODE)); - binder.unbindProducers("batching.0"); - binder.unbindConsumers("batching.0"); + binder.unbind(producerBinding); + binder.unbind(consumerBinding); } /* @@ -641,53 +611,51 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { public void testLateBinding() throws Exception { RabbitTestSupport.RabbitProxy proxy = new RabbitTestSupport.RabbitProxy(); CachingConnectionFactory cf = new CachingConnectionFactory("localhost", proxy.getPort()); - RabbitMessageChannelBinder binder = new RabbitMessageChannelBinder(cf); - AbstractApplicationContext applicationContext = mock(AbstractApplicationContext.class); - when(applicationContext.getBeanFactory()).thenReturn(mock(ConfigurableListableBeanFactory.class)); - binder.setApplicationContext(applicationContext); - binder.setDefaultAutoBindDLQ(true); - binder.afterPropertiesSet(); + RabbitMessageChannelBinder rabbitBinder = new RabbitMessageChannelBinder(cf); + rabbitBinder.setDefaultAutoBindDLQ(true); + AbstractTestBinder binder = new RabbitTestBinder(cf, rabbitBinder); + Properties properties = new Properties(); properties.put("prefix", "latebinder."); MessageChannel moduleOutputChannel = new DirectChannel(); - binder.bindProducer("late.0", moduleOutputChannel, properties); + Binding late0ProducerBinding = binder.bindProducer("late.0", moduleOutputChannel, properties); QueueChannel moduleInputChannel = new QueueChannel(); - binder.bindConsumer("late.0", moduleInputChannel, properties); + Binding late0ConsumerBinding = binder.bindConsumer("late.0", "test", moduleInputChannel, properties); properties.put("partitionKeyExpression", "payload.equals('0') ? 0 : 1"); properties.put("partitionSelectorExpression", "hashCode()"); properties.put("nextModuleCount", "2"); MessageChannel partOutputChannel = new DirectChannel(); - binder.bindProducer("partlate.0", partOutputChannel, properties); + Binding partlate0ProducerBinding = binder.bindProducer("partlate.0", partOutputChannel, properties); QueueChannel partInputChannel0 = new QueueChannel(); QueueChannel partInputChannel1 = new QueueChannel(); properties.clear(); properties.put("prefix", "latebinder."); properties.put("partitionIndex", "0"); - binder.bindConsumer("partlate.0", partInputChannel0, properties); + Binding partlate0Consumer0Binding = binder.bindConsumer("partlate.0", "test", partInputChannel0, properties); properties.put("partitionIndex", "1"); - binder.bindConsumer("partlate.0", partInputChannel1, properties); + Binding partlate0Consumer1Binding = binder.bindConsumer("partlate.0", "test", partInputChannel1, properties); - binder.setDefaultAutoBindDLQ(false); + rabbitBinder.setDefaultAutoBindDLQ(false); properties.clear(); properties.put("prefix", "latebinder."); MessageChannel noDLQOutputChannel = new DirectChannel(); - binder.bindProducer("lateNoDLQ.0", noDLQOutputChannel, properties); + Binding noDlqProducerBinding = binder.bindProducer("lateNoDLQ.0", noDLQOutputChannel, properties); QueueChannel noDLQInputChannel = new QueueChannel(); - binder.bindConsumer("lateNoDLQ.0", noDLQInputChannel, properties); + Binding noDlqConsumerBinding = binder.bindConsumer("lateNoDLQ.0", "test", noDLQInputChannel, properties); - MessageChannel pubSubOutputChannel = new DirectChannel(); - binder.bindPubSubProducer("latePubSub", pubSubOutputChannel, properties); + MessageChannel outputChannel = new DirectChannel(); + Binding pubSubProducerBinding = binder.bindProducer("latePubSub", outputChannel, properties); QueueChannel pubSubInputChannel = new QueueChannel(); - binder.bindPubSubConsumer("latePubSub", pubSubInputChannel, "lategroup", properties); + Binding nonDurableConsumerBinding = binder.bindConsumer("latePubSub", "lategroup", pubSubInputChannel, properties); QueueChannel durablePubSubInputChannel = new QueueChannel(); properties.setProperty("durableSubscription", "true"); - binder.bindPubSubConsumer("latePubSub", durablePubSubInputChannel, "lateDurableGroup", properties); + Binding durableConsumerBinding = binder.bindConsumer("latePubSub", "lateDurableGroup", durablePubSubInputChannel, properties); proxy.start(); @@ -701,7 +669,7 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { assertNotNull(message); assertEquals("bar", message.getPayload()); - pubSubOutputChannel.send(new GenericMessage<>("baz")); + outputChannel.send(new GenericMessage<>("baz")); message = pubSubInputChannel.receive(10000); assertNotNull(message); assertEquals("baz", message.getPayload()); @@ -718,28 +686,22 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { assertNotNull(message); assertEquals("1", message.getPayload()); - binder.unbindProducer("late.0", moduleOutputChannel); - binder.unbindConsumer("late.0", moduleInputChannel); - binder.unbindProducer("partlate.0", moduleOutputChannel); - binder.unbindConsumers("partlate.0"); + binder.unbind(late0ProducerBinding); + binder.unbind(late0ConsumerBinding); + binder.unbind(partlate0ProducerBinding); + binder.unbind(partlate0Consumer0Binding); + binder.unbind(partlate0Consumer1Binding); + binder.unbind(noDlqProducerBinding); + binder.unbind(noDlqConsumerBinding); + binder.unbind(pubSubProducerBinding); + binder.unbind(nonDurableConsumerBinding); + binder.unbind(durableConsumerBinding); + + binder.cleanup(); proxy.stop(); cf.destroy(); - RabbitAdmin admin = new RabbitAdmin(this.rabbitAvailableRule.getResource()); - admin.deleteQueue("latebinder.late.0"); - admin.deleteQueue("latebinder.lateNoDLQ.0"); - admin.deleteQueue("latebinder.partlate.0-0"); - admin.deleteQueue("latebinder.partlate.0-1"); - admin.deleteQueue("latebinder.late.0.dlq"); - admin.deleteQueue("latebinder.partlate.0-0.dlq"); - admin.deleteQueue("latebinder.partlate.0-1.dlq"); - admin.deleteQueue("latebinder.lateDurableGroup.latePubSub"); - admin.deleteExchange("latebinder.late.0"); - admin.deleteExchange("latebinder.lateNoDLQ.0"); - admin.deleteExchange("latebinder.partlate.0"); - admin.deleteExchange("latebinder.latePubSub"); - admin.deleteExchange("latebinder.DLX"); this.rabbitAvailableRule.getResource().destroy(); } @@ -772,26 +734,6 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { return container; } - private void verifyBarReplyConsumer(AbstractEndpoint endpoint) { - List replyMatchers; - replyMatchers = TestUtils.getPropertyValue(endpoint, - "headerMapper.replyHeaderMatcher.strategies", - List.class); - assertEquals(1, replyMatchers.size()); - assertEquals("bar", - TestUtils.getPropertyValue(replyMatchers.get(0), "patterns", Collection.class).iterator().next()); - } - - private void verifyFooRequestBarReplyProducer(AbstractEndpoint endpoint) { - verifyFooRequestProducer(endpoint); - List replyMatchers = TestUtils.getPropertyValue(endpoint, - "handler.delegate.headerMapper.replyHeaderMatcher.strategies", - List.class); - assertEquals(1, replyMatchers.size()); - assertEquals("bar", - TestUtils.getPropertyValue(replyMatchers.get(0), "patterns", Collection.class).iterator().next()); - } - private void verifyFooRequestProducer(AbstractEndpoint endpoint) { List requestMatchers = TestUtils.getPropertyValue(endpoint, "handler.delegate.headerMapper.requestHeaderMatcher.strategies", @@ -807,6 +749,11 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { SpelExpression.class).getExpressionString(); } + @Override + protected String getExpectedRoutingBaseDestination(String name, String group) { + return name; + } + @Override protected String getPubSubEndpointRouting(AbstractEndpoint endpoint) { return TestUtils.getPropertyValue(endpoint, "handler.delegate.exchangeNameExpression", diff --git a/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitTestBinder.java b/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitTestBinder.java index 0d80496c0..65651b88d 100644 --- a/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitTestBinder.java +++ b/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitTestBinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-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. @@ -23,19 +23,20 @@ import java.util.Set; import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.core.RabbitAdmin; import org.springframework.cloud.stream.binder.AbstractTestBinder; +import org.springframework.cloud.stream.binder.Binding; import org.springframework.context.support.GenericApplicationContext; import org.springframework.integration.codec.kryo.PojoCodec; import org.springframework.integration.context.IntegrationContextUtils; import org.springframework.messaging.MessageChannel; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; - /** * Test support class for {@link RabbitMessageChannelBinder}. * * @author Ilayaperumal Gopinathan * @author Gary Russell * @author David Turanski + * @author Mark Fisher */ public class RabbitTestBinder extends AbstractTestBinder { @@ -45,8 +46,15 @@ public class RabbitTestBinder extends AbstractTestBinder prefixes = new HashSet<>(); + private final Set queues = new HashSet(); + + private final Set exchanges = new HashSet(); + public RabbitTestBinder(ConnectionFactory connectionFactory) { - RabbitMessageChannelBinder binder = new RabbitMessageChannelBinder(connectionFactory); + this(connectionFactory, new RabbitMessageChannelBinder(connectionFactory)); + } + + public RabbitTestBinder(ConnectionFactory connectionFactory, RabbitMessageChannelBinder binder) { GenericApplicationContext context = new GenericApplicationContext(); ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); scheduler.setPoolSize(1); @@ -60,75 +68,48 @@ public class RabbitTestBinder extends AbstractTestBinder bindConsumer(String name, String group, MessageChannel moduleInputChannel, Properties properties) { + this.queues.add(prefix(properties) + name + (group == null ? ".default" : "." + group)); + this.exchanges.add(prefix(properties) + name); + return super.bindConsumer(name, group, moduleInputChannel, properties); + } @Override - public void bindProducer(String name, MessageChannel moduleOutputChannel, Properties properties) { - capturePrefix(properties); - super.bindProducer(name, moduleOutputChannel, properties); + public Binding bindProducer(String name, MessageChannel moduleOutputChannel, Properties properties) { + this.queues.add(prefix(properties) + name + ".default"); + this.exchanges.add(prefix(properties) + name); + return super.bindProducer(name, moduleOutputChannel, properties); } - @Override - public void bindPubSubConsumer(String name, MessageChannel inputChannel, String group, Properties properties) { - capturePrefix(properties); - super.bindPubSubConsumer(name, inputChannel, group, properties); - } - - @Override - public void bindPubSubProducer(String name, MessageChannel outputChannel, Properties properties) { - capturePrefix(properties); - super.bindPubSubProducer(name, outputChannel, properties); - } - - @Override - public void bindRequestor(String name, MessageChannel requests, MessageChannel replies, Properties properties) { - capturePrefix(properties); - super.bindRequestor(name, requests, replies, properties); - } - - @Override - public void bindReplier(String name, MessageChannel requests, MessageChannel replies, Properties properties) { - capturePrefix(properties); - super.bindReplier(name, requests, replies, properties); - } - - public void capturePrefix(Properties properties) { + public String prefix(Properties properties) { if (properties != null) { String prefix = properties.getProperty("prefix"); if (prefix != null) { this.prefixes.add(prefix); + return prefix; } } + return BINDER_PREFIX; } @Override public void cleanup() { - if (!queues.isEmpty()) { - for (String queue : queues) { - rabbitAdmin.deleteQueue(BINDER_PREFIX + queue); - // delete any partitioned queues - for (int i = 0; i < 10; i++) { - rabbitAdmin.deleteQueue(BINDER_PREFIX + queue + "-" + i); - } - for (String prefix : this.prefixes) { - rabbitAdmin.deleteQueue(prefix + queue); - // delete any partitioned queues - for (int i = 0; i < 10; i++) { - rabbitAdmin.deleteQueue(prefix + queue + "-" + i); - } - rabbitAdmin.deleteExchange(prefix + queue); - rabbitAdmin.deleteExchange(prefix + queue + ".requests"); - } - rabbitAdmin.deleteExchange(BINDER_PREFIX + queue); + for (String queue : this.queues) { + this.rabbitAdmin.deleteQueue(queue); + this.rabbitAdmin.deleteQueue(queue + ".dlq"); + // delete any partitioned queues + for (int i = 0; i < 10; i++) { + this.rabbitAdmin.deleteQueue(queue + "-" + i); + this.rabbitAdmin.deleteQueue(queue + "-" + i + ".dlq"); } } - if (!topics.isEmpty()) { - for (String exchange : topics) { - rabbitAdmin.deleteExchange(BINDER_PREFIX + exchange); - } + for (String exchange : this.exchanges) { + this.rabbitAdmin.deleteExchange(exchange); + } + for (String prefix : this.prefixes) { + this.rabbitAdmin.deleteExchange(prefix + "DLX"); } } + } diff --git a/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/integration/RabbitBinderModuleTests.java b/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/integration/RabbitBinderModuleTests.java index bf237c426..2c693eedb 100644 --- a/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/integration/RabbitBinderModuleTests.java +++ b/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/integration/RabbitBinderModuleTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-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. @@ -65,8 +65,8 @@ public class RabbitBinderModuleTests { context = null; } RabbitAdmin admin = new RabbitAdmin(rabbitTestSupport.getResource()); - admin.deleteQueue("binder.input"); - admin.deleteQueue("binder.output"); + admin.deleteQueue("binder.input.default"); + admin.deleteQueue("binder.output.default"); admin.deleteExchange("binder.input"); admin.deleteExchange("binder.output"); } diff --git a/spring-cloud-stream-binders/spring-cloud-stream-binder-redis/src/main/java/org/springframework/cloud/stream/binder/redis/RedisMessageChannelBinder.java b/spring-cloud-stream-binders/spring-cloud-stream-binder-redis/src/main/java/org/springframework/cloud/stream/binder/redis/RedisMessageChannelBinder.java index e9eb6f7a3..6ebe5af68 100644 --- a/spring-cloud-stream-binders/spring-cloud-stream-binder-redis/src/main/java/org/springframework/cloud/stream/binder/redis/RedisMessageChannelBinder.java +++ b/spring-cloud-stream-binders/spring-cloud-stream-binder-redis/src/main/java/org/springframework/cloud/stream/binder/redis/RedisMessageChannelBinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-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. @@ -18,7 +18,9 @@ package org.springframework.cloud.stream.binder.redis; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Properties; import java.util.Set; @@ -31,6 +33,8 @@ import org.springframework.cloud.stream.binder.EmbeddedHeadersMessageConverter; import org.springframework.cloud.stream.binder.MessageChannelBinderSupport; import org.springframework.cloud.stream.binder.MessageValues; import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisOperations; +import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.expression.Expression; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.integration.channel.DirectChannel; @@ -38,9 +42,7 @@ import org.springframework.integration.endpoint.EventDrivenConsumer; import org.springframework.integration.endpoint.MessageProducerSupport; import org.springframework.integration.handler.AbstractMessageHandler; import org.springframework.integration.handler.AbstractReplyProducingMessageHandler; -import org.springframework.integration.redis.inbound.RedisInboundChannelAdapter; import org.springframework.integration.redis.inbound.RedisQueueMessageDrivenEndpoint; -import org.springframework.integration.redis.outbound.RedisPublishingMessageHandler; import org.springframework.integration.redis.outbound.RedisQueueOutboundChannelAdapter; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; @@ -56,6 +58,7 @@ import org.springframework.util.StringUtils; /** * A {@link org.springframework.cloud.stream.binder.Binder} implementation backed by Redis. + * * @author Mark Fisher * @author Gary Russell * @author David Turanski @@ -65,71 +68,30 @@ public class RedisMessageChannelBinder extends MessageChannelBinderSupport imple private static final String ERROR_HEADER = "errorKey"; + private static final String CONSUMER_GROUPS_KEY_PREFIX = "groups."; + private static final SpelExpressionParser parser = new SpelExpressionParser(); private final String[] headersToMap; - /** - * Retry only. - */ - private static final Set SUPPORTED_PUBSUB_CONSUMER_PROPERTIES = new SetBuilder() - .addAll(CONSUMER_STANDARD_PROPERTIES) - .addAll(CONSUMER_RETRY_PROPERTIES) - .build(); + private final RedisOperations redisOperations; /** - * Retry + concurrency. - */ - private static final Set SUPPORTED_NAMED_CONSUMER_PROPERTIES = new SetBuilder() - .addAll(CONSUMER_STANDARD_PROPERTIES) - .addAll(CONSUMER_RETRY_PROPERTIES) - .add(BinderPropertyKeys.CONCURRENCY) - .build(); - - /** - * Named + partitioning. + * Retry + concurrency + partitioning. */ private static final Set SUPPORTED_CONSUMER_PROPERTIES = new SetBuilder() - .addAll(SUPPORTED_NAMED_CONSUMER_PROPERTIES) - .add(BinderPropertyKeys.PARTITION_INDEX) - .build(); - - /** - * Retry + concurrency (request). - */ - private static final Set SUPPORTED_REPLYING_CONSUMER_PROPERTIES = new SetBuilder() - // request .addAll(CONSUMER_STANDARD_PROPERTIES) .addAll(CONSUMER_RETRY_PROPERTIES) .add(BinderPropertyKeys.CONCURRENCY) + .add(BinderPropertyKeys.PARTITION_INDEX) .build(); - /** - * None. - */ - private static final Set SUPPORTED_PUBSUB_PRODUCER_PROPERTIES = PRODUCER_STANDARD_PROPERTIES; - - /** - * None. - */ - private static final Set SUPPORTED_NAMED_PRODUCER_PROPERTIES = PRODUCER_STANDARD_PROPERTIES; - /** * Partitioning. */ private static final Set SUPPORTED_PRODUCER_PROPERTIES = new SetBuilder() .addAll(PRODUCER_PARTITIONING_PROPERTIES) .addAll(PRODUCER_STANDARD_PROPERTIES) - .add(BinderPropertyKeys.DIRECT_BINDING_ALLOWED) - .build(); - - /** - * Retry, concurrency (reply). - */ - private static final Set SUPPORTED_REQUESTING_PRODUCER_PROPERTIES = new SetBuilder() - // reply - .addAll(CONSUMER_RETRY_PROPERTIES) - .add(BinderPropertyKeys.CONCURRENCY) .build(); private final RedisConnectionFactory connectionFactory; @@ -143,11 +105,12 @@ public class RedisMessageChannelBinder extends MessageChannelBinderSupport imple this(connectionFactory, new String[0]); } - public RedisMessageChannelBinder(RedisConnectionFactory connectionFactory, - String... headersToMap) { + public RedisMessageChannelBinder(RedisConnectionFactory connectionFactory, String... headersToMap) { Assert.notNull(connectionFactory, "connectionFactory must not be null"); this.connectionFactory = connectionFactory; - + StringRedisTemplate template = new StringRedisTemplate(connectionFactory); + template.afterPropertiesSet(); + this.redisOperations = template; if (headersToMap != null && headersToMap.length > 0) { String[] combinedHeadersToMap = Arrays.copyOfRange(BinderHeaders.STANDARD_HEADERS, 0, BinderHeaders.STANDARD_HEADERS.length @@ -159,7 +122,6 @@ public class RedisMessageChannelBinder extends MessageChannelBinderSupport imple else { this.headersToMap = BinderHeaders.STANDARD_HEADERS; } - this.errorAdapter = new RedisQueueOutboundChannelAdapter( parser.parseExpression("headers['" + ERROR_HEADER + "']"), connectionFactory); } @@ -173,23 +135,16 @@ public class RedisMessageChannelBinder extends MessageChannelBinderSupport imple } @Override - public void bindConsumer(final String name, MessageChannel moduleInputChannel, Properties properties) { - if (name.startsWith(P2P_NAMED_CHANNEL_TYPE_PREFIX)) { - validateConsumerProperties(name, properties, SUPPORTED_NAMED_CONSUMER_PROPERTIES); - } - else { - validateConsumerProperties(name, properties, SUPPORTED_CONSUMER_PROPERTIES); - } + protected Binding doBindConsumer(final String name, String group, MessageChannel moduleInputChannel, Properties properties) { RedisPropertiesAccessor accessor = new RedisPropertiesAccessor(properties); - String queueName = "queue." + name; + String queueName = groupedName(name, group); + validateConsumerProperties(queueName, properties, SUPPORTED_CONSUMER_PROPERTIES); int partitionIndex = accessor.getPartitionIndex(); if (partitionIndex >= 0) { queueName += "-" + partitionIndex; } MessageProducerSupport adapter = createInboundAdapter(accessor, queueName); - doRegisterConsumer(name, name + (partitionIndex >= 0 ? "-" + partitionIndex : ""), moduleInputChannel, adapter, - accessor); - bindExistingProducerDirectlyIfPossible(name, moduleInputChannel); + return doRegisterConsumer(name, group, queueName, moduleInputChannel, adapter, accessor); } private MessageProducerSupport createInboundAdapter(RedisPropertiesAccessor accessor, String queueName) { @@ -209,37 +164,25 @@ public class RedisMessageChannelBinder extends MessageChannelBinderSupport imple return adapter; } - @Override - public void bindPubSubConsumer(final String name, MessageChannel moduleInputChannel, String group, - Properties properties) { - if (logger.isInfoEnabled()) { - logger.info("declaring pubsub for inbound: " + name); - } - validateConsumerProperties(name, properties, SUPPORTED_PUBSUB_CONSUMER_PROPERTIES); - RedisInboundChannelAdapter adapter = new RedisInboundChannelAdapter(this.connectionFactory); - adapter.setBeanFactory(this.getBeanFactory()); - adapter.setSerializer(null); - adapter.setTopics(applyPubSub(name)); - doRegisterConsumer(name, name, moduleInputChannel, adapter, new RedisPropertiesAccessor(properties)); - } - - private void doRegisterConsumer(String bindingName, String channelName, MessageChannel moduleInputChannel, + private Binding doRegisterConsumer(String bindingName, String group, String channelName, MessageChannel moduleInputChannel, MessageProducerSupport adapter, RedisPropertiesAccessor properties) { DirectChannel bridgeToModuleChannel = new DirectChannel(); bridgeToModuleChannel.setBeanFactory(this.getBeanFactory()); bridgeToModuleChannel.setBeanName(channelName + ".bridge"); MessageChannel bridgeInputChannel = addRetryIfNeeded(channelName, bridgeToModuleChannel, properties); adapter.setOutputChannel(bridgeInputChannel); - adapter.setBeanName("inbound." + bindingName); + adapter.setBeanName("inbound." + channelName); adapter.afterPropertiesSet(); - Binding consumerBinding = Binding.forConsumer(bindingName, adapter, moduleInputChannel, properties); + Binding consumerBinding = Binding.forConsumer(channelName, group, adapter, moduleInputChannel, properties); addBinding(consumerBinding); ReceivingHandler convertingBridge = new ReceivingHandler(); convertingBridge.setOutputChannel(moduleInputChannel); convertingBridge.setBeanName(channelName + ".bridge.handler"); convertingBridge.afterPropertiesSet(); bridgeToModuleChannel.subscribe(convertingBridge); + this.redisOperations.boundZSetOps(CONSUMER_GROUPS_KEY_PREFIX + bindingName).incrementScore(group, 1); consumerBinding.start(); + return consumerBinding; } /** @@ -300,101 +243,49 @@ public class RedisMessageChannelBinder extends MessageChannelBinderSupport imple } @Override - public void bindProducer(final String name, MessageChannel moduleOutputChannel, - Properties properties) { - Assert.isInstanceOf(SubscribableChannel.class, moduleOutputChannel); - if (name.startsWith(P2P_NAMED_CHANNEL_TYPE_PREFIX)) { - validateProducerProperties(name, properties, SUPPORTED_NAMED_PRODUCER_PROPERTIES); - } - else { - validateProducerProperties(name, properties, SUPPORTED_PRODUCER_PROPERTIES); - } - RedisPropertiesAccessor accessor = new RedisPropertiesAccessor(properties); - if (!bindNewProducerDirectlyIfPossible(name, (SubscribableChannel) moduleOutputChannel, accessor)) { - String partitionKeyExtractorClass = accessor.getPartitionKeyExtractorClass(); - Expression partitionKeyExpression = accessor.getPartitionKeyExpression(); - RedisQueueOutboundChannelAdapter queue; - String queueName = "queue." + name; - if (partitionKeyExpression == null && !StringUtils.hasText(partitionKeyExtractorClass)) { - queue = new RedisQueueOutboundChannelAdapter(queueName, this.connectionFactory); - } - else { - queue = new RedisQueueOutboundChannelAdapter( - parser.parseExpression(buildPartitionRoutingExpression(queueName)), this.connectionFactory); - } - queue.setIntegrationEvaluationContext(this.evaluationContext); - queue.setBeanFactory(this.getBeanFactory()); - queue.afterPropertiesSet(); - doRegisterProducer(name, moduleOutputChannel, queue, accessor); + protected void afterUnbind(Binding binding) { + if (Binding.Type.consumer.equals(binding.getType())) { + String key = CONSUMER_GROUPS_KEY_PREFIX + binding.getName(); + this.redisOperations.boundZSetOps(key).incrementScore(binding.getGroup(), -1); } } @Override - public void bindPubSubProducer(final String name, MessageChannel moduleOutputChannel, - Properties properties) { - validateProducerProperties(name, properties, SUPPORTED_PUBSUB_PRODUCER_PROPERTIES); - RedisPublishingMessageHandler topic = new RedisPublishingMessageHandler(connectionFactory); - topic.setBeanFactory(this.getBeanFactory()); - topic.setTopic(applyPubSub(name)); - topic.afterPropertiesSet(); - doRegisterProducer(name, moduleOutputChannel, topic, new RedisPropertiesAccessor(properties)); - } - - private void doRegisterProducer(final String name, MessageChannel moduleOutputChannel, MessageHandler delegate, - RedisPropertiesAccessor properties) { - this.doRegisterProducer(name, moduleOutputChannel, delegate, null, properties); - } - - private void doRegisterProducer(final String name, MessageChannel moduleOutputChannel, MessageHandler delegate, - String replyTo, RedisPropertiesAccessor properties) { + public Binding bindProducer(final String name, MessageChannel moduleOutputChannel, Properties properties) { Assert.isInstanceOf(SubscribableChannel.class, moduleOutputChannel); - MessageHandler handler = new SendingHandler(delegate, replyTo, properties); + validateProducerProperties(name, properties, SUPPORTED_PRODUCER_PROPERTIES); + RedisPropertiesAccessor accessor = new RedisPropertiesAccessor(properties); + return doRegisterProducer(name, moduleOutputChannel, accessor); + } + + private RedisQueueOutboundChannelAdapter createProducerEndpoint(String name, RedisPropertiesAccessor accessor) { + String partitionKeyExtractorClass = accessor.getPartitionKeyExtractorClass(); + Expression partitionKeyExpression = accessor.getPartitionKeyExpression(); + RedisQueueOutboundChannelAdapter queue; + if (partitionKeyExpression == null && !StringUtils.hasText(partitionKeyExtractorClass)) { + queue = new RedisQueueOutboundChannelAdapter(name, this.connectionFactory); + } + else { + queue = new RedisQueueOutboundChannelAdapter( + parser.parseExpression(buildPartitionRoutingExpression(name)), this.connectionFactory); + } + queue.setIntegrationEvaluationContext(this.evaluationContext); + queue.setBeanFactory(this.getBeanFactory()); + queue.afterPropertiesSet(); + return queue; + } + + private Binding doRegisterProducer(final String name, MessageChannel moduleOutputChannel, RedisPropertiesAccessor properties) { + Assert.isInstanceOf(SubscribableChannel.class, moduleOutputChannel); + MessageHandler handler = new SendingHandler(name, properties); EventDrivenConsumer consumer = new EventDrivenConsumer((SubscribableChannel) moduleOutputChannel, handler); consumer.setBeanFactory(this.getBeanFactory()); consumer.setBeanName("outbound." + name); consumer.afterPropertiesSet(); - Binding producerBinding = Binding.forProducer(name, moduleOutputChannel, consumer, properties); + Binding producerBinding = Binding.forProducer(name, moduleOutputChannel, consumer, properties); addBinding(producerBinding); producerBinding.start(); - } - - @Override - public void bindRequestor(String name, MessageChannel requests, MessageChannel replies, - Properties properties) { - if (logger.isInfoEnabled()) { - logger.info("binding requestor: " + name); - } - Assert.isInstanceOf(SubscribableChannel.class, requests); - validateProducerProperties(name, properties, SUPPORTED_REQUESTING_PRODUCER_PROPERTIES); - RedisQueueOutboundChannelAdapter queue = new RedisQueueOutboundChannelAdapter("queue." + applyRequests(name), - this.connectionFactory); - queue.setBeanFactory(this.getBeanFactory()); - queue.afterPropertiesSet(); - String replyQueueName = name + ".replies." + this.getIdGenerator().generateId(); - RedisPropertiesAccessor accessor = new RedisPropertiesAccessor(properties); - this.doRegisterProducer(name, requests, queue, replyQueueName, accessor); - MessageProducerSupport adapter = createInboundAdapter(accessor, replyQueueName); - this.doRegisterConsumer(name, name, replies, adapter, accessor); - } - - @Override - public void bindReplier(String name, MessageChannel requests, MessageChannel replies, - Properties properties) { - if (logger.isInfoEnabled()) { - logger.info("binding replier: " + name); - } - validateConsumerProperties(name, properties, SUPPORTED_REPLYING_CONSUMER_PROPERTIES); - RedisPropertiesAccessor accessor = new RedisPropertiesAccessor(properties); - MessageProducerSupport adapter = createInboundAdapter(accessor, "queue." + applyRequests(name)); - this.doRegisterConsumer(name, name, requests, adapter, accessor); - - RedisQueueOutboundChannelAdapter replyQueue = new RedisQueueOutboundChannelAdapter( - RedisMessageChannelBinder.parser.parseExpression("headers['" + BinderHeaders.REPLY_TO + "']"), - this.connectionFactory); - replyQueue.setBeanFactory(this.getBeanFactory()); - replyQueue.setIntegrationEvaluationContext(this.evaluationContext); - replyQueue.afterPropertiesSet(); - this.doRegisterProducer(name, replies, replyQueue, accessor); + return producerBinding; } @Override @@ -404,37 +295,48 @@ public class RedisMessageChannelBinder extends MessageChannelBinderSupport imple private class SendingHandler extends AbstractMessageHandler { - private final MessageHandler delegate; - - private final String replyTo; + private final String bindingName; private final PartitioningMetadata partitioningMetadata; + private final RedisPropertiesAccessor accessor; - private SendingHandler(MessageHandler delegate, String replyTo, RedisPropertiesAccessor properties) { - this.delegate = delegate; - this.replyTo = replyTo; + private final Map adapters = new HashMap<>(); + + private SendingHandler(String bindingName, RedisPropertiesAccessor properties) { + this.bindingName = bindingName; + this.accessor = properties; this.partitioningMetadata = new PartitioningMetadata(properties, properties.getNextModuleCount()); this.setBeanFactory(RedisMessageChannelBinder.this.getBeanFactory()); + refreshChannelAdapters(); } @Override protected void handleMessageInternal(Message message) throws Exception { MessageValues transformed = serializePayloadIfNecessary(message); - if (replyTo != null) { - transformed.put(BinderHeaders.REPLY_TO, this.replyTo); - } if (this.partitioningMetadata.isPartitionedModule()) { - transformed.put(PARTITION_HEADER, determinePartition(message, this.partitioningMetadata)); } byte[] messageToSend = embeddedHeadersMessageConverter.embedHeaders(transformed, RedisMessageChannelBinder.this.headersToMap); - delegate.handleMessage(MessageBuilder.withPayload(messageToSend).copyHeaders(transformed).build()); + + refreshChannelAdapters(); + for (RedisQueueOutboundChannelAdapter adapter : adapters.values()) { + adapter.handleMessage((MessageBuilder.withPayload(messageToSend).copyHeaders(transformed).build())); + } } + private void refreshChannelAdapters() { + Set groups = redisOperations.boundZSetOps(CONSUMER_GROUPS_KEY_PREFIX + bindingName).rangeByScore(1, Double.MAX_VALUE); + for (String group : groups) { + if (!adapters.containsKey(group)) { + String channel = String.format("%s.%s", this.bindingName, group); + adapters.put(group, createProducerEndpoint(channel, accessor)); + } + } + } } private class ReceivingHandler extends AbstractReplyProducingMessageHandler { diff --git a/spring-cloud-stream-binders/spring-cloud-stream-binder-redis/src/test/java/org/springframework/cloud/stream/binder/redis/RedisBinderTests.java b/spring-cloud-stream-binders/spring-cloud-stream-binder-redis/src/test/java/org/springframework/cloud/stream/binder/redis/RedisBinderTests.java index 50f052f57..367f548c6 100644 --- a/spring-cloud-stream-binders/spring-cloud-stream-binder-redis/src/test/java/org/springframework/cloud/stream/binder/redis/RedisBinderTests.java +++ b/spring-cloud-stream-binders/spring-cloud-stream-binder-redis/src/test/java/org/springframework/cloud/stream/binder/redis/RedisBinderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2015 the original author or authors. + * Copyright 2013-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. @@ -16,7 +16,6 @@ package org.springframework.cloud.stream.binder.redis; -import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.instanceOf; import static org.junit.Assert.assertEquals; @@ -24,17 +23,15 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.Properties; import java.util.concurrent.TimeUnit; -import org.junit.Before; -import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; @@ -52,6 +49,7 @@ import org.springframework.expression.Expression; import org.springframework.integration.channel.DirectChannel; import org.springframework.integration.endpoint.AbstractEndpoint; import org.springframework.integration.redis.inbound.RedisQueueMessageDrivenEndpoint; +import org.springframework.integration.redis.outbound.RedisQueueOutboundChannelAdapter; import org.springframework.integration.test.util.TestUtils; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.support.GenericMessage; @@ -60,6 +58,7 @@ import org.springframework.retry.support.RetryTemplate; /** * @author Gary Russell * @author David Turanski + * @author Mark Fisher */ public class RedisBinderTests extends PartitionCapableBinderTests { @@ -86,34 +85,20 @@ public class RedisBinderTests extends PartitionCapableBinderTests { return true; } - @Override - @Test - @Ignore("https://github.com/spring-cloud/spring-cloud-stream/issues/247") - public void testSendAndReceivePubSub() throws Exception { - - //TimeUnit.SECONDS.sleep(2); - - super.testSendAndReceivePubSub(); - } - - @Before - public void setup() { - createTemplate().boundListOps("queue.direct.0").trim(1, 0); - } - @Test public void testConsumerProperties() throws Exception { Binder binder = getBinder(); Properties properties = new Properties(); properties.put("maxAttempts", "1"); // disable retry - binder.bindConsumer("props.0", new DirectChannel(), properties); + Binding binding = binder.bindConsumer("props.0", "test", new DirectChannel(), properties); @SuppressWarnings("unchecked") - List bindings = TestUtils.getPropertyValue(binder, "binder.bindings", List.class); + List> bindings = TestUtils.getPropertyValue(binder, "binder.bindings", List.class); assertEquals(1, bindings.size()); - AbstractEndpoint endpoint = bindings.get(0).getEndpoint(); + assertEquals(binding, bindings.get(0)); + AbstractEndpoint endpoint = binding.getEndpoint(); assertThat(endpoint, instanceOf(RedisQueueMessageDrivenEndpoint.class)); assertSame(DirectChannel.class, TestUtils.getPropertyValue(endpoint, "outputChannel").getClass()); - binder.unbindConsumers("props.0"); + binder.unbind(binding); assertEquals(0, bindings.size()); properties.put("backOffInitialInterval", "2000"); @@ -123,48 +108,34 @@ public class RedisBinderTests extends PartitionCapableBinderTests { properties.put("maxAttempts", "23"); properties.put("partitionIndex", 0); - binder.bindConsumer("props.0", new DirectChannel(), properties); + binding = binder.bindConsumer("props.0", "test", new DirectChannel(), properties); assertEquals(1, bindings.size()); - endpoint = bindings.get(0).getEndpoint(); + assertEquals(binding, bindings.get(0)); + endpoint = binding.getEndpoint(); verifyConsumer(endpoint); - try { - binder.bindPubSubConsumer("dummy", null, null, properties); - fail("Expected exception"); - } - catch (IllegalArgumentException e) { - assertThat(e.getMessage(), allOf( - containsString(getClassUnderTestName() + " does not support consumer properties: "), - containsString("partitionIndex"), - containsString("concurrency"), - containsString(" for dummy."))); - } - try { - binder.bindConsumer("queue:dummy", null, properties); - fail("Expected exception"); - } - catch (IllegalArgumentException e) { - assertEquals(getClassUnderTestName() + " does not support consumer property: partitionIndex for queue:dummy.", - e.getMessage()); - } - - binder.unbindConsumers("props.0"); + binder.unbind(binding); assertEquals(0, bindings.size()); } @Test public void testProducerProperties() throws Exception { Binder binder = getBinder(); - binder.bindProducer("props.0", new DirectChannel(), null); + Binding consumerBinding = binder.bindConsumer("props.0", "test", new DirectChannel(), null); + Binding producerBinding = binder.bindProducer("props.0", new DirectChannel(), null); @SuppressWarnings("unchecked") - List bindings = TestUtils.getPropertyValue(binder, "binder.bindings", List.class); - assertEquals(1, bindings.size()); - AbstractEndpoint endpoint = bindings.get(0).getEndpoint(); + List> bindings = TestUtils.getPropertyValue(binder, "binder.bindings", List.class); + assertEquals(2, bindings.size()); + assertEquals(producerBinding, bindings.get(1)); + AbstractEndpoint endpoint = producerBinding.getEndpoint(); + @SuppressWarnings("unchecked") + Map adapters = TestUtils.getPropertyValue(endpoint, "handler.adapters", Map.class); + RedisQueueOutboundChannelAdapter adapter = adapters.get("test"); assertEquals( - "queue.props.0", - TestUtils.getPropertyValue(endpoint, "handler.delegate.queueNameExpression", Expression.class).getExpressionString()); - binder.unbindProducers("props.0"); - assertEquals(0, bindings.size()); + "props.0.test", + TestUtils.getPropertyValue(adapter, "queueNameExpression", Expression.class).getExpressionString()); + binder.unbind(producerBinding); + assertEquals(1, bindings.size()); Properties properties = new Properties(); properties.put("partitionKeyExpression", "'foo'"); @@ -173,138 +144,16 @@ public class RedisBinderTests extends PartitionCapableBinderTests { properties.put("partitionSelectorClass", "foo"); properties.put(BinderPropertyKeys.NEXT_MODULE_COUNT, "1"); - binder.bindProducer("props.0", new DirectChannel(), properties); - assertEquals(1, bindings.size()); - endpoint = bindings.get(0).getEndpoint(); - assertEquals( - "'queue.props.0-' + headers['partition']", - TestUtils.getPropertyValue(endpoint, "handler.delegate.queueNameExpression", Expression.class).getExpressionString()); - - try { - binder.bindPubSubProducer("dummy", null, properties); - fail("Expected exception"); - } - catch (IllegalArgumentException e) { - assertThat(e.getMessage(), allOf( - containsString(getClassUnderTestName() + " does not support producer properties: "), - containsString("partitionSelectorExpression"), - containsString("partitionKeyExtractorClass"), - containsString("partitionKeyExpression"), - containsString("partitionSelectorClass"))); - assertThat(e.getMessage(), containsString("for dummy.")); - } - try { - binder.bindProducer("queue:dummy", new DirectChannel(), properties); - fail("Expected exception"); - } - catch (IllegalArgumentException e) { - assertThat(e.getMessage(), allOf( - containsString(getClassUnderTestName() + " does not support producer properties: "), - containsString("partitionSelectorExpression"), - containsString("partitionKeyExtractorClass"), - containsString("partitionKeyExpression"), - containsString("partitionSelectorClass"))); - assertThat(e.getMessage(), containsString("for queue:dummy.")); - } - - binder.unbindProducers("props.0"); - assertEquals(0, bindings.size()); - } - - @Test - public void testRequestReplyRequestorProperties() throws Exception { - Binder binder = getBinder(); - Properties properties = new Properties(); - - properties.put("backOffInitialInterval", "2000"); - properties.put("backOffMaxInterval", "20000"); - properties.put("backOffMultiplier", "5.0"); - properties.put("concurrency", "2"); - properties.put("maxAttempts", "23"); - - binder.bindRequestor("props.0", new DirectChannel(), new DirectChannel(), properties); - @SuppressWarnings("unchecked") - List bindings = TestUtils.getPropertyValue(binder, "binder.bindings", List.class); - + producerBinding = binder.bindProducer("props.0", new DirectChannel(), properties); assertEquals(2, bindings.size()); - AbstractEndpoint endpoint = bindings.get(0).getEndpoint(); // producer + endpoint = bindings.get(1).getEndpoint(); + adapter = (RedisQueueOutboundChannelAdapter) TestUtils.getPropertyValue(endpoint, "handler.adapters", Map.class).get("test"); assertEquals( - "queue.props.0.requests", - TestUtils.getPropertyValue(endpoint, "handler.delegate.queueNameExpression", Expression.class).getExpressionString()); + "'props.0.test-' + headers['partition']", + TestUtils.getPropertyValue(adapter, "queueNameExpression", Expression.class).getExpressionString()); - endpoint = bindings.get(1).getEndpoint(); // consumer - verifyConsumer(endpoint); - - properties.put("partitionKeyExpression", "'foo'"); - properties.put("partitionKeyExtractorClass", "foo"); - properties.put("partitionSelectorExpression", "0"); - properties.put("partitionSelectorClass", "foo"); - properties.put("partitionIndex", "0"); - try { - binder.bindRequestor("dummy", new DirectChannel(), new DirectChannel(), properties); - fail("Expected exception"); - } - catch (IllegalArgumentException e) { - assertThat(e.getMessage(), allOf( - containsString(getClassUnderTestName() + " does not support producer properties: "), - containsString("partitionSelectorExpression"), - containsString("partitionKeyExtractorClass"), - containsString("partitionKeyExpression"), - containsString("partitionSelectorClass"))); - assertThat(e.getMessage(), allOf(containsString("partitionIndex"), containsString("for dummy."))); - } - - binder.unbindConsumers("props.0"); - binder.unbindProducers("props.0"); - assertEquals(0, bindings.size()); - } - - @Test - public void testRequestReplyReplierProperties() throws Exception { - Binder binder = getBinder(); - Properties properties = new Properties(); - - properties.put("backOffInitialInterval", "2000"); - properties.put("backOffMaxInterval", "20000"); - properties.put("backOffMultiplier", "5.0"); - properties.put("concurrency", "2"); - properties.put("maxAttempts", "23"); - - binder.bindReplier("props.0", new DirectChannel(), new DirectChannel(), properties); - @SuppressWarnings("unchecked") - List bindings = TestUtils.getPropertyValue(binder, "binder.bindings", List.class); - - assertEquals(2, bindings.size()); - AbstractEndpoint endpoint = bindings.get(1).getEndpoint(); // producer - assertEquals( - "headers['replyTo']", - TestUtils.getPropertyValue(endpoint, "handler.delegate.queueNameExpression", Expression.class).getExpressionString()); - - endpoint = bindings.get(0).getEndpoint(); // consumer - verifyConsumer(endpoint); - - properties.put("partitionKeyExpression", "'foo'"); - properties.put("partitionKeyExtractorClass", "foo"); - properties.put("partitionSelectorExpression", "0"); - properties.put("partitionSelectorClass", "foo"); - properties.put(BinderPropertyKeys.NEXT_MODULE_COUNT, "1"); - properties.put("partitionIndex", "0"); - try { - binder.bindReplier("dummy", new DirectChannel(), new DirectChannel(), properties); - fail("Expected exception"); - } - catch (IllegalArgumentException e) { - assertThat(e.getMessage(), allOf( - containsString(getClassUnderTestName() + " does not support consumer properties: "), - containsString("partitionSelectorExpression"), - containsString("partitionKeyExtractorClass"), - containsString("partitionKeyExpression"), - containsString("partitionSelectorClass"))); - assertThat(e.getMessage(), allOf(containsString("partitionIndex"), containsString("for dummy."))); - } - - binder.unbindConsumers("props.0"); - binder.unbindProducers("props.0"); + binder.unbind(producerBinding); + binder.unbind(consumerBinding); assertEquals(0, bindings.size()); } @@ -335,12 +184,13 @@ public class RedisBinderTests extends PartitionCapableBinderTests { props.put("maxAttempts", 2); props.put("backOffInitialInterval", 100); props.put("backOffMultiplier", "1.0"); - binder.bindConsumer("retry.0", new DirectChannel(), props); // no subscriber + Binding consumerBinding = binder.bindConsumer("retry.0", "test", new DirectChannel(), props); // no subscriber channel.send(new GenericMessage("foo")); RedisTemplate template = createTemplate(); - Object rightPop = template.boundListOps("ERRORS:retry.0").rightPop(5, TimeUnit.SECONDS); + Object rightPop = template.boundListOps("ERRORS:retry.0.test").rightPop(5, TimeUnit.SECONDS); assertNotNull(rightPop); assertThat(new String((byte[]) rightPop), containsString("foo")); + binder.unbind(consumerBinding); } @Test @@ -352,10 +202,6 @@ public class RedisBinderTests extends PartitionCapableBinderTests { assertTrue(headers.contains("bar")); } - @Override @Ignore("https://github.com/spring-cloud/spring-cloud-stream/issues/247") - public void createInboundPubSubBeforeOutboundPubSub() throws Exception { - } - private RedisTemplate createTemplate() { if (this.redisTemplate != null) { return this.redisTemplate; @@ -370,13 +216,15 @@ public class RedisBinderTests extends PartitionCapableBinderTests { } @Override + @SuppressWarnings("unchecked") protected String getEndpointRouting(AbstractEndpoint endpoint) { - return TestUtils.getPropertyValue(endpoint, "handler.delegate.queueNameExpression", Expression.class).getExpressionString(); + Map adapters = TestUtils.getPropertyValue(endpoint, "handler.adapters", Map.class); + return TestUtils.getPropertyValue(adapters.values().iterator().next(), "queueNameExpression", Expression.class).getExpressionString(); } @Override - protected String getPubSubEndpointRouting(AbstractEndpoint endpoint) { - return TestUtils.getPropertyValue(endpoint, "handler.delegate.topicExpression", Expression.class).getExpressionString(); + protected String getExpectedRoutingBaseDestination(String name, String group) { + return name + "." + group; } @Override @@ -391,7 +239,7 @@ public class RedisBinderTests extends PartitionCapableBinderTests { @Override public Object receive(boolean expectNull) throws Exception { - byte[] bytes = (byte[]) template.boundListOps("queue." + queue).rightPop(50, TimeUnit.MILLISECONDS); + byte[] bytes = (byte[]) template.boundListOps(queue).rightPop(50, TimeUnit.MILLISECONDS); if (bytes == null) { return null; } diff --git a/spring-cloud-stream-binders/spring-cloud-stream-binder-redis/src/test/java/org/springframework/cloud/stream/binder/redis/RedisTestBinder.java b/spring-cloud-stream-binders/spring-cloud-stream-binder-redis/src/test/java/org/springframework/cloud/stream/binder/redis/RedisTestBinder.java index 3da99192c..d3fc05276 100644 --- a/spring-cloud-stream-binders/spring-cloud-stream-binder-redis/src/test/java/org/springframework/cloud/stream/binder/redis/RedisTestBinder.java +++ b/spring-cloud-stream-binders/spring-cloud-stream-binder-redis/src/test/java/org/springframework/cloud/stream/binder/redis/RedisTestBinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-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. @@ -16,18 +16,16 @@ package org.springframework.cloud.stream.binder.redis; +import org.springframework.cloud.stream.binder.AbstractTestBinder; import org.springframework.context.support.GenericApplicationContext; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.integration.channel.DefaultHeaderChannelRegistry; -import org.springframework.integration.codec.Codec; import org.springframework.integration.codec.kryo.PojoCodec; import org.springframework.integration.context.IntegrationContextUtils; import org.springframework.integration.support.DefaultMessageBuilderFactory; import org.springframework.integration.support.utils.IntegrationUtils; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; -import org.springframework.cloud.stream.binder.AbstractTestBinder; - /** * Test support class for {@link RedisMessageChannelBinder}. diff --git a/spring-cloud-stream-binders/spring-cloud-stream-binder-test/src/main/java/org/springframework/cloud/stream/binder/AbstractBinderTests.java b/spring-cloud-stream-binders/spring-cloud-stream-binder-test/src/main/java/org/springframework/cloud/stream/binder/AbstractBinderTests.java index 36ffa8950..98dc1f3dc 100644 --- a/spring-cloud-stream-binders/spring-cloud-stream-binder-test/src/main/java/org/springframework/cloud/stream/binder/AbstractBinderTests.java +++ b/spring-cloud-stream-binders/spring-cloud-stream-binder-test/src/main/java/org/springframework/cloud/stream/binder/AbstractBinderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2014 the original author or authors. + * Copyright 2013-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. @@ -17,19 +17,15 @@ package org.springframework.cloud.stream.binder; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.Properties; import org.junit.After; -import org.junit.Assert; import org.junit.Test; import org.springframework.beans.DirectFieldAccessor; @@ -45,6 +41,7 @@ import org.springframework.messaging.MessageHeaders; * @author Gary Russell * @author Ilayaperumal Gopinathan * @author David Turanski + * @author Mark Fisher */ public abstract class AbstractBinderTests { @@ -55,20 +52,20 @@ public abstract class AbstractBinderTests { @Test public void testClean() throws Exception { Binder binder = getBinder(); - binder.bindProducer("foo.0", new DirectChannel(), null); - binder.bindConsumer("foo.0", new DirectChannel(), null); - binder.bindProducer("foo.1", new DirectChannel(), null); - binder.bindConsumer("foo.1", new DirectChannel(), null); - binder.bindProducer("foo.2", new DirectChannel(), null); + Binding foo0ProducerBinding = binder.bindProducer("foo.0", new DirectChannel(), null); + Binding foo0ConsumerBinding = binder.bindConsumer("foo.0", "test", new DirectChannel(), null); + Binding foo1ProducerBinding = binder.bindProducer("foo.1", new DirectChannel(), null); + Binding foo1ConsumerBinding = binder.bindConsumer("foo.1", "test", new DirectChannel(), null); + Binding foo2ProducerBinding = binder.bindProducer("foo.2", new DirectChannel(), null); Collection bindings = getBindings(binder); assertEquals(5, bindings.size()); - binder.unbindProducers("foo.0"); + binder.unbind(foo0ProducerBinding); assertEquals(4, bindings.size()); - binder.unbindConsumers("foo.0"); - binder.unbindProducers("foo.1"); + binder.unbind(foo0ConsumerBinding); + binder.unbind(foo1ProducerBinding); assertEquals(2, bindings.size()); - binder.unbindConsumers("foo.1"); - binder.unbindProducers("foo.2"); + binder.unbind(foo1ConsumerBinding); + binder.unbind(foo2ProducerBinding); assertTrue(bindings.isEmpty()); } @@ -77,8 +74,8 @@ public abstract class AbstractBinderTests { Binder binder = getBinder(); DirectChannel moduleOutputChannel = new DirectChannel(); QueueChannel moduleInputChannel = new QueueChannel(); - binder.bindProducer("foo.0", moduleOutputChannel, null); - binder.bindConsumer("foo.0", moduleInputChannel, null); + Binding producerBinding = binder.bindProducer("foo.0", moduleOutputChannel, null); + Binding consumerBinding = binder.bindConsumer("foo.0", "test", moduleInputChannel, null); Message message = MessageBuilder.withPayload("foo").setHeader(MessageHeaders.CONTENT_TYPE, "foo/bar").build(); // Let the consumer actually bind to the producer before sending a msg @@ -89,8 +86,8 @@ public abstract class AbstractBinderTests { assertEquals("foo", inbound.getPayload()); assertNull(inbound.getHeaders().get(BinderHeaders.BINDER_ORIGINAL_CONTENT_TYPE)); assertEquals("foo/bar", inbound.getHeaders().get(MessageHeaders.CONTENT_TYPE)); - binder.unbindProducers("foo.0"); - binder.unbindConsumers("foo.0"); + binder.unbind(producerBinding); + binder.unbind(consumerBinding); } @Test @@ -98,8 +95,8 @@ public abstract class AbstractBinderTests { Binder binder = getBinder(); DirectChannel moduleOutputChannel = new DirectChannel(); QueueChannel moduleInputChannel = new QueueChannel(); - binder.bindProducer("bar.0", moduleOutputChannel, null); - binder.bindConsumer("bar.0", moduleInputChannel, null); + Binding producerBinding = binder.bindProducer("bar.0", moduleOutputChannel, null); + Binding consumerBinding = binder.bindConsumer("bar.0", "test", moduleInputChannel, null); binderBindUnbindLatency(); Message message = MessageBuilder.withPayload("foo").build(); @@ -109,164 +106,13 @@ public abstract class AbstractBinderTests { assertEquals("foo", inbound.getPayload()); assertNull(inbound.getHeaders().get(BinderHeaders.BINDER_ORIGINAL_CONTENT_TYPE)); assertNull(inbound.getHeaders().get(MessageHeaders.CONTENT_TYPE)); - binder.unbindProducers("bar.0"); - binder.unbindConsumers("bar.0"); - } - - @Test - public void testSendAndReceivePubSub() throws Exception { - Binder binder = getBinder(); - DirectChannel moduleOutputChannel = new DirectChannel(); - QueueChannel moduleInputChannel = new QueueChannel(); - QueueChannel module2InputChannel = new QueueChannel(); - QueueChannel module3InputChannel = new QueueChannel(); - binder.bindProducer("baz.0", moduleOutputChannel, null); - binder.bindConsumer("baz.0", moduleInputChannel, null); - // A new module is using the tap as an input channel - String fooTapName = "baz.0"; - binder.bindPubSubConsumer(fooTapName, module2InputChannel, "tgroup1", null); - // Another new module is using tap as an input channel - String barTapName = "baz.0"; - binder.bindPubSubConsumer(barTapName, module3InputChannel, "tgroup2", null); - Message message = MessageBuilder.withPayload("foo").setHeader(MessageHeaders.CONTENT_TYPE, - "foo/bar").build(); - boolean success = false; - boolean retried = false; - while (!success) { - moduleOutputChannel.send(message); - Message inbound = moduleInputChannel.receive(5000); - assertNotNull(inbound); - assertEquals("foo", inbound.getPayload()); - assertNull(inbound.getHeaders().get(BinderHeaders.BINDER_ORIGINAL_CONTENT_TYPE)); - assertEquals("foo/bar", inbound.getHeaders().get(MessageHeaders.CONTENT_TYPE)); - Message tapped1 = module2InputChannel.receive(5000); - Message tapped2 = module3InputChannel.receive(5000); - if (tapped1 == null || tapped2 == null) { - // listener may not have started - assertFalse("Failed to receive tap after retry", retried); - retried = true; - continue; - } - success = true; - assertEquals("foo", tapped1.getPayload()); - assertNull(tapped1.getHeaders().get(BinderHeaders.BINDER_ORIGINAL_CONTENT_TYPE)); - assertEquals("foo/bar", tapped1.getHeaders().get(MessageHeaders.CONTENT_TYPE)); - assertEquals("foo", tapped2.getPayload()); - assertNull(tapped2.getHeaders().get(BinderHeaders.BINDER_ORIGINAL_CONTENT_TYPE)); - assertEquals("foo/bar", tapped2.getHeaders().get(MessageHeaders.CONTENT_TYPE)); - } - // delete one tap stream is deleted - binder.unbindPubSubConsumers(barTapName, "tgroup2"); - Message message2 = MessageBuilder.withPayload("bar").setHeader(MessageHeaders.CONTENT_TYPE, - "foo/bar").build(); - moduleOutputChannel.send(message2); - - // other tap still receives messages - Message tapped = module2InputChannel.receive(5000); - assertNotNull(tapped); - - // Removed tap does not - assertNull(module3InputChannel.receive(1000)); - - // when other tap stream is deleted - binder.unbindConsumer(fooTapName, module2InputChannel); - // Clean up as StreamPlugin would - binder.unbindConsumer("baz.0", moduleInputChannel); - binder.unbindProducer("baz.0", moduleOutputChannel); - binder.unbindPubSubConsumers(fooTapName, "tgroup1"); - assertTrue(getBindings(binder).isEmpty()); - } - - @Test - public void createInboundPubSubBeforeOutboundPubSub() throws Exception { - Binder binder = getBinder(); - DirectChannel moduleOutputChannel = new DirectChannel(); - QueueChannel moduleInputChannel = new QueueChannel(); - QueueChannel module2InputChannel = new QueueChannel(); - QueueChannel module3InputChannel = new QueueChannel(); - // Create the tap first - String fooTapName = "baz.0"; - binder.bindPubSubConsumer(fooTapName, module2InputChannel, "tgroup1", null); - - // Then create the stream - binder.bindProducer("baz.0", moduleOutputChannel, null); - binder.bindConsumer("baz.0", moduleInputChannel, null); - - // Another new module is using tap as an input channel - String barTapName = "baz.0"; - binder.bindPubSubConsumer(barTapName, module3InputChannel, "tgroup2", null); - Message message = MessageBuilder.withPayload("foo").setHeader(MessageHeaders.CONTENT_TYPE, - "foo/bar").build(); - boolean success = false; - boolean retried = false; - while (!success) { - moduleOutputChannel.send(message); - Message inbound = moduleInputChannel.receive(5000); - assertNotNull(inbound); - assertEquals("foo", inbound.getPayload()); - assertNull(inbound.getHeaders().get(BinderHeaders.BINDER_ORIGINAL_CONTENT_TYPE)); - assertEquals("foo/bar", inbound.getHeaders().get(MessageHeaders.CONTENT_TYPE)); - Message tapped1 = module2InputChannel.receive(5000); - Message tapped2 = module3InputChannel.receive(5000); - if (tapped1 == null || tapped2 == null) { - // listener may not have started - assertFalse("Failed to receive tap after retry", retried); - retried = true; - continue; - } - success = true; - assertEquals("foo", tapped1.getPayload()); - assertNull(tapped1.getHeaders().get(BinderHeaders.BINDER_ORIGINAL_CONTENT_TYPE)); - assertEquals("foo/bar", tapped1.getHeaders().get(MessageHeaders.CONTENT_TYPE)); - assertEquals("foo", tapped2.getPayload()); - assertNull(tapped2.getHeaders().get(BinderHeaders.BINDER_ORIGINAL_CONTENT_TYPE)); - assertEquals("foo/bar", tapped2.getHeaders().get(MessageHeaders.CONTENT_TYPE)); - } - // delete one tap stream is deleted - binder.unbindPubSubConsumers(barTapName, "tgroup2"); - Message message2 = MessageBuilder.withPayload("bar").setHeader(MessageHeaders.CONTENT_TYPE, - "foo/bar").build(); - moduleOutputChannel.send(message2); - - // other tap still receives messages - Message tapped = module2InputChannel.receive(5000); - assertNotNull(tapped); - - // Removed tap does not - assertNull(module3InputChannel.receive(1000)); - - // when other tap stream is deleted - binder.unbindConsumer(fooTapName, module2InputChannel); - // Clean up as StreamPlugin would - binder.unbindConsumer("baz.0", moduleInputChannel); - binder.unbindProducer("baz.0", moduleOutputChannel); - binder.unbindPubSubConsumers(fooTapName, "tgroup1"); - assertTrue(getBindings(binder).isEmpty()); - } - - @Test - public void testBadDynamic() throws Exception { - Properties properties = new Properties(); - properties.setProperty(BinderPropertyKeys.PARTITION_KEY_EXPRESSION, "'foo'"); - Binder binder = getBinder(); - try { - binder.bindDynamicProducer("queue:foo", properties); - fail("Exception expected"); - } - catch (BinderException mbe) { - Assert.assertEquals("Failed to bind dynamic channel 'queue:foo' with properties " + - "{partitionKeyExpression='foo'}", - mbe.getMessage()); - if (binder instanceof AbstractTestBinder) { - binder = ((AbstractTestBinder) binder).getCoreBinder(); - } - assertFalse(((MessageChannelBinderSupport) binder).getApplicationContext().containsBean("queue:foo")); - } + binder.unbind(producerBinding); + binder.unbind(consumerBinding); } protected Collection getBindings(Binder testBinder) { if (testBinder instanceof AbstractTestBinder) { - return getBindingsFromBinder(((AbstractTestBinder) testBinder).getCoreBinder()); + return getBindingsFromBinder(((AbstractTestBinder) testBinder).getCoreBinder()); } return Collections.EMPTY_LIST; } diff --git a/spring-cloud-stream-binders/spring-cloud-stream-binder-test/src/main/java/org/springframework/cloud/stream/binder/AbstractTestBinder.java b/spring-cloud-stream-binders/spring-cloud-stream-binder-test/src/main/java/org/springframework/cloud/stream/binder/AbstractTestBinder.java index f5caa5fd7..a4cc37e69 100644 --- a/spring-cloud-stream-binders/spring-cloud-stream-binder-test/src/main/java/org/springframework/cloud/stream/binder/AbstractTestBinder.java +++ b/spring-cloud-stream-binders/spring-cloud-stream-binder-test/src/main/java/org/springframework/cloud/stream/binder/AbstractTestBinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-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. @@ -22,19 +22,17 @@ import java.util.Set; import org.springframework.messaging.MessageChannel; - /** * Abstract class that adds test support for {@link Binder}. * * @author Ilayaperumal Gopinathan * @author Gary Russell + * @author Mark Fisher */ public abstract class AbstractTestBinder implements Binder { protected Set queues = new HashSet(); - protected Set topics = new HashSet(); - private C binder; public void setBinder(C binder) { @@ -48,45 +46,15 @@ public abstract class AbstractTestBinder } @Override - public void bindConsumer(String name, MessageChannel moduleInputChannel, Properties properties) { - binder.bindConsumer(name, moduleInputChannel, properties); + public Binding bindConsumer(String name, String group, MessageChannel moduleInputChannel, Properties properties) { queues.add(name); + return binder.bindConsumer(name, group, moduleInputChannel, properties); } @Override - public void bindPubSubConsumer(String name, MessageChannel inputChannel, String group, Properties properties) { - binder.bindPubSubConsumer(name, inputChannel, group, properties); - addTopic(name); - } - - @Override - public void bindProducer(String name, MessageChannel moduleOutputChannel, Properties properties) { - binder.bindProducer(name, moduleOutputChannel, properties); + public Binding bindProducer(String name, MessageChannel moduleOutputChannel, Properties properties) { queues.add(name); - } - - @Override - public void bindPubSubProducer(String name, MessageChannel outputChannel, Properties properties) { - binder.bindPubSubProducer(name, outputChannel, properties); - addTopic(name); - } - - @Override - public void bindRequestor(String name, MessageChannel requests, MessageChannel replies, - Properties properties) { - binder.bindRequestor(name, requests, replies, properties); - queues.add(name + ".requests"); - } - - @Override - public void bindReplier(String name, MessageChannel requests, MessageChannel replies, - Properties properties) { - binder.bindReplier(name, requests, replies, properties); - queues.add(name + ".requests"); - } - - private void addTopic(String topicName) { - topics.add("topic." + topicName); + return binder.bindProducer(name, moduleOutputChannel, properties); } public C getCoreBinder() { @@ -96,40 +64,8 @@ public abstract class AbstractTestBinder public abstract void cleanup(); @Override - public void unbindConsumers(String name) { - binder.unbindConsumers(name); - } - - @Override - public void unbindPubSubConsumers(String name, String group) { - binder.unbindPubSubConsumers(name, group); - } - - @Override - public void unbindProducers(String name) { - binder.unbindProducers(name); - } - - @Override - public void unbindConsumer(String name, MessageChannel channel) { - binder.unbindConsumer(name, channel); - } - - @Override - public void unbindProducer(String name, MessageChannel channel) { - binder.unbindProducer(name, channel); - } - - @Override - public MessageChannel bindDynamicProducer(String name, Properties properties) { - this.queues.add(name); - return this.binder.bindDynamicProducer(name, properties); - } - - @Override - public MessageChannel bindDynamicPubSubProducer(String name, Properties properties) { - this.topics.add(name); - return this.binder.bindDynamicPubSubProducer(name, properties); + public void unbind(Binding binding) { + binder.unbind(binding); } public C getBinder() { diff --git a/spring-cloud-stream-binders/spring-cloud-stream-binder-test/src/main/java/org/springframework/cloud/stream/binder/BrokerBinderTests.java b/spring-cloud-stream-binders/spring-cloud-stream-binder-test/src/main/java/org/springframework/cloud/stream/binder/BrokerBinderTests.java index 60e03ceab..59f9a926f 100644 --- a/spring-cloud-stream-binders/spring-cloud-stream-binder-test/src/main/java/org/springframework/cloud/stream/binder/BrokerBinderTests.java +++ b/spring-cloud-stream-binders/spring-cloud-stream-binder-test/src/main/java/org/springframework/cloud/stream/binder/BrokerBinderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-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. @@ -16,91 +16,12 @@ package org.springframework.cloud.stream.binder; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; - -import java.util.Properties; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; - -import org.junit.Test; - -import org.springframework.integration.channel.DirectChannel; -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageHandler; -import org.springframework.messaging.MessagingException; -import org.springframework.messaging.support.GenericMessage; - - /** * Tests for binders that use an external broker. * * @author Gary Russell */ -public abstract class BrokerBinderTests extends - AbstractBinderTests { - - @Test - public void testDirectBinding() throws Exception { - Binder binder = getBinder(); - Properties properties = new Properties(); - properties.setProperty(BinderPropertyKeys.DIRECT_BINDING_ALLOWED, "true"); - - DirectChannel moduleInputChannel = new DirectChannel(); - moduleInputChannel.setBeanName("direct.input"); - DirectChannel moduleOutputChannel = new DirectChannel(); - moduleOutputChannel.setBeanName("direct.output"); - binder.bindConsumer("direct.0", moduleInputChannel, null); - binder.bindProducer("direct.0", moduleOutputChannel, properties); - - final AtomicReference caller = new AtomicReference(); - final AtomicInteger count = new AtomicInteger(); - moduleInputChannel.subscribe(new MessageHandler() { - - @Override - public void handleMessage(Message message) throws MessagingException { - caller.set(Thread.currentThread()); - count.incrementAndGet(); - } - }); - - moduleOutputChannel.send(new GenericMessage("foo")); - moduleOutputChannel.send(new GenericMessage("foo")); - - assertNotNull(caller.get()); - assertSame(Thread.currentThread(), caller.get()); - assertEquals(2, count.get()); - assertNull(spyOn("direct.0").receive(true)); - - // Remove direct binding and bind the producer - binder.unbindConsumers("direct.0"); - binderBindUnbindLatency(); - - Spy spy = spyOn("direct.0"); - count.set(0); - moduleOutputChannel.send(new GenericMessage("bar")); - moduleOutputChannel.send(new GenericMessage("baz")); - Object bar = spy.receive(false); - assertEquals("bar", bar); - Object baz = spy.receive(false); - assertEquals("baz", baz); - assertEquals(0, count.get()); - - // Unbind producer from binder and bind directly again - caller.set(null); - binder.bindConsumer("direct.0", moduleInputChannel, null); - moduleOutputChannel.send(new GenericMessage("foo")); - moduleOutputChannel.send(new GenericMessage("foo")); - assertNotNull(caller.get()); - assertSame(Thread.currentThread(), caller.get()); - assertEquals(2, count.get()); - assertNull(spy.receive(true)); - - binder.unbindProducers("direct.0"); - binder.unbindConsumers("direct.0"); - } +public abstract class BrokerBinderTests extends AbstractBinderTests { /** * Create a new spy on the given 'queue'. This allows de-correlating the creation of @@ -109,6 +30,4 @@ public abstract class BrokerBinderTests extends */ public abstract Spy spyOn(final String name); - - } diff --git a/spring-cloud-stream-binders/spring-cloud-stream-binder-test/src/main/java/org/springframework/cloud/stream/binder/PartitionCapableBinderTests.java b/spring-cloud-stream-binders/spring-cloud-stream-binder-test/src/main/java/org/springframework/cloud/stream/binder/PartitionCapableBinderTests.java index d3e8262af..e3c2ad6fb 100644 --- a/spring-cloud-stream-binders/spring-cloud-stream-binder-test/src/main/java/org/springframework/cloud/stream/binder/PartitionCapableBinderTests.java +++ b/spring-cloud-stream-binders/spring-cloud-stream-binder-test/src/main/java/org/springframework/cloud/stream/binder/PartitionCapableBinderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-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. @@ -43,11 +43,11 @@ import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.support.GenericMessage; - /** * Tests for binders that support partitioning. * * @author Gary Russell + * @author Mark Fisher */ abstract public class PartitionCapableBinderTests extends BrokerBinderTests { @@ -67,58 +67,59 @@ abstract public class PartitionCapableBinderTests extends BrokerBinderTests { + " does not support producer "), containsString("foo"), containsString("baz"), - containsString(" for badprops.0."))); + containsString(" for badprops.0"))); } properties.remove("baz"); try { - binder.bindConsumer("badprops.0", output, properties); + binder.bindConsumer("badprops.0", "test", output, properties); } catch (IllegalArgumentException e) { assertThat(e.getMessage(), equalTo(getClassUnderTestName() - + " does not support consumer property: foo for badprops.0.")); + + " does not support consumer property: foo for badprops.0.test.")); } } @Test public void testPartitionedModuleSpEL() throws Exception { Binder binder = getBinder(); - Properties properties = new Properties(); - properties.put("partitionKeyExpression", "payload"); - properties.put("partitionSelectorExpression", "hashCode()"); - properties.put(BinderPropertyKeys.NEXT_MODULE_COUNT, "3"); - properties.put(BinderPropertyKeys.NEXT_MODULE_CONCURRENCY, "2"); + + Properties consumerProperties = new Properties(); + consumerProperties.put("concurrency", "2"); + consumerProperties.put("partitionIndex", "0"); + consumerProperties.put("count","3"); + QueueChannel input0 = new QueueChannel(); + input0.setBeanName("test.input0S"); + Binding input0Binding = binder.bindConsumer("part.0", "test", input0, consumerProperties); + consumerProperties.put("partitionIndex", "1"); + QueueChannel input1 = new QueueChannel(); + input1.setBeanName("test.input1S"); + Binding input1Binding = binder.bindConsumer("part.0", "test", input1, consumerProperties); + consumerProperties.put("partitionIndex", "2"); + QueueChannel input2 = new QueueChannel(); + input2.setBeanName("test.input2S"); + Binding input2Binding = binder.bindConsumer("part.0", "test", input2, consumerProperties); + + Properties producerProperties = new Properties(); + producerProperties.put("partitionKeyExpression", "payload"); + producerProperties.put("partitionSelectorExpression", "hashCode()"); + producerProperties.put(BinderPropertyKeys.NEXT_MODULE_COUNT, "3"); + producerProperties.put(BinderPropertyKeys.NEXT_MODULE_CONCURRENCY, "2"); DirectChannel output = new DirectChannel(); output.setBeanName("test.output"); - binder.bindProducer("part.0", output, properties); + Binding outputBinding = binder.bindProducer("part.0", output, producerProperties); @SuppressWarnings("unchecked") - List bindings = TestUtils.getPropertyValue(binder, "binder.bindings", List.class); - assertEquals(1, bindings.size()); + List> bindings = TestUtils.getPropertyValue(binder, "binder.bindings", List.class); + assertEquals(4, bindings.size()); try { - AbstractEndpoint endpoint = bindings.get(0).getEndpoint(); - assertThat(getEndpointRouting(endpoint), containsString("part.0-' + headers['partition']")); + AbstractEndpoint endpoint = bindings.get(3).getEndpoint(); + assertThat(getEndpointRouting(endpoint), containsString( + getExpectedRoutingBaseDestination("part.0", "test") + "-' + headers['partition']")); } catch (UnsupportedOperationException ignored) { - } - properties.clear(); - properties.put("concurrency", "2"); - properties.put("partitionIndex", "0"); - properties.put("count","3"); - QueueChannel input0 = new QueueChannel(); - input0.setBeanName("test.input0S"); - binder.bindConsumer("part.0", input0, properties); - properties.put("partitionIndex", "1"); - QueueChannel input1 = new QueueChannel(); - input1.setBeanName("test.input1S"); - binder.bindConsumer("part.0", input1, properties); - properties.put("partitionIndex", "2"); - QueueChannel input2 = new QueueChannel(); - input2.setBeanName("test.input2S"); - binder.bindConsumer("part.0", input2, properties); - Message message2 = MessageBuilder.withPayload(2) .setHeader(IntegrationMessageHeaderAccessor.CORRELATION_ID, "foo") .setHeader(IntegrationMessageHeaderAccessor.SEQUENCE_NUMBER, 42) @@ -148,17 +149,13 @@ abstract public class PartitionCapableBinderTests extends BrokerBinderTests { return result; } }; - if (usesExplicitRouting()) { assertEquals(0, receive0.getPayload()); assertEquals(1, receive1.getPayload()); assertEquals(2, receive2.getPayload()); - assertThat(receive2, fooMatcher); - } else { - assertThat(Arrays.asList( (Integer) receive0.getPayload(), (Integer) receive1.getPayload(), @@ -176,46 +173,48 @@ abstract public class PartitionCapableBinderTests extends BrokerBinderTests { containsOur3Messages); } - - binder.unbindConsumers("part.0"); - binder.unbindProducers("part.0"); + binder.unbind(input0Binding); + binder.unbind(input1Binding); + binder.unbind(input2Binding); + binder.unbind(outputBinding); } @Test public void testPartitionedModuleJava() throws Exception { Binder binder = getBinder(); - Properties properties = new Properties(); - properties.put("partitionKeyExtractorClass", "org.springframework.cloud.stream.binder.PartitionTestSupport"); - properties.put("partitionSelectorClass", "org.springframework.cloud.stream.binder.PartitionTestSupport"); - properties.put(BinderPropertyKeys.NEXT_MODULE_COUNT, "3"); - properties.put(BinderPropertyKeys.NEXT_MODULE_CONCURRENCY, "2"); - DirectChannel output = new DirectChannel(); - output.setBeanName("test.output"); - binder.bindProducer("partJ.0", output, properties); - @SuppressWarnings("unchecked") - List bindings = TestUtils.getPropertyValue(binder, "binder.bindings", List.class); - assertEquals(1, bindings.size()); - if (usesExplicitRouting()) { - AbstractEndpoint endpoint = bindings.get(0).getEndpoint(); - assertThat(getEndpointRouting(endpoint), containsString("partJ.0-' + headers['partition']")); - } - - properties.clear(); - properties.put("concurrency", "2"); - properties.put("count","3"); - properties.put("partitionIndex", "0"); + Properties consumerProperties = new Properties(); + consumerProperties.put("concurrency", "2"); + consumerProperties.put("count","3"); + consumerProperties.put("partitionIndex", "0"); QueueChannel input0 = new QueueChannel(); input0.setBeanName("test.input0J"); - binder.bindConsumer("partJ.0", input0, properties); - properties.put("partitionIndex", "1"); + Binding input0Binding = binder.bindConsumer("partJ.0", "test", input0, consumerProperties); + consumerProperties.put("partitionIndex", "1"); QueueChannel input1 = new QueueChannel(); input1.setBeanName("test.input1J"); - binder.bindConsumer("partJ.0", input1, properties); - properties.put("partitionIndex", "2"); + Binding input1Binding = binder.bindConsumer("partJ.0", "test", input1, consumerProperties); + consumerProperties.put("partitionIndex", "2"); QueueChannel input2 = new QueueChannel(); input2.setBeanName("test.input2J"); - binder.bindConsumer("partJ.0", input2, properties); + Binding input2Binding = binder.bindConsumer("partJ.0", "test", input2, consumerProperties); + + Properties producerProperties = new Properties(); + producerProperties.put("partitionKeyExtractorClass", "org.springframework.cloud.stream.binder.PartitionTestSupport"); + producerProperties.put("partitionSelectorClass", "org.springframework.cloud.stream.binder.PartitionTestSupport"); + producerProperties.put(BinderPropertyKeys.NEXT_MODULE_COUNT, "3"); + producerProperties.put(BinderPropertyKeys.NEXT_MODULE_CONCURRENCY, "2"); + DirectChannel output = new DirectChannel(); + output.setBeanName("test.output"); + Binding outputBinding = binder.bindProducer("partJ.0", output, producerProperties); + @SuppressWarnings("unchecked") + List> bindings = TestUtils.getPropertyValue(binder, "binder.bindings", List.class); + assertEquals(4, bindings.size()); + if (usesExplicitRouting()) { + AbstractEndpoint endpoint = bindings.get(3).getEndpoint(); + assertThat(getEndpointRouting(endpoint), containsString( + getExpectedRoutingBaseDestination("partJ.0", "test") + "-' + headers['partition']")); + } output.send(new GenericMessage(2)); output.send(new GenericMessage(1)); @@ -242,8 +241,10 @@ abstract public class PartitionCapableBinderTests extends BrokerBinderTests { containsInAnyOrder(0, 1, 2)); } - binder.unbindConsumers("partJ.0"); - binder.unbindProducers("partJ.0"); + binder.unbind(input0Binding); + binder.unbind(input1Binding); + binder.unbind(input2Binding); + binder.unbind(outputBinding); } /** @@ -261,6 +262,14 @@ abstract public class PartitionCapableBinderTests extends BrokerBinderTests { throw new UnsupportedOperationException(); } + /** + * For implementations that rely on explicit routing, return the expected base destination + * (the part that precedes '-partition' within the expression). + */ + protected String getExpectedRoutingBaseDestination(String name, String group) { + throw new UnsupportedOperationException(); + } + /** * For implementations that rely on explicit routing, return the routing expression. */ diff --git a/spring-cloud-stream-binders/spring-cloud-stream-binder-test/src/test/java/org/springframework/cloud/stream/binder/MessageChannelBinderSupportTests.java b/spring-cloud-stream-binders/spring-cloud-stream-binder-test/src/test/java/org/springframework/cloud/stream/binder/MessageChannelBinderSupportTests.java index f633c9181..907483c0c 100644 --- a/spring-cloud-stream-binders/spring-cloud-stream-binder-test/src/test/java/org/springframework/cloud/stream/binder/MessageChannelBinderSupportTests.java +++ b/spring-cloud-stream-binders/spring-cloud-stream-binder-test/src/test/java/org/springframework/cloud/stream/binder/MessageChannelBinderSupportTests.java @@ -269,31 +269,13 @@ public class MessageChannelBinderSupportTests { public class TestMessageChannelBinder extends MessageChannelBinderSupport { @Override - public void bindConsumer(String name, MessageChannel channel, Properties properties) { + protected Binding doBindConsumer(String name, String group, MessageChannel channel, Properties properties) { + return null; } @Override - public void bindPubSubConsumer(String name, MessageChannel moduleInputChannel, String group, - Properties properties) { - } - - @Override - public void bindPubSubProducer(String name, MessageChannel moduleOutputChannel, - Properties properties) { - } - - @Override - public void bindProducer(String name, MessageChannel channel, Properties properties) { - } - - @Override - public void bindRequestor(String name, MessageChannel requests, MessageChannel replies, - Properties properties) { - } - - @Override - public void bindReplier(String name, MessageChannel requests, MessageChannel replies, - Properties properties) { + public Binding bindProducer(String name, MessageChannel channel, Properties properties) { + return null; } } diff --git a/spring-cloud-stream-samples/multibinder-differentsystems/src/test/java/multibinder/TwoKafkaBindersApplicationTest.java b/spring-cloud-stream-samples/multibinder-differentsystems/src/test/java/multibinder/TwoKafkaBindersApplicationTest.java index 62dd8dc0b..918e97f87 100644 --- a/spring-cloud-stream-samples/multibinder-differentsystems/src/test/java/multibinder/TwoKafkaBindersApplicationTest.java +++ b/spring-cloud-stream-samples/multibinder-differentsystems/src/test/java/multibinder/TwoKafkaBindersApplicationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-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. @@ -76,8 +76,8 @@ public class TwoKafkaBindersApplicationTest { binderFactory.getBinder("kafka1").bindProducer("dataIn", dataProducer, null); QueueChannel dataConsumer = new QueueChannel(); - binderFactory.getBinder("kafka2").bindPubSubConsumer("dataOut", dataConsumer, - UUID.randomUUID().toString(), null); + binderFactory.getBinder("kafka2").bindConsumer("dataOut", UUID.randomUUID().toString(), + dataConsumer, null); String testPayload = "testFoo" + UUID.randomUUID().toString(); dataProducer.send(MessageBuilder.withPayload(testPayload).build()); diff --git a/spring-cloud-stream-samples/multibinder/src/test/java/multibinder/RabbitAndRedisBinderApplicationTests.java b/spring-cloud-stream-samples/multibinder/src/test/java/multibinder/RabbitAndRedisBinderApplicationTests.java index 6d8981831..93b70f19d 100644 --- a/spring-cloud-stream-samples/multibinder/src/test/java/multibinder/RabbitAndRedisBinderApplicationTests.java +++ b/spring-cloud-stream-samples/multibinder/src/test/java/multibinder/RabbitAndRedisBinderApplicationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-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. @@ -60,10 +60,13 @@ public class RabbitAndRedisBinderApplicationTests { @Autowired private BinderFactory binderFactory; + private final String randomGroup = UUID.randomUUID().toString(); + @After public void cleanUp() { RabbitAdmin admin = new RabbitAdmin(rabbitTestSupport.getResource()); - admin.deleteQueue("binder.dataOut"); + admin.deleteQueue("binder.dataOut.default"); + admin.deleteQueue("binder.dataOut." + this.randomGroup); admin.deleteExchange("binder.dataOut"); } @@ -77,10 +80,10 @@ public class RabbitAndRedisBinderApplicationTests { binderFactory.getBinder("redis").bindProducer("dataIn", dataProducer, null); QueueChannel dataConsumer = new QueueChannel(); - binderFactory.getBinder("rabbit").bindPubSubConsumer("dataOut", dataConsumer, - UUID.randomUUID().toString(), null); + binderFactory.getBinder("rabbit").bindConsumer("dataOut", this.randomGroup, + dataConsumer, null); - String testPayload = "testFoo" + UUID.randomUUID().toString(); + String testPayload = "testFoo" + this.randomGroup; dataProducer.send(MessageBuilder.withPayload(testPayload).build()); Message receive = dataConsumer.receive(2000); diff --git a/spring-cloud-stream-test-support/src/main/java/org/springframework/cloud/stream/test/binder/TestSupportBinder.java b/spring-cloud-stream-test-support/src/main/java/org/springframework/cloud/stream/test/binder/TestSupportBinder.java index 79cf12dc2..921814416 100644 --- a/spring-cloud-stream-test-support/src/main/java/org/springframework/cloud/stream/test/binder/TestSupportBinder.java +++ b/spring-cloud-stream-test-support/src/main/java/org/springframework/cloud/stream/test/binder/TestSupportBinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-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. @@ -23,6 +23,7 @@ import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingDeque; import org.springframework.cloud.stream.binder.Binder; +import org.springframework.cloud.stream.binder.Binding; import org.springframework.cloud.stream.test.matcher.MessageQueueMatcher; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; @@ -39,6 +40,7 @@ import org.springframework.util.Assert; * * @author Eric Bottard * @author Gary Russell + * @author Mark Fisher * @see MessageQueueMatcher */ public class TestSupportBinder implements Binder { @@ -47,78 +49,29 @@ public class TestSupportBinder implements Binder { @Override - public void bindConsumer(String name, MessageChannel inboundBindTarget, Properties properties) { - } - - @Override - public void bindPubSubConsumer(String name, MessageChannel inboundBindTarget, String group, Properties properties) { - + public Binding bindConsumer(String name, String group, MessageChannel inboundBindTarget, Properties properties) { + return null; } /** * Registers a single subscriber to the channel, that enqueues messages for later retrieval and assertion in tests. */ @Override - @SuppressWarnings("unchecked") - public void bindProducer(String name, MessageChannel outboundBindTarget, Properties properties) { - final BlockingQueue queue = messageCollector.register(outboundBindTarget); + public Binding bindProducer(String name, MessageChannel outboundBindTarget, Properties properties) { + final BlockingQueue> queue = messageCollector.register(outboundBindTarget); ((SubscribableChannel)outboundBindTarget).subscribe(new MessageHandler() { @Override public void handleMessage(Message message) throws MessagingException { queue.add(message); } }); - - } - - @Override - public void unbindProducer(String name, MessageChannel channel) { - messageCollector.unregister(channel); - } - - @Override - public void bindPubSubProducer(String name, MessageChannel outboundBindTarget, Properties properties) { - - } - - @Override - public void unbindConsumers(String name) { - - } - - @Override - public void unbindPubSubConsumers(String name, String group) { - - } - - @Override - public void unbindProducers(String name) { - - } - - @Override - public void unbindConsumer(String name, MessageChannel inboundBindTarget) { - - } - - @Override - public void bindRequestor(String name, MessageChannel requests, MessageChannel replies, Properties properties) { - - } - - @Override - public void bindReplier(String name, MessageChannel requests, MessageChannel replies, Properties properties) { - - } - - @Override - public MessageChannel bindDynamicProducer(String name, Properties properties) { return null; } @Override - public MessageChannel bindDynamicPubSubProducer(String name, Properties properties) { - return null; + public void unbind(Binding binding) { + if (Binding.Type.producer.equals(binding.getType())) + messageCollector.unregister(binding.getTarget()); } public MessageCollector messageCollector() { @@ -134,7 +87,7 @@ public class TestSupportBinder implements Binder { private final Map>> results = new HashMap<>(); - private BlockingQueue register(MessageChannel channel) { + private BlockingQueue> register(MessageChannel channel) { LinkedBlockingDeque> result = new LinkedBlockingDeque<>(); Assert.isTrue(!results.containsKey(channel), "Channel [" + channel + "] was already bound"); results.put(channel, result); diff --git a/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/AbstractBindingPropertiesAccessor.java b/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/AbstractBindingPropertiesAccessor.java index c0172bb06..55ad48f6f 100644 --- a/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/AbstractBindingPropertiesAccessor.java +++ b/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/AbstractBindingPropertiesAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-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. @@ -288,15 +288,6 @@ public abstract class AbstractBindingPropertiesAccessor { return getProperty(BinderPropertyKeys.PARTITION_INDEX, -1); } - // Direct Binding - - /** - * If true, the binder can attempt a direct binding. - */ - public boolean isDirectBindingAllowed() { - return getProperty(BinderPropertyKeys.DIRECT_BINDING_ALLOWED, false); - } - // Batching /** diff --git a/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/Binder.java b/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/Binder.java index 48f8de10b..fdd4b66dc 100644 --- a/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/Binder.java +++ b/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/Binder.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2014 the original author or authors. + * Copyright 2013-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. @@ -32,109 +32,27 @@ import java.util.Properties; public interface Binder { /** - * Bind a message consumer on a p2p channel + * Bind a message consumer on a channel * @param name the logical identity of the message source - * @param inboundBindTarget the module interface to be bound as a point to point consumer - * @param properties arbitrary String key/value pairs that will be used in the binding - */ - void bindConsumer(String name, T inboundBindTarget, Properties properties); - - /** - * Bind a message consumer on a pub/sub channel - * @param name the logical identity of the message source - * @param inboundBindTarget the module interface to be bound as a pub/sub consumer * @param group the consumer group to which this consumer belongs - subscriptions are shared among consumers - * in the same group + * in the same group (if null or empty String, the "default" group will be used) + * @param inboundBindTarget the module interface to be bound as a consumer * @param properties arbitrary String key/value pairs that will be used in the binding */ - void bindPubSubConsumer(final String name, T inboundBindTarget, String group, Properties properties); + Binding bindConsumer(String name, String group, T inboundBindTarget, Properties properties); /** - * Bind a message producer on a p2p channel. + * Bind a message producer on a channel. * @param name the logical identity of the message target * @param outboundBindTarget the module interface bound as a producer * @param properties arbitrary String key/value pairs that will be used in the binding */ - void bindProducer(String name, T outboundBindTarget, Properties properties); - + Binding bindProducer(String name, T outboundBindTarget, Properties properties); /** - * Bind a message producer on a pub/sub channel. - * @param name the logical identity of the message target - * @param outboundBindTarget the module interface bound as a producer - * @param properties arbitrary String key/value pairs that will be used in the binding + * Unbind the target component represented by the provided Binding and stop any active components. + * @param binding the Binding instance to unbind */ - void bindPubSubProducer(final String name, T outboundBindTarget, Properties properties); - - /** - * Unbind inbound module components and stop any active components that use the channel. - * @param name the channel name - */ - void unbindConsumers(String name); - - /** - * Unbind inbound module components and stop any active components that use the channel - * with the supplied consumer group. - * @param name the channel name - * @param group the consumer group - */ - void unbindPubSubConsumers(String name, String group); - - /** - * Unbind outbound module components and stop any active components that use the channel. - * @param name the channel name - */ - void unbindProducers(String name); - - /** - * Unbind a specific p2p or pub/sub message consumer - * @param name The logical identify of a message source - * @param inboundBindTarget The module interface bound as a consumer - */ - void unbindConsumer(String name, T inboundBindTarget); - - /** - * Unbind a specific p2p or pub/sub message producer - * @param name the logical identity of the message target - * @param outboundBindTarget the channel bound as a producer - */ - void unbindProducer(String name, T outboundBindTarget); - - /** - * Bind a producer that expects async replies. To unbind, invoke unbindProducer() and unbindConsumer(). - * @param name The name of the requestor. - * @param requests The interface used to send requests. - * @param replies The interface used to receive replies. - * @param properties arbitrary String key/value pairs that will be used in the binding. - */ - void bindRequestor(String name, T requests, T replies, Properties properties); - - /** - * Bind a consumer that handles requests from a requestor and asynchronously sends replies. To unbind, invoke - * unbindProducer() and unbindConsumer(). - * @param name The name of the requestor for which this replier will handle requests. - * @param requests The interface used to send requests. - * @param replies The interface used to receive replies. - * @param properties arbitrary String key/value pairs that will be used in the binding. - */ - void bindReplier(String name, T requests, T replies, Properties properties); - - /** - * Create an object and bind a producer dynamically, creating the infrastructure - * required by the binder technology. - * @param name The name of the "queue:" channel. - * @param properties arbitrary String key/value pairs that will be used in the binding. - * @return The bound object. - */ - T bindDynamicProducer(String name, Properties properties); - - /** - * Create an object and bind a producer dynamically, creating the infrastructure - * required by the binder technology to broadcast messages to consumers. - * @param name The name of the "topic:" channel. - * @param properties arbitrary String key/value pairs that will be used in the binding. - * @return The bound Object. - */ - T bindDynamicPubSubProducer(String name, Properties properties); + void unbind(Binding binding); } diff --git a/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderPropertyKeys.java b/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderPropertyKeys.java index 10fbf1b61..9cb753b98 100644 --- a/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderPropertyKeys.java +++ b/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderPropertyKeys.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-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. @@ -16,7 +16,6 @@ package org.springframework.cloud.stream.binder; - /** * Common binder properties. * @@ -92,11 +91,6 @@ public abstract class BinderPropertyKeys { */ public static final String PARTITION_SELECTOR_EXPRESSION = "partitionSelectorExpression"; - /** - * If true, the binder will attempt to create a direct binding between the producer and consumer. - */ - public static final String DIRECT_BINDING_ALLOWED = "directBindingAllowed"; - /** * True if message batching is enabled. */ diff --git a/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderUtils.java b/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderUtils.java deleted file mode 100644 index 662781120..000000000 --- a/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderUtils.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2015 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; - -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; - -/** - * Binder utilities. - * - * @author Gary Russell - */ -public class BinderUtils { - - /** - * The delimiter between a group and index when constructing a binder consumer/producer. - */ - public static final String GROUP_INDEX_DELIMITER = "."; - - /** - * The prefix for the consumer/producer when creating a topic. - */ - public static final String TOPIC_CHANNEL_PREFIX = "topic:"; - - /** - * Determine whether the provided channel name represents a pub/sub channel (i.e. topic or tap). - * @param channelName name of the channel to check - * @return true if pub/sub. - */ - public static boolean isChannelPubSub(String channelName) { - Assert.isTrue(StringUtils.hasText(channelName), "Channel name should not be empty/null."); - return channelName.startsWith(TOPIC_CHANNEL_PREFIX); - } - - /** - * Construct a name comprised of the group and name. - * @param name the name. - * @param group the group. - * @return the constructed name. - */ - public static String groupedName(String name, String group) { - return group == null ? name : group + BinderUtils.GROUP_INDEX_DELIMITER + name; - } - -} diff --git a/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/Binding.java b/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/Binding.java index aa653f983..f8e9884fa 100644 --- a/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/Binding.java +++ b/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/Binding.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2014 the original author or authors. + * Copyright 2013-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. @@ -18,76 +18,74 @@ package org.springframework.cloud.stream.binder; import org.springframework.context.Lifecycle; import org.springframework.integration.endpoint.AbstractEndpoint; -import org.springframework.messaging.MessageChannel; import org.springframework.util.Assert; /** - * Represents a binding between a module's channel and an adapter endpoint that connects to the Binder. The binding - * could be for a consumer or a producer. A consumer binding represents a connection from an adapter on the binder to a - * module's input channel. A producer binding represents a connection from a module's output channel to an adapter on - * the binder. + * Represents a binding between a channel and an adapter endpoint that connects via a Binder. The binding + * could be for a consumer or a producer. A consumer binding represents a connection from an adapter to an + * input channel. A producer binding represents a connection from an output channel to an adapter. * * @author Jennifer Hickey * @author Mark Fisher * @author Gary Russell */ -public class Binding implements Lifecycle { +public class Binding implements Lifecycle { - public static final String PRODUCER = "producer"; - - public static final String CONSUMER = "consumer"; - - public static final String DIRECT = "direct"; + public static enum Type { + producer, consumer + } private final String name; - private final MessageChannel channel; + private final String group; + + private final T target; private final AbstractEndpoint endpoint; - private final String type; + private final Type type; private final AbstractBindingPropertiesAccessor properties; - private Binding(String name, MessageChannel channel, AbstractEndpoint endpoint, String type, + private Binding(String name, String group, T target, AbstractEndpoint endpoint, Type type, AbstractBindingPropertiesAccessor properties) { - Assert.notNull(channel, "channel must not be null"); + Assert.notNull(target, "target must not be null"); Assert.notNull(endpoint, "endpoint must not be null"); this.name = name; - this.channel = channel; + this.group = group; + this.target = target; this.endpoint = endpoint; this.type = type; this.properties = properties; } - public static Binding forConsumer(String name, AbstractEndpoint adapterFromBinder, MessageChannel moduleInputChannel, + public static Binding forConsumer(String name, String group, AbstractEndpoint adapterFromBinder, T inputTarget, AbstractBindingPropertiesAccessor properties) { - return new Binding(name, moduleInputChannel, adapterFromBinder, CONSUMER, properties); + return new Binding(name, group, inputTarget, adapterFromBinder, Type.consumer, properties); } - public static Binding forProducer(String name, MessageChannel moduleOutputChannel, AbstractEndpoint adapterToBinder, + public static Binding forProducer(String name, T outputTarget, AbstractEndpoint adapterToBinder, AbstractBindingPropertiesAccessor properties) { - return new Binding(name, moduleOutputChannel, adapterToBinder, PRODUCER, properties); - } - - public static Binding forDirectProducer(String name, MessageChannel moduleOutputChannel, - AbstractEndpoint adapter, AbstractBindingPropertiesAccessor properties) { - return new Binding(name, moduleOutputChannel, adapter, DIRECT, properties); + return new Binding(name, null, outputTarget, adapterToBinder, Type.producer, properties); } public String getName() { return name; } - public MessageChannel getChannel() { - return channel; + public String getGroup() { + return group; + } + + public T getTarget() { + return target; } public AbstractEndpoint getEndpoint() { return endpoint; } - public String getType() { + public Type getType() { return type; } @@ -112,8 +110,60 @@ public class Binding implements Lifecycle { @Override public String toString() { - return type + " Binding [name=" + name + ", channel=" + channel + ", endpoint=" + endpoint.getComponentName() + return type + " Binding [name=" + name + ", target=" + target + ", endpoint=" + endpoint.getComponentName() + "]"; } + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((endpoint == null) ? 0 : endpoint.hashCode()); + result = prime * result + ((group == null) ? 0 : group.hashCode()); + result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((properties == null) ? 0 : properties.hashCode()); + result = prime * result + ((target == null) ? 0 : target.hashCode()); + result = prime * result + ((type == null) ? 0 : type.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Binding other = (Binding) obj; + if (endpoint == null) { + if (other.endpoint != null) + return false; + } else if (!endpoint.equals(other.endpoint)) + return false; + if (group == null) { + if (other.group != null) + return false; + } else if (!group.equals(other.group)) + return false; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + if (properties == null) { + if (other.properties != null) + return false; + } else if (!properties.equals(other.properties)) + return false; + if (target == null) { + if (other.target != null) + return false; + } else if (!target.equals(other.target)) + return false; + if (type != other.type) + return false; + return true; + } + } diff --git a/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/MessageChannelBinderSupport.java b/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/MessageChannelBinderSupport.java index 08027e54b..b3b040da2 100644 --- a/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/MessageChannelBinderSupport.java +++ b/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/MessageChannelBinderSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2015 the original author or authors. + * Copyright 2013-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. @@ -29,7 +29,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; -import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -56,14 +55,12 @@ import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; import org.springframework.integration.channel.DirectChannel; import org.springframework.integration.codec.Codec; -import org.springframework.integration.endpoint.EventDrivenConsumer; import org.springframework.integration.expression.ExpressionUtils; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageHandler; import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.MessagingException; -import org.springframework.messaging.SubscribableChannel; import org.springframework.retry.backoff.ExponentialBackOffPolicy; import org.springframework.retry.policy.SimpleRetryPolicy; import org.springframework.retry.support.RetryTemplate; @@ -78,18 +75,23 @@ import org.springframework.util.StringUtils; * @author David Turanski * @author Gary Russell * @author Ilayaperumal Gopinathan + * @author Mark Fisher */ public abstract class MessageChannelBinderSupport implements Binder, ApplicationContextAware, InitializingBean { - protected static final String P2P_NAMED_CHANNEL_TYPE_PREFIX = "queue:"; - - protected static final String PUBSUB_NAMED_CHANNEL_TYPE_PREFIX = "topic:"; - - protected static final String JOB_CHANNEL_TYPE_PREFIX = "job:"; - protected static final String PARTITION_HEADER = "partition"; + /** + * Default group name (used if null or empty String is provided). + */ + protected static final String DEFAULT_CONSUMER_GROUP = "default"; + + /** + * The delimiter between a group and index when constructing a binder consumer/producer. + */ + private static final String GROUP_INDEX_DELIMITER = "."; + protected final Logger logger = LoggerFactory.getLogger(getClass()); private volatile AbstractApplicationContext applicationContext; @@ -98,8 +100,6 @@ public abstract class MessageChannelBinderSupport private final StringConvertingContentTypeResolver contentTypeResolver = new StringConvertingContentTypeResolver(); - private final ThreadLocal revertingDirectBinding = new ThreadLocal(); - protected static final List MEDIATYPES_MEDIATYPE_ALL = Collections.singletonList(ALL); private static final int DEFAULT_BACKOFF_INITIAL_INTERVAL = 1000; @@ -161,7 +161,7 @@ public abstract class MessageChannelBinderSupport BinderPropertyKeys.BATCH_BUFFER_LIMIT, })); - private final List bindings = Collections.synchronizedList(new ArrayList()); + private final List> bindings = Collections.synchronizedList(new ArrayList>()); private final IdGenerator idGenerator = new AlternativeJdkIdGenerator(); @@ -206,7 +206,7 @@ public abstract class MessageChannelBinderSupport protected volatile boolean defaultCompress = false; - protected volatile boolean defaultDurableSubscription = false; + protected volatile boolean defaultDurableSubscription = true; // Payload type cache private volatile Map> payloadTypeCache = new ConcurrentHashMap<>(); @@ -220,23 +220,6 @@ public abstract class MessageChannelBinderSupport return prefix + name; } - /** - * For binder implementations that include a pub/sub component in identifiers, construct the name. - * @param name the name. - */ - public static String applyPubSub(String name) { - return "topic." + name; - } - - /** - * Build the requests entity name. - * @param name the name. - * @return the request entity name. - */ - public static String applyRequests(String name) { - return name + ".requests"; - } - /** * For binder implementations that support dead lettering, construct the name of the dead letter entity for the * underlying pipe name. @@ -383,17 +366,14 @@ public abstract class MessageChannelBinderSupport } } - /** - * Dynamically create a producer for the named channel. - * @param name The name. - * @param properties The properties. - * @return The channel. - */ @Override - public MessageChannel bindDynamicProducer(String name, Properties properties) { - return doBindDynamicProducer(name, name, properties); + public final Binding bindConsumer(String name, String group, MessageChannel inputChannel, Properties properties) { + group = (StringUtils.hasText(group)) ? group : DEFAULT_CONSUMER_GROUP; + return doBindConsumer(name, group, inputChannel, properties); } + protected abstract Binding doBindConsumer(String name, String group, MessageChannel inputChannel, Properties properties); + /** * Create a producer for the named channel and bind it to the binder. Synchronized to avoid creating multiple * instances. @@ -419,44 +399,6 @@ public abstract class MessageChannelBinderSupport return channel; } - /** - * Dynamically create a producer for the named channel. Note: even though it's pub/sub, we still use a direct - * channel. It will be bridged to a pub/sub channel in the local binder and bound to an appropriate element for other - * binders. - * @param name The name. - * @param properties The properties. - * @return The channel. - */ - @Override - public MessageChannel bindDynamicPubSubProducer(String name, Properties properties) { - return doBindDynamicPubSubProducer(name, name, properties); - } - - /** - * Create a producer for the named channel and bind it to the binder. Synchronized to avoid creating multiple - * instances. - * @param name The name. - * @param channelName The name of the channel to be created, and registered as bean. - * @param properties The properties. - * @return The channel. - */ - protected synchronized MessageChannel doBindDynamicPubSubProducer(String name, String channelName, - Properties properties) { - MessageChannel channel = this.directChannelProvider.lookupSharedChannel(channelName); - if (channel == null) { - try { - channel = this.directChannelProvider.createAndRegisterChannel(channelName); - bindPubSubProducer(name, channel, properties); - } - catch (RuntimeException e) { - destroyCreatedChannel(channelName, channel); - throw new BinderException( - "Failed to bind dynamic channel '" + name + "' with properties " + properties, e); - } - } - return channel; - } - private void destroyCreatedChannel(String name, MessageChannel channel) { BeanFactory beanFactory = this.applicationContext.getBeanFactory(); if (beanFactory.containsBean(name)) { @@ -467,82 +409,17 @@ public abstract class MessageChannelBinderSupport } @Override - public void unbindConsumers(String name) { - deleteBindings("inbound." + name); - } - - @Override - public void unbindPubSubConsumers(String name, String group) { - unbindConsumers(BinderUtils.groupedName(name, group)); - } - - @Override - public void unbindProducers(String name) { - deleteBindings("outbound." + name); - } - - @Override - public void unbindConsumer(String name, MessageChannel channel) { - deleteBinding("inbound." + name, channel); - } - - @Override - public void unbindProducer(String name, MessageChannel channel) { - deleteBinding("outbound." + name, channel); - } - - protected void addBinding(Binding binding) { - this.bindings.add(binding); - } - - protected void deleteBindings(String name) { - Assert.hasText(name, "a valid name is required to remove bindings"); - List bindingsToRemove = new ArrayList(); - synchronized (this.bindings) { - Iterator iterator = this.bindings.iterator(); - while (iterator.hasNext()) { - Binding binding = iterator.next(); - if (binding.getEndpoint().getComponentName().equals(name)) { - bindingsToRemove.add(binding); - } - } - for (Binding binding : bindingsToRemove) { - doDeleteBinding(binding); - } - } - } - - protected void deleteBinding(String name, MessageChannel channel) { - Assert.hasText(name, "a valid name is required to remove a binding"); - Assert.notNull(channel, "a valid channel is required to remove a binding"); - Binding bindingToRemove = null; - synchronized (this.bindings) { - Iterator iterator = this.bindings.iterator(); - while (iterator.hasNext()) { - Binding binding = iterator.next(); - if (binding.getChannel().equals(channel) && - binding.getEndpoint().getComponentName().equals(name)) { - bindingToRemove = binding; - break; - } - } - if (bindingToRemove != null) { - doDeleteBinding(bindingToRemove); - } - } - - } - - private void doDeleteBinding(Binding binding) { - if (Binding.CONSUMER.equals(binding.getType())) { - /* - * Revert the direct binding before stopping the consumer; the module - * outputChannel will temporarily have 2 subscribers. - */ - revertDirectBindingIfNecessary(binding); - } + public void unbind(Binding binding) { binding.stop(); this.bindings.remove(binding); + afterUnbind(binding); + } + + protected void afterUnbind(Binding binding) { + } + + protected void addBinding(Binding binding) { + this.bindings.add(binding); } protected void stopBindings() { @@ -558,6 +435,19 @@ public abstract class MessageChannelBinderSupport } } + /** + * Construct a name comprised of the name and group. + * @param name the name. + * @param group the group. + * @return the constructed name. + */ + protected final String groupedName(String name, String group) { + if (!StringUtils.hasText(group)) { + group = "default"; + } + return name + GROUP_INDEX_DELIMITER + group; + } + protected final MessageValues serializePayloadIfNecessary(Message message) { Object originalPayload = message.getPayload(); Object originalContentType = message.getHeaders().get(MessageHeaders.CONTENT_TYPE); @@ -821,125 +711,6 @@ public abstract class MessageChannelBinderSupport } } - protected boolean isNamedChannel(String name) { - return name.startsWith(PUBSUB_NAMED_CHANNEL_TYPE_PREFIX) || name.startsWith(P2P_NAMED_CHANNEL_TYPE_PREFIX) - || name.startsWith(JOB_CHANNEL_TYPE_PREFIX); - } - - /** - * Attempt to create a direct binding (avoiding the broker) if the consumer is local. Named channel producers are not - * bound directly. - * @param name The name. - * @param moduleOutputChannel The channel to bind. - * @param properties The producer properties. - * @return true if the producer is bound. - */ - protected boolean bindNewProducerDirectlyIfPossible(String name, SubscribableChannel moduleOutputChannel, - AbstractBindingPropertiesAccessor properties) { - if (!properties.isDirectBindingAllowed()) { - return false; - } - else if (isNamedChannel(name)) { - return false; - } - else if (this.revertingDirectBinding.get() != null) { - // we're in the process of unbinding a direct binding - this.revertingDirectBinding.remove(); - return false; - } - else { - Binding consumerBinding = null; - synchronized (this.bindings) { - for (Binding binding : this.bindings) { - if (binding.getName().equals(name) && Binding.CONSUMER.equals(binding.getType())) { - consumerBinding = binding; - break; - } - } - } - if (consumerBinding == null) { - return false; - } - else { - bindProducerDirectly(name, moduleOutputChannel, consumerBinding.getChannel(), properties); - return true; - } - } - } - - private void bindProducerDirectly(String name, SubscribableChannel producerChannel, - MessageChannel consumerChannel, AbstractBindingPropertiesAccessor properties) { - DirectHandler handler = new DirectHandler(consumerChannel); - EventDrivenConsumer consumer = new EventDrivenConsumer(producerChannel, handler); - consumer.setBeanFactory(getBeanFactory()); - consumer.setBeanName("outbound." + name); - consumer.afterPropertiesSet(); - Binding binding = Binding.forDirectProducer(name, producerChannel, consumer, properties); - addBinding(binding); - binding.start(); - if (this.logger.isInfoEnabled()) { - this.logger.info("Producer bound directly: " + binding); - } - } - - /** - * Attempt to bind a producer directly (avoiding the broker) if there is already a local producer. PubSub producers - * cannot be bound directly. Create the direct binding, then unbind the existing producer. - * @param name The name. - * @param consumerChannel The channel to bind the producer to. - */ - protected void bindExistingProducerDirectlyIfPossible(String name, MessageChannel consumerChannel) { - if (!isNamedChannel(name)) { - Binding producerBinding = null; - synchronized (this.bindings) { - for (Binding binding : this.bindings) { - if (binding.getName().equals(name) && Binding.PRODUCER.equals(binding.getType())) { - producerBinding = binding; - break; - } - } - if (producerBinding != null && producerBinding.getChannel() instanceof SubscribableChannel) { - AbstractBindingPropertiesAccessor properties = producerBinding.getPropertiesAccessor(); - if (properties.isDirectBindingAllowed()) { - bindProducerDirectly(name, (SubscribableChannel) producerBinding.getChannel(), consumerChannel, - properties); - producerBinding.stop(); - this.bindings.remove(producerBinding); - } - } - } - } - } - - private void revertDirectBindingIfNecessary(Binding binding) { - try { - synchronized (this.bindings) { // Not necessary, called while synchronized, but just in case... - Binding directBinding = null; - Iterator iterator = this.bindings.iterator(); - while (iterator.hasNext()) { - Binding producer = iterator.next(); - if (Binding.DIRECT.equals(producer.getType()) && binding.getName().equals(producer.getName())) { - this.revertingDirectBinding.set(Boolean.TRUE); - bindProducer(producer.getName(), producer.getChannel(), - producer.getPropertiesAccessor().getProperties()); - directBinding = producer; - break; - } - } - if (directBinding != null) { - directBinding.stop(); - this.bindings.remove(directBinding); - if (this.logger.isInfoEnabled()) { - this.logger.info("direct binding reverted: " + directBinding); - } - } - } - } - catch (Exception e) { - this.logger.error("Could not revert direct binding: " + binding, e); - } - } - /** * Default partition strategy; only works on keys with "real" hash codes, such as String. Caller now always applies * modulo so no need to do so here. diff --git a/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/BinderAwareChannelResolver.java b/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/BinderAwareChannelResolver.java index 02c5dfba0..26bc24c15 100644 --- a/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/BinderAwareChannelResolver.java +++ b/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/BinderAwareChannelResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2014 the original author or authors. + * Copyright 2013-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. @@ -18,15 +18,21 @@ package org.springframework.cloud.stream.binding; import java.util.Properties; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.cloud.stream.binder.Binder; import org.springframework.cloud.stream.binder.BinderFactory; +import org.springframework.integration.channel.DirectChannel; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.core.BeanFactoryMessageChannelDestinationResolver; import org.springframework.messaging.core.DestinationResolutionException; /** - * A {@link org.springframework.messaging.core.DestinationResolver} implementation that first checks for any channel - * whose name begins with a colon in the {@link Binder}. + * A {@link org.springframework.messaging.core.DestinationResolver} implementation that + * resolves the channel from the bean factory and, if not present, creates a new channel + * and adds it to the factory after binding it to the binder. The binder is optionally + * determined with a prefix preceding a colon. * @author Mark Fisher * @author Gary Russell */ @@ -36,11 +42,21 @@ public class BinderAwareChannelResolver extends BeanFactoryMessageChannelDestina private final Properties producerProperties; + private DefaultListableBeanFactory beanFactory; + public BinderAwareChannelResolver(BinderFactory binderFactory, Properties producerProperties) { this.binderFactory = binderFactory; this.producerProperties = producerProperties; } + @Override + public void setBeanFactory(BeanFactory beanFactory) { + super.setBeanFactory(beanFactory); + if (beanFactory instanceof ConfigurableBeanFactory) { + this.beanFactory = (DefaultListableBeanFactory) beanFactory; + } + } + @Override public MessageChannel resolveDestination(String name) { MessageChannel channel = null; @@ -49,38 +65,32 @@ public class BinderAwareChannelResolver extends BeanFactoryMessageChannelDestina } catch (DestinationResolutionException e) { } - if (name.contains(":")) { - if (binderFactory != null) { - String[] tokens = name.split(":", 2); + synchronized (this) { + try { + return super.resolveDestination(name); + } + catch (DestinationResolutionException e) { + } + if (this.beanFactory != null && this.binderFactory != null) { + channel = new DirectChannel(); + this.beanFactory.registerSingleton(name, channel); + channel = (MessageChannel) this.beanFactory.initializeBean(channel, name); String transport = null; - String type; - if (tokens.length == 2) { - type = tokens[0]; - } - else if (tokens.length == 3) { - transport = tokens[0]; - type = tokens[1]; - } - else { - throw new IllegalArgumentException("Unrecognized channel naming scheme: " + name + " , should be" + - " [:]:"); + if (name.contains(":")) { + String[] tokens = name.split(":", 2); + if (tokens.length == 2) { + transport = tokens[0]; + } + else if (tokens.length != 1) { + throw new IllegalArgumentException("Unrecognized channel naming scheme: " + name + " , should be" + + " [:]"); + } } Binder binder = binderFactory.getBinder(transport); - if ("queue".equals(type)) { - channel = binder.bindDynamicProducer(name, this.producerProperties); - } - else if ("topic".equals(type)) { - channel = binder.bindDynamicPubSubProducer(name, this.producerProperties); - } - else { - throw new IllegalArgumentException("unrecognized channel type: " + type); - } + binder.bindProducer(name, channel, this.producerProperties); } + return channel; } - if (channel == null) { - channel = super.resolveDestination(name); - } - return channel; } } diff --git a/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/ChannelBindingService.java b/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/ChannelBindingService.java index 0192df052..bb24a6a57 100644 --- a/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/ChannelBindingService.java +++ b/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/ChannelBindingService.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-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. @@ -16,14 +16,15 @@ package org.springframework.cloud.stream.binding; +import java.util.HashMap; +import java.util.Map; + import org.springframework.cloud.stream.binder.Binder; import org.springframework.cloud.stream.binder.BinderFactory; -import org.springframework.cloud.stream.binder.BinderUtils; +import org.springframework.cloud.stream.binder.Binding; import org.springframework.cloud.stream.config.BindingProperties; import org.springframework.cloud.stream.config.ChannelBindingServiceProperties; import org.springframework.messaging.MessageChannel; -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; /** * Handles the operations related to channel binding including binding of input/output channels by delegating @@ -41,57 +42,42 @@ public class ChannelBindingService { private final ChannelBindingServiceProperties channelBindingServiceProperties; + private final Map> producerBindings = new HashMap<>(); + + private final Map> consumerBindings = new HashMap<>(); + public ChannelBindingService(ChannelBindingServiceProperties channelBindingServiceProperties, BinderFactory binderFactory) { this.channelBindingServiceProperties = channelBindingServiceProperties; this.binderFactory = binderFactory; } - public void bindConsumer(MessageChannel inputChannel, String inputChannelName) { + public Binding bindConsumer(MessageChannel inputChannel, String inputChannelName) { String channelBindingTarget = this.channelBindingServiceProperties.getBindingDestination(inputChannelName); Binder binder = getBinderForChannel(inputChannelName); - if (BinderUtils.isChannelPubSub(channelBindingTarget)) { - binder.bindPubSubConsumer(removePrefix(channelBindingTarget), - inputChannel, consumerGroup(inputChannelName), - this.channelBindingServiceProperties.getConsumerProperties(inputChannelName)); - } - else { - binder.bindConsumer(channelBindingTarget, inputChannel, - this.channelBindingServiceProperties.getConsumerProperties(inputChannelName)); - } + Binding binding = binder.bindConsumer(channelBindingTarget, consumerGroup(inputChannelName), inputChannel, + this.channelBindingServiceProperties.getConsumerProperties(inputChannelName)); + this.consumerBindings.put(inputChannelName, binding); + return binding; } - public void bindProducer(MessageChannel outputChannel, String outputChannelName) { + public Binding bindProducer(MessageChannel outputChannel, String outputChannelName) { String channelBindingTarget = this.channelBindingServiceProperties.getBindingDestination(outputChannelName); Binder binder = getBinderForChannel(outputChannelName); - if (BinderUtils.isChannelPubSub(channelBindingTarget)) { - binder.bindPubSubProducer(removePrefix(channelBindingTarget), - outputChannel, this.channelBindingServiceProperties.getProducerProperties(outputChannelName)); - } - else { - binder.bindProducer(channelBindingTarget, outputChannel, - this.channelBindingServiceProperties.getProducerProperties(outputChannelName)); - } - } - - private String removePrefix(String bindingTarget) { - Assert.isTrue(StringUtils.hasText(bindingTarget), "Binding target should not be empty/null."); - return bindingTarget.substring(bindingTarget.indexOf(":") + 1); + Binding binding = binder.bindProducer(channelBindingTarget, outputChannel, + this.channelBindingServiceProperties.getProducerProperties(outputChannelName)); + this.producerBindings.put(outputChannelName, binding); + return binding; } public void unbindConsumers(String inputChannelName) { Binder binder = getBinderForChannel(inputChannelName); - if (BinderUtils.isChannelPubSub(this.channelBindingServiceProperties.getBindingDestination(inputChannelName))) { - binder.unbindPubSubConsumers(inputChannelName, consumerGroup(inputChannelName)); - } - else { - binder.unbindConsumers(inputChannelName); - } + binder.unbind(this.consumerBindings.remove(inputChannelName)); } public void unbindProducers(String outputChannelName) { Binder binder = getBinderForChannel(outputChannelName); - binder.unbindProducers(outputChannelName); + binder.unbind(this.producerBindings.remove(outputChannelName)); } private Binder getBinderForChannel(String channelName) { diff --git a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/ArbitraryInterfaceBindingTestsWithBindingTargets.java b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/ArbitraryInterfaceBindingTestsWithBindingTargets.java index 906d541cb..ad00daa50 100644 --- a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/ArbitraryInterfaceBindingTestsWithBindingTargets.java +++ b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/ArbitraryInterfaceBindingTestsWithBindingTargets.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-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. @@ -16,6 +16,7 @@ package org.springframework.cloud.stream.binder; +import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -54,8 +55,8 @@ public class ArbitraryInterfaceBindingTestsWithBindingTargets { @SuppressWarnings("unchecked") @Test public void testArbitraryInterfaceChannelsBound() { - verify(binder).bindConsumer(eq("someQueue.0"), eq(fooChannels.foo()), Mockito.any()); - verify(binder).bindConsumer(eq("someQueue.1"), eq(fooChannels.bar()), Mockito.any()); + verify(binder).bindConsumer(eq("someQueue.0"), anyString(), eq(fooChannels.foo()), Mockito.any()); + verify(binder).bindConsumer(eq("someQueue.1"), anyString(), eq(fooChannels.bar()), Mockito.any()); verify(binder).bindProducer(eq("someQueue.2"), eq(fooChannels.baz()), Mockito.any()); verify(binder).bindProducer(eq("someQueue.3"), eq(fooChannels.qux()), Mockito.any()); verifyNoMoreInteractions(binder); diff --git a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/ArbitraryInterfaceBindingTestsWithDefaults.java b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/ArbitraryInterfaceBindingTestsWithDefaults.java index 120cc3112..874f2bb3f 100644 --- a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/ArbitraryInterfaceBindingTestsWithDefaults.java +++ b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/ArbitraryInterfaceBindingTestsWithDefaults.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-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. @@ -16,6 +16,7 @@ package org.springframework.cloud.stream.binder; +import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -53,8 +54,8 @@ public class ArbitraryInterfaceBindingTestsWithDefaults { @SuppressWarnings("unchecked") @Test public void testArbitraryInterfaceChannelsBound() { - verify(binder).bindConsumer(eq("foo"), eq(fooChannels.foo()), Mockito.any()); - verify(binder).bindConsumer(eq("bar"), eq(fooChannels.bar()), Mockito.any()); + verify(binder).bindConsumer(eq("foo"), anyString(), eq(fooChannels.foo()), Mockito.any()); + verify(binder).bindConsumer(eq("bar"), anyString(), eq(fooChannels.bar()), Mockito.any()); verify(binder).bindProducer(eq("baz"), eq(fooChannels.baz()), Mockito.any()); verify(binder).bindProducer(eq("qux"), eq(fooChannels.qux()), Mockito.any()); verifyNoMoreInteractions(binder); diff --git a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/BinderAwareChannelResolverTests.java b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/BinderAwareChannelResolverTests.java index 83963a164..d261fb46b 100644 --- a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/BinderAwareChannelResolverTests.java +++ b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/BinderAwareChannelResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2014 the original author or authors. + * Copyright 2013-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. @@ -20,8 +20,9 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.doReturn; +import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -41,7 +42,6 @@ import org.springframework.cloud.stream.binder.local.LocalMessageChannelBinder; import org.springframework.cloud.stream.binding.BinderAwareChannelResolver; import org.springframework.context.support.StaticApplicationContext; import org.springframework.integration.channel.DirectChannel; -import org.springframework.integration.channel.PublishSubscribeChannel; import org.springframework.integration.scheduling.PollerMetadata; import org.springframework.integration.support.DefaultMessageBuilderFactory; import org.springframework.integration.support.MessageBuilder; @@ -76,7 +76,7 @@ public class BinderAwareChannelResolverTests { return binder; } }, null); - this.resolver.setBeanFactory(context); + this.resolver.setBeanFactory(context.getBeanFactory()); context.getBeanFactory().registerSingleton("channelResolver", this.resolver); context.registerSingleton("other", DirectChannel.class); @@ -91,8 +91,8 @@ public class BinderAwareChannelResolverTests { } @Test - public void resolveQueueChannel() { - MessageChannel registered = resolver.resolveDestination("queue:foo"); + public void resolveChannel() { + MessageChannel registered = resolver.resolveDestination("foo"); DirectChannel testChannel = new DirectChannel(); final CountDownLatch latch = new CountDownLatch(1); final List> received = new ArrayList>(); @@ -104,7 +104,7 @@ public class BinderAwareChannelResolverTests { latch.countDown(); } }); - binder.bindConsumer("queue:foo", testChannel, null); + binder.bindConsumer("foo", null, testChannel, null); assertEquals(0, received.size()); registered.send(MessageBuilder.withPayload("hello").build()); try { @@ -119,41 +119,6 @@ public class BinderAwareChannelResolverTests { context.close(); } - @Test - public void resolveTopicChannel() { - MessageChannel registered = resolver.resolveDestination("topic:bar"); - PublishSubscribeChannel[] testChannels = { - new PublishSubscribeChannel(), new PublishSubscribeChannel(), new PublishSubscribeChannel() - }; - final CountDownLatch latch = new CountDownLatch(testChannels.length); - final List> received = new ArrayList>(); - for (PublishSubscribeChannel testChannel : testChannels) { - testChannel.subscribe(new MessageHandler() { - - @Override - public void handleMessage(Message message) throws MessagingException { - received.add(message); - latch.countDown(); - } - }); - binder.bindPubSubConsumer("topic:bar", testChannel, null, null); - } - assertEquals(0, received.size()); - registered.send(MessageBuilder.withPayload("hello").build()); - try { - assertTrue("latch timed out", latch.await(1, TimeUnit.SECONDS)); - } - catch (InterruptedException e) { - Thread.currentThread().interrupt(); - fail("interrupted while awaiting latch"); - } - assertEquals(3, received.size()); - assertEquals("hello", received.get(0).getPayload()); - assertEquals("hello", received.get(1).getPayload()); - assertEquals("hello", received.get(2).getPayload()); - context.close(); - } - @Test public void resolveNonRegisteredChannel() { MessageChannel other = resolver.resolveDestination("other"); @@ -161,12 +126,11 @@ public class BinderAwareChannelResolverTests { } @Test + @SuppressWarnings("rawtypes") public void propertyPassthrough() { Properties properties = new Properties(); - @SuppressWarnings("rawtypes") - Binder binderFactory = mock(Binder.class); - doReturn(new DirectChannel()).when(binderFactory).bindDynamicProducer("queue:foo", properties); - doReturn(new DirectChannel()).when(binderFactory).bindDynamicPubSubProducer("topic:bar", properties); + @SuppressWarnings("unchecked") + Binder binderFactory = mock(Binder.class); BinderFactory mockBinderFactory = Mockito.mock(BinderFactory.class); Mockito.when(mockBinderFactory.getBinder(anyString())).thenReturn(binderFactory); @SuppressWarnings("unchecked") @@ -174,10 +138,12 @@ public class BinderAwareChannelResolverTests { new BinderAwareChannelResolver(mockBinderFactory, properties); BeanFactory beanFactory = new DefaultListableBeanFactory(); resolver.setBeanFactory(beanFactory); - resolver.resolveDestination("queue:foo"); - resolver.resolveDestination("topic:bar"); - verify(binderFactory).bindDynamicProducer("queue:foo", properties); - verify(binderFactory).bindDynamicPubSubProducer("topic:bar", properties); + MessageChannel resolved = resolver.resolveDestination("foo"); + verify(binderFactory).bindProducer(eq("foo"), any(MessageChannel.class), eq(properties)); + assertSame(resolved, beanFactory.getBean("foo")); + resolved = resolver.resolveDestination("someTransport:foo"); + verify(binderFactory).bindProducer(eq("someTransport:foo"), any(MessageChannel.class), eq(properties)); + assertSame(resolved, beanFactory.getBean("someTransport:foo")); } } diff --git a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/InputOutputBindingOrderTest.java b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/InputOutputBindingOrderTest.java index 69a7135b8..b735b08ff 100644 --- a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/InputOutputBindingOrderTest.java +++ b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/InputOutputBindingOrderTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-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. @@ -18,6 +18,7 @@ package org.springframework.cloud.stream.binder; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -51,7 +52,7 @@ public class InputOutputBindingOrderTest { Binder binder = applicationContext.getBean(BinderFactory.class).getBinder(null); Processor processor = applicationContext.getBean(Processor.class); // input is bound after the context has been started - verify(binder).bindConsumer(eq("input"), eq(processor.input()), Mockito.any()); + verify(binder).bindConsumer(eq("input"), anyString(), eq(processor.input()), Mockito.any()); SomeLifecycle someLifecycle = applicationContext.getBean(SomeLifecycle.class); assertTrue(someLifecycle.isRunning()); applicationContext.close(); @@ -81,6 +82,7 @@ public class InputOutputBindingOrderTest { private Processor processor; @Override + @SuppressWarnings("unchecked") public synchronized void start() { verify(this.binder).bindProducer(eq("output"), eq(this.processor.output()), Mockito.any()); // input was not bound yet diff --git a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/ProcessorBindingTestsWithBindingTargets.java b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/ProcessorBindingTestsWithBindingTargets.java index 33320c063..07d18fbb5 100644 --- a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/ProcessorBindingTestsWithBindingTargets.java +++ b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/ProcessorBindingTestsWithBindingTargets.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-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. @@ -16,6 +16,7 @@ package org.springframework.cloud.stream.binder; +import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.verify; @@ -53,7 +54,7 @@ public class ProcessorBindingTestsWithBindingTargets { @SuppressWarnings("unchecked") @Test public void testSourceOutputChannelBound() { - verify(binder).bindConsumer(eq("testtock.0"), eq(testProcessor.input()), Mockito.any()); + verify(binder).bindConsumer(eq("testtock.0"), anyString(), eq(testProcessor.input()), Mockito.any()); verify(binder).bindProducer(eq("testtock.1"), eq(testProcessor.output()), Mockito.any()); } diff --git a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/ProcessorBindingTestsWithDefaults.java b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/ProcessorBindingTestsWithDefaults.java index c86227daa..d1b21f746 100644 --- a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/ProcessorBindingTestsWithDefaults.java +++ b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/ProcessorBindingTestsWithDefaults.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-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. @@ -16,6 +16,7 @@ package org.springframework.cloud.stream.binder; +import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -52,7 +53,7 @@ public class ProcessorBindingTestsWithDefaults { @SuppressWarnings("unchecked") @Test public void testSourceOutputChannelBound() { - Mockito.verify(binder).bindConsumer(eq("input"), eq(processor.input()), Mockito.any()); + Mockito.verify(binder).bindConsumer(eq("input"), anyString(), eq(processor.input()), Mockito.any()); Mockito.verify(binder).bindProducer(eq("output"), eq(processor.output()), Mockito.any()); verifyNoMoreInteractions(binder); } diff --git a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/ProcessorBindingTestsWithPubSubBindingTargets.java b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/ProcessorBindingTestsWithPubSubBindingTargets.java deleted file mode 100644 index df69433f5..000000000 --- a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/ProcessorBindingTestsWithPubSubBindingTargets.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2015 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; - -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyString; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; - -import java.util.Properties; - -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.messaging.Processor; -import org.springframework.cloud.stream.utils.MockBinderRegistryConfiguration; -import org.springframework.context.annotation.Import; -import org.springframework.context.annotation.PropertySource; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; - -/** - * @author Marius Bogoevici - */ -@RunWith(SpringJUnit4ClassRunner.class) -@SpringApplicationConfiguration(ProcessorBindingTestsWithPubSubBindingTargets.TestProcessor.class) -public class ProcessorBindingTestsWithPubSubBindingTargets { - - @SuppressWarnings("rawtypes") - @Autowired - private Binder binder; - - @Autowired @Bindings(TestProcessor.class) - private Processor testProcessor; - - @SuppressWarnings("unchecked") - @Test - public void testSourceOutputChannelBound() { - verify(binder).bindPubSubConsumer(eq("testtock.0"), eq(testProcessor.input()), anyString(), - any(Properties.class)); - verify(binder).bindPubSubProducer(eq("testtock.1"), eq(testProcessor.output()), any(Properties.class)); - verifyNoMoreInteractions(binder); - } - - @EnableBinding(Processor.class) - @EnableAutoConfiguration - @Import(MockBinderRegistryConfiguration.class) - @PropertySource("classpath:/org/springframework/cloud/stream/binder/processor-binding-test-pubsub.properties") - public static class TestProcessor { - - } -} diff --git a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/SinkBindingPubSubTests.java b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/SinkBindingPubSubTests.java deleted file mode 100644 index a81261cf2..000000000 --- a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/SinkBindingPubSubTests.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2015 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; - -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; - -import java.util.Properties; - -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.messaging.Sink; -import org.springframework.cloud.stream.utils.MockBinderRegistryConfiguration; -import org.springframework.context.annotation.Import; -import org.springframework.context.annotation.PropertySource; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; - -/** - * @author Marius Bogoevici - * @author Gary Russell - */ -@RunWith(SpringJUnit4ClassRunner.class) -@SpringApplicationConfiguration(SinkBindingPubSubTests.TestSink.class) -public class SinkBindingPubSubTests { - - @SuppressWarnings("rawtypes") - @Autowired - private Binder binder; - - @Autowired @Bindings(TestSink.class) - private Sink testSink; - - @SuppressWarnings("unchecked") - @Test - public void testSourceOutputChannelBound() { - verify(binder).bindPubSubConsumer(eq("testpubsub"), eq(testSink.input()), eq("tgroup"), any(Properties.class)); - verifyNoMoreInteractions(binder); - } - - @EnableBinding(Sink.class) - @EnableAutoConfiguration - @Import(MockBinderRegistryConfiguration.class) - @PropertySource("classpath:/org/springframework/cloud/stream/binder/sink-binding-pubsub-test.properties") - public static class TestSink { - - } - -} diff --git a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/SinkBindingTestsWithBindingTargets.java b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/SinkBindingTestsWithBindingTargets.java index ef6f0f182..5e59a0639 100644 --- a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/SinkBindingTestsWithBindingTargets.java +++ b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/SinkBindingTestsWithBindingTargets.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-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. @@ -16,6 +16,7 @@ package org.springframework.cloud.stream.binder; +import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -54,7 +55,7 @@ public class SinkBindingTestsWithBindingTargets { @SuppressWarnings("unchecked") @Test public void testSourceOutputChannelBound() { - verify(binder).bindConsumer(eq("testtock"), eq(testSink.input()), Mockito.any()); + verify(binder).bindConsumer(eq("testtock"), anyString(), eq(testSink.input()), Mockito.any()); verifyNoMoreInteractions(binder); } diff --git a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/SinkBindingTestsWithDefaults.java b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/SinkBindingTestsWithDefaults.java index d54c53ba5..285f795b5 100644 --- a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/SinkBindingTestsWithDefaults.java +++ b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/SinkBindingTestsWithDefaults.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-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. @@ -16,6 +16,7 @@ package org.springframework.cloud.stream.binder; +import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -53,7 +54,7 @@ public class SinkBindingTestsWithDefaults { @SuppressWarnings("unchecked") @Test public void testSourceOutputChannelBound() { - verify(binder).bindConsumer(eq("input"), eq(testSink.input()), Mockito.any()); + verify(binder).bindConsumer(eq("input"), anyString(), eq(testSink.input()), Mockito.any()); verifyNoMoreInteractions(binder); } diff --git a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/local/LocalMessageChannelBinder.java b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/local/LocalMessageChannelBinder.java index 83de44c85..4001d515e 100644 --- a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/local/LocalMessageChannelBinder.java +++ b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/local/LocalMessageChannelBinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2015 the original author or authors. + * Copyright 2013-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. @@ -17,18 +17,12 @@ package org.springframework.cloud.stream.binder.local; import java.util.Collection; -import java.util.HashMap; -import java.util.Map; import java.util.Properties; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; import org.springframework.cloud.stream.binder.AbstractBindingPropertiesAccessor; -import org.springframework.cloud.stream.binder.BinderPropertyKeys; import org.springframework.cloud.stream.binder.Binding; import org.springframework.cloud.stream.binder.MessageChannelBinderSupport; import org.springframework.integration.channel.DirectChannel; -import org.springframework.integration.channel.ExecutorChannel; import org.springframework.integration.channel.PublishSubscribeChannel; import org.springframework.integration.channel.QueueChannel; import org.springframework.integration.config.ConsumerEndpointFactoryBean; @@ -37,10 +31,7 @@ import org.springframework.integration.scheduling.PollerMetadata; import org.springframework.integration.support.context.NamedComponent; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.MessageHandler; -import org.springframework.messaging.MessagingException; import org.springframework.messaging.PollableChannel; -import org.springframework.messaging.SubscribableChannel; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.util.Assert; import org.springframework.util.MimeType; @@ -60,6 +51,8 @@ import org.springframework.util.MimeType; */ public class LocalMessageChannelBinder extends MessageChannelBinderSupport { + public static final String THREAD_NAME_PREFIX = "binder.local-"; + private static final int DEFAULT_EXECUTOR_CORE_POOL_SIZE = 0; private static final int DEFAULT_EXECUTOR_MAX_POOL_SIZE = 200; @@ -68,19 +61,8 @@ public class LocalMessageChannelBinder extends MessageChannelBinderSupport { private static final int DEFAULT_EXECUTOR_KEEPALIVE_SECONDS = 60; - private static final int DEFAULT_REQ_REPLY_CONCURRENCY = 1; - - protected static final Set CONSUMER_REQUEST_REPLY_PROPERTIES = new SetBuilder() - .addAll(CONSUMER_STANDARD_PROPERTIES) - .add(BinderPropertyKeys.CONCURRENCY) - .build(); - - public static final String THREAD_NAME_PREFIX = "binder.local-"; - private volatile PollerMetadata poller; - private final Map requestReplyChannels = new HashMap(); - private final ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); private volatile int executorCorePoolSize = DEFAULT_EXECUTOR_CORE_POOL_SIZE; @@ -91,23 +73,6 @@ public class LocalMessageChannelBinder extends MessageChannelBinderSupport { private volatile int executorKeepAliveSeconds = DEFAULT_EXECUTOR_KEEPALIVE_SECONDS; - private volatile int queueSize = Integer.MAX_VALUE; - - private final Map reqRepExecutors = new ConcurrentHashMap<>(); - - /** - * Used to create and customize {@link QueueChannel}s when the binding operation involves aliased names. - */ - private final SharedChannelProvider queueChannelProvider = new SharedChannelProvider( - QueueChannel.class) { - - @Override - protected QueueChannel createSharedChannel(String name) { - QueueChannel queueChannel = new QueueChannel(queueSize); - return queueChannel; - } - }; - private final SharedChannelProvider pubsubChannelProvider = new SharedChannelProvider( PublishSubscribeChannel.class) { @@ -126,13 +91,6 @@ public class LocalMessageChannelBinder extends MessageChannelBinderSupport { this.poller = poller; } - /** - * Set the size of the queue when using {@link QueueChannel}s. - */ - public void setQueueSize(int queueSize) { - this.queueSize = queueSize; - } - /** * Set the {@link ThreadPoolTaskExecutor}} core pool size to limit the number of concurrent * threads. The executor is used for PubSub operations. @@ -183,61 +141,23 @@ public class LocalMessageChannelBinder extends MessageChannelBinderSupport { this.executor.initialize(); } - /** - * For the local binder we bridge the router "output" channel to a queue channel; the queue - * channel gets the name and the source channel is named 'dynamic.output.to.' + name. - * {@inheritDoc} - */ @Override - public MessageChannel bindDynamicProducer(String name, Properties properties) { - return doBindDynamicProducer(name, "dynamic.output.to." + name, properties); - } - - /** - * For the local binder we bridge the router "output" channel to a pub/sub channel; the pub/sub - * channel gets the name and the source channel is named 'dynamic.output.to.' + name. - * {@inheritDoc} - */ - @Override - public MessageChannel bindDynamicPubSubProducer(String name, Properties properties) { - return doBindDynamicPubSubProducer(name, "dynamic.output.to." + name, properties); - } - - private SharedChannelProvider getChannelProvider(String name) { - SharedChannelProvider channelProvider = directChannelProvider; - // Use queue channel provider in case of named channels: - // point-to-point type syntax (queue:) and job input channel syntax (job:) - if (name.startsWith(P2P_NAMED_CHANNEL_TYPE_PREFIX) || name.startsWith(JOB_CHANNEL_TYPE_PREFIX)) { - channelProvider = queueChannelProvider; - } - return channelProvider; - } - - /** - * Looks up or creates a DirectChannel with the given name and creates a bridge from that channel to the provided - * channel instance. - */ - @Override - public void bindConsumer(String name, MessageChannel moduleInputChannel, Properties properties) { - validateConsumerProperties(name, properties, CONSUMER_STANDARD_PROPERTIES); - doRegisterConsumer(name, moduleInputChannel, getChannelProvider(name), properties); - } - - @Override - public void bindPubSubConsumer(String name, MessageChannel moduleInputChannel, String group, + protected Binding doBindConsumer(String name, String group, MessageChannel moduleInputChannel, Properties properties) { validateConsumerProperties(name, properties, CONSUMER_STANDARD_PROPERTIES); - doRegisterConsumer(name, moduleInputChannel, this.pubsubChannelProvider, properties); + return doRegisterConsumer(name, moduleInputChannel, this.pubsubChannelProvider, properties); } - private void doRegisterConsumer(String name, MessageChannel moduleInputChannel, + private Binding doRegisterConsumer(String name, MessageChannel moduleInputChannel, SharedChannelProvider channelProvider, Properties properties) { Assert.hasText(name, "a valid name is required to register an inbound channel"); Assert.notNull(moduleInputChannel, "channel must not be null"); - MessageChannel registeredChannel = channelProvider.lookupOrCreateSharedChannel(name); + MessageChannel registeredChannel = channelProvider.lookupOrCreateSharedChannel("localbinder." + name); bridge(name, registeredChannel, moduleInputChannel, "inbound." + ((NamedComponent) registeredChannel).getComponentName(), new LocalBindingPropertiesAccessor(properties)); + // TODO: ? + return null; } /** @@ -245,109 +165,25 @@ public class LocalMessageChannelBinder extends MessageChannelBinderSupport { * channel instance. */ @Override - public void bindProducer(String name, MessageChannel moduleOutputChannel, Properties properties) { + public Binding bindProducer(String name, MessageChannel moduleOutputChannel, Properties properties) { validateConsumerProperties(name, properties, PRODUCER_STANDARD_PROPERTIES); - doRegisterProducer(name, moduleOutputChannel, getChannelProvider(name), properties); + return doRegisterProducer(name, moduleOutputChannel, this.pubsubChannelProvider, properties); } - @Override - public void bindPubSubProducer(String name, MessageChannel moduleOutputChannel, - Properties properties) { - validateConsumerProperties(name, properties, PRODUCER_STANDARD_PROPERTIES); - doRegisterProducer(name, moduleOutputChannel, this.pubsubChannelProvider, properties); - } - - private void doRegisterProducer(String name, MessageChannel moduleOutputChannel, + private Binding doRegisterProducer(String name, MessageChannel moduleOutputChannel, SharedChannelProvider channelProvider, Properties properties) { Assert.hasText(name, "a valid name is required to register an outbound channel"); Assert.notNull(moduleOutputChannel, "channel must not be null"); - MessageChannel registeredChannel = channelProvider.lookupOrCreateSharedChannel(name); + MessageChannel registeredChannel = channelProvider.lookupOrCreateSharedChannel("localbinder." + name); bridge(name, moduleOutputChannel, registeredChannel, "outbound." + ((NamedComponent) registeredChannel).getComponentName(), new LocalBindingPropertiesAccessor(properties)); + // TODO: ? + return null; } @Override - public void bindRequestor(final String name, MessageChannel requests, final MessageChannel replies, - Properties properties) { - validateConsumerProperties(name, properties, CONSUMER_REQUEST_REPLY_PROPERTIES); - final MessageChannel requestChannel = this.findOrCreateRequestReplyChannel(name, "requestor.", properties); - Assert.isInstanceOf(SubscribableChannel.class, requests); - ((SubscribableChannel) requests).subscribe(new MessageHandler() { - - @Override - public void handleMessage(Message message) throws MessagingException { - requestChannel.send(message); - } - }); - - ExecutorChannel replyChannel = this.findOrCreateRequestReplyChannel(name, "replier.", properties); - replyChannel.subscribe(new MessageHandler() { - - @Override - public void handleMessage(Message message) throws MessagingException { - replies.send(message); - } - }); - } - - @Override - public void bindReplier(String name, final MessageChannel requests, MessageChannel replies, - Properties properties) { - validateConsumerProperties(name, properties, CONSUMER_REQUEST_REPLY_PROPERTIES); - SubscribableChannel requestChannel = this.findOrCreateRequestReplyChannel(name, "requestor.", properties); - requestChannel.subscribe(new MessageHandler() { - - @Override - public void handleMessage(Message message) throws MessagingException { - requests.send(message); - } - }); - - Assert.isInstanceOf(SubscribableChannel.class, replies); - final SubscribableChannel replyChannel = this.findOrCreateRequestReplyChannel(name, "replier.", properties); - ((SubscribableChannel) replies).subscribe(new MessageHandler() { - - @Override - public void handleMessage(Message message) throws MessagingException { - replyChannel.send(message); - } - }); - } - - private synchronized ExecutorChannel findOrCreateRequestReplyChannel(String name, String prefix, - Properties properties) { - String channelName = prefix + name; - ExecutorChannel channel = this.requestReplyChannels.get(channelName); - if (channel == null) { - ThreadPoolTaskExecutor executor = createRequestReplyExecutor(name, properties); - channel = new ExecutorChannel(executor); - channel.setBeanFactory(getBeanFactory()); - this.requestReplyChannels.put(channelName, channel); - this.reqRepExecutors.put(name, executor); - } - return channel; - } - - private ThreadPoolTaskExecutor createRequestReplyExecutor(String name, Properties properties) { - ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); - executor.setCorePoolSize(new LocalBindingPropertiesAccessor(properties).getConcurrency(DEFAULT_REQ_REPLY_CONCURRENCY)); - executor.setThreadNamePrefix(THREAD_NAME_PREFIX + name + "-"); - executor.initialize(); - return executor; - } - - @Override - public void unbindProducer(String name, MessageChannel channel) { - this.requestReplyChannels.remove("replier." + name); - MessageChannel requestChannel = this.requestReplyChannels.remove("requestor." + name); - if (requestChannel == null) { - super.unbindProducer(name, channel); - } - ThreadPoolTaskExecutor executor = this.reqRepExecutors.remove(name); - if (executor != null) { - executor.shutdown(); - } + public void unbind(Binding binding) { } protected BridgeHandler bridge(String name, MessageChannel from, MessageChannel to, String bridgeName, @@ -397,7 +233,7 @@ public class LocalMessageChannelBinder extends MessageChannelBinderSupport { try { cefb.getObject().setComponentName(handler.getComponentName()); - Binding binding = isInbound ? Binding.forConsumer(name, cefb.getObject(), to, properties) + Binding binding = isInbound ? Binding.forConsumer(name, null, cefb.getObject(), to, properties) : Binding.forProducer(name, from, cefb.getObject(), properties); addBinding(binding); binding.start(); diff --git a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/stub1/StubBinder1.java b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/stub1/StubBinder1.java index 833fe9666..8089684c2 100644 --- a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/stub1/StubBinder1.java +++ b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/stub1/StubBinder1.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-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. @@ -19,11 +19,13 @@ package org.springframework.cloud.stream.binder.stub1; import java.util.Properties; import org.springframework.cloud.stream.binder.Binder; +import org.springframework.cloud.stream.binder.Binding; /** * @author Marius Bogoevici + * @author Mark Fisher */ -public class StubBinder1 implements Binder { +public class StubBinder1 implements Binder { private String name; @@ -36,67 +38,17 @@ public class StubBinder1 implements Binder { } @Override - public void bindConsumer(String name, Object inboundBindTarget, Properties properties) { - - } - - @Override - public void bindPubSubConsumer(String name, Object inboundBindTarget, String group, Properties properties) { - - } - - @Override - public void bindProducer(String name, Object outboundBindTarget, Properties properties) { - - } - - @Override - public void bindPubSubProducer(String name, Object outboundBindTarget, Properties properties) { - - } - - @Override - public void unbindConsumers(String name) { - - } - - @Override - public void unbindPubSubConsumers(String name, String group) { - - } - - @Override - public void unbindProducers(String name) { - - } - - @Override - public void unbindConsumer(String name, Object inboundBindTarget) { - - } - - @Override - public void unbindProducer(String name, Object outboundBindTarget) { - - } - - @Override - public void bindRequestor(String name, Object requests, Object replies, Properties properties) { - - } - - @Override - public void bindReplier(String name, Object requests, Object replies, Properties properties) { - - } - - @Override - public Object bindDynamicProducer(String name, Properties properties) { + public Binding bindConsumer(String name, String group, Object inboundBindTarget, Properties properties) { return null; } @Override - public Object bindDynamicPubSubProducer(String name, Properties properties) { + public Binding bindProducer(String name, Object outboundBindTarget, Properties properties) { return null; } + + @Override + public void unbind(Binding binding) { + } + } diff --git a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/stub1/StubBinder1Configuration.java b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/stub1/StubBinder1Configuration.java index b6a625ae0..dc2eb9ac2 100644 --- a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/stub1/StubBinder1Configuration.java +++ b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/stub1/StubBinder1Configuration.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-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. @@ -31,7 +31,7 @@ public class StubBinder1Configuration { @Bean @ConfigurationProperties("binder1") - public Binder binder() { + public Binder binder() { return new StubBinder1(); } } diff --git a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/stub2/StubBinder2.java b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/stub2/StubBinder2.java index f20c4e0c8..a4cf2e307 100644 --- a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/stub2/StubBinder2.java +++ b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/stub2/StubBinder2.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-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. @@ -19,80 +19,33 @@ package org.springframework.cloud.stream.binder.stub2; import java.util.Properties; import org.springframework.cloud.stream.binder.Binder; +import org.springframework.cloud.stream.binder.Binding; /** * @author Marius Bogoevici + * @author Mark Fisher */ -public class StubBinder2 implements Binder { +public class StubBinder2 implements Binder { - private StubBinder2Dependency stubBinder2Dependency; + @SuppressWarnings("unused") + private final StubBinder2Dependency stubBinder2Dependency; public StubBinder2(StubBinder2Dependency stubBinder2Dependency) { this.stubBinder2Dependency = stubBinder2Dependency; } @Override - public void bindConsumer(String name, Object inboundBindTarget, Properties properties) { - - } - - @Override - public void bindPubSubConsumer(String name, Object inboundBindTarget, String group, Properties properties) { - - } - - @Override - public void bindProducer(String name, Object outboundBindTarget, Properties properties) { - - } - - @Override - public void bindPubSubProducer(String name, Object outboundBindTarget, Properties properties) { - - } - - @Override - public void unbindConsumers(String name) { - - } - - @Override - public void unbindPubSubConsumers(String name, String group) { - - } - - @Override - public void unbindProducers(String name) { - - } - - @Override - public void unbindConsumer(String name, Object inboundBindTarget) { - - } - - @Override - public void unbindProducer(String name, Object outboundBindTarget) { - - } - - @Override - public void bindRequestor(String name, Object requests, Object replies, Properties properties) { - - } - - @Override - public void bindReplier(String name, Object requests, Object replies, Properties properties) { - - } - - @Override - public Object bindDynamicProducer(String name, Properties properties) { + public Binding bindConsumer(String name, String group, Object inboundBindTarget, Properties properties) { return null; } @Override - public Object bindDynamicPubSubProducer(String name, Properties properties) { + public Binding bindProducer(String name, Object outboundBindTarget, Properties properties) { return null; } + + @Override + public void unbind(Binding binding) { + } + } diff --git a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/stub2/StubBinder2ConfigurationA.java b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/stub2/StubBinder2ConfigurationA.java index 5f896ef6f..1043ee9d2 100644 --- a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/stub2/StubBinder2ConfigurationA.java +++ b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/stub2/StubBinder2ConfigurationA.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-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. @@ -28,7 +28,7 @@ import org.springframework.context.annotation.Configuration; public class StubBinder2ConfigurationA { @Bean - public Binder binder(StubBinder2Dependency dependency) { + public Binder binder(StubBinder2Dependency dependency) { return new StubBinder2(dependency); } } diff --git a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binding/ChannelBindingServiceTests.java b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binding/ChannelBindingServiceTests.java index 61fb120b2..761462cbf 100644 --- a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binding/ChannelBindingServiceTests.java +++ b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binding/ChannelBindingServiceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-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. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.cloud.stream.binding; import static org.mockito.Mockito.verify; @@ -27,6 +28,7 @@ import org.junit.Test; import org.springframework.cloud.stream.binder.Binder; import org.springframework.cloud.stream.binder.BinderConfiguration; import org.springframework.cloud.stream.binder.BinderType; +import org.springframework.cloud.stream.binder.Binding; import org.springframework.cloud.stream.binder.DefaultBinderFactory; import org.springframework.cloud.stream.config.BindingProperties; import org.springframework.cloud.stream.config.ChannelBindingServiceProperties; @@ -36,12 +38,12 @@ import org.springframework.messaging.MessageChannel; /** * @author Gary Russell - * + * @author Mark Fisher */ public class ChannelBindingServiceTests { @Test - public void testSimple() throws Exception { + public void testDefaultGroup() throws Exception { ChannelBindingServiceProperties properties = new ChannelBindingServiceProperties(); Map bindings = new HashMap<>(); BindingProperties props = new BindingProperties(); @@ -49,7 +51,6 @@ public class ChannelBindingServiceTests { String name = "foo"; bindings.put(name, props); properties.setBindings(bindings); - @SuppressWarnings("unchecked") DefaultBinderFactory binderFactory = new DefaultBinderFactory<>(Collections.singletonMap("mock", new BinderConfiguration(new BinderType("mock", new Class[]{MockBinderConfiguration.class}), @@ -57,23 +58,22 @@ public class ChannelBindingServiceTests { Binder binder = binderFactory.getBinder("mock"); ChannelBindingService service = new ChannelBindingService(properties, binderFactory); MessageChannel inputChannel = new DirectChannel(); - service.bindConsumer(inputChannel, name); + Binding binding = service.bindConsumer(inputChannel, name); service.unbindConsumers(name); - verify(binder).bindConsumer(name, inputChannel, properties.getConsumerProperties(name)); - verify(binder).unbindConsumers(name); + verify(binder).bindConsumer(name, props.getGroup(), inputChannel, properties.getConsumerProperties(name)); + verify(binder).unbind(binding); binderFactory.destroy(); } @Test - public void testPubSub() throws Exception { + public void testExplicitGroup() throws Exception { ChannelBindingServiceProperties properties = new ChannelBindingServiceProperties(); Map bindings = new HashMap<>(); BindingProperties props = new BindingProperties(); - props.setDestination("topic:foo"); + props.setDestination("foo"); String name = "foo"; bindings.put(name, props); properties.setBindings(bindings); - @SuppressWarnings("unchecked") DefaultBinderFactory binderFactory = new DefaultBinderFactory<>(Collections.singletonMap("mock", new BinderConfiguration(new BinderType("mock", new Class[]{MockBinderConfiguration.class}), @@ -81,10 +81,10 @@ public class ChannelBindingServiceTests { Binder binder = binderFactory.getBinder("mock"); ChannelBindingService service = new ChannelBindingService(properties, binderFactory); MessageChannel inputChannel = new DirectChannel(); - service.bindConsumer(inputChannel, name); + Binding binding = service.bindConsumer(inputChannel, name); service.unbindConsumers(name); - verify(binder).bindPubSubConsumer(name, inputChannel, props.getGroup(), properties.getConsumerProperties(name)); - verify(binder).unbindPubSubConsumers(name, props.getGroup()); + verify(binder).bindConsumer(name, props.getGroup(), inputChannel, properties.getConsumerProperties(name)); + verify(binder).unbind(binding); binderFactory.destroy(); } diff --git a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/partitioning/PartitionedConsumerTest.java b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/partitioning/PartitionedConsumerTest.java index 6aed10986..a7a29aa9d 100644 --- a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/partitioning/PartitionedConsumerTest.java +++ b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/partitioning/PartitionedConsumerTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-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. @@ -17,6 +17,7 @@ package org.springframework.cloud.stream.partitioning; import static org.hamcrest.core.IsEqual.equalTo; +import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -61,10 +62,9 @@ public class PartitionedConsumerTest { @SuppressWarnings("unchecked") public void testBindingPartitionedConsumer() { ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Properties.class); - verify(binder).bindConsumer(eq("partIn"), eq(testSink.input()), argumentCaptor.capture()); + verify(binder).bindConsumer(eq("partIn"), anyString(), eq(testSink.input()), argumentCaptor.capture()); Assert.assertThat(argumentCaptor.getValue().getProperty(BinderPropertyKeys.PARTITION_INDEX), equalTo("0")); - Assert.assertThat(argumentCaptor.getValue().getProperty(BinderPropertyKeys.COUNT), - equalTo("2")); + Assert.assertThat(argumentCaptor.getValue().getProperty(BinderPropertyKeys.COUNT), equalTo("2")); verifyNoMoreInteractions(binder); }