From 4e66d2b513b43e52e8aebf4898ce469e83329a43 Mon Sep 17 00:00:00 2001 From: Oleg Zhurakousky Date: Fri, 25 Mar 2022 11:21:13 +0100 Subject: [PATCH] Cherry-pick 2309 --- .../pom.xml | 5 + .../properties/RabbitProducerProperties.java | 14 ++ .../RabbitExchangeQueueProvisioner.java | 174 +++++++++++--- .../RabbitExchangeQueueProvisionerTests.java | 221 ++++++++++++++++++ .../rabbit/RabbitMessageChannelBinder.java | 19 +- .../binder/rabbit/RabbitBinderTests.java | 147 +++++++++++- 6 files changed, 534 insertions(+), 46 deletions(-) create mode 100644 spring-cloud-stream-binder-rabbit-core/src/test/java/org/springframework/cloud/stream/binder/rabbit/provisioning/RabbitExchangeQueueProvisionerTests.java diff --git a/spring-cloud-stream-binder-rabbit-core/pom.xml b/spring-cloud-stream-binder-rabbit-core/pom.xml index dfa4c8583..7d3e7ee64 100644 --- a/spring-cloud-stream-binder-rabbit-core/pom.xml +++ b/spring-cloud-stream-binder-rabbit-core/pom.xml @@ -44,5 +44,10 @@ http-client 2.1.0.RELEASE + + org.springframework.cloud + spring-cloud-stream-binder-rabbit-test-support + test + diff --git a/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/properties/RabbitProducerProperties.java b/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/properties/RabbitProducerProperties.java index bbfd7f64b..b25a6e228 100644 --- a/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/properties/RabbitProducerProperties.java +++ b/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/properties/RabbitProducerProperties.java @@ -102,6 +102,12 @@ public class RabbitProducerProperties extends RabbitCommonProperties { */ private Expression delayExpression; + /** + * a static routing key when publishing messages; default is the destination name; + * suffixed by "-partition" when partitioned. This is only used if `routingKeyExpression` is null + */ + private String routingKey; + /** * a custom routing key when publishing messages; default is the destination name; * suffixed by "-partition" when partitioned. @@ -239,6 +245,14 @@ public class RabbitProducerProperties extends RabbitCommonProperties { this.routingKeyExpression = routingKeyExpression; } + public String getRoutingKey() { + return this.routingKey; + } + + public void setRoutingKey(String routingKey) { + this.routingKey = routingKey; + } + public String getConfirmAckChannel() { return this.confirmAckChannel; } diff --git a/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/provisioning/RabbitExchangeQueueProvisioner.java b/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/provisioning/RabbitExchangeQueueProvisioner.java index 46264fffc..b0a036b95 100644 --- a/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/provisioning/RabbitExchangeQueueProvisioner.java +++ b/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/provisioning/RabbitExchangeQueueProvisioner.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2018 the original author or authors. + * Copyright 2016-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Stream; import org.apache.commons.logging.Log; @@ -59,6 +60,7 @@ import org.springframework.cloud.stream.provisioning.ProvisioningException; import org.springframework.cloud.stream.provisioning.ProvisioningProvider; import org.springframework.context.ApplicationListener; import org.springframework.context.support.GenericApplicationContext; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -94,6 +96,8 @@ public class RabbitExchangeQueueProvisioner private final List customizers; + private final AtomicInteger producerExchangeBeanNameQualifier = new AtomicInteger(); + public RabbitExchangeQueueProvisioner(ConnectionFactory connectionFactory) { this(connectionFactory, Collections.emptyList()); } @@ -115,8 +119,9 @@ public class RabbitExchangeQueueProvisioner producerProperties.getExtension().getPrefix(), name); Exchange exchange = buildExchange(producerProperties.getExtension(), exchangeName); + String beanNameQualifier = "prod" + this.producerExchangeBeanNameQualifier.incrementAndGet(); if (producerProperties.getExtension().isDeclareExchange()) { - declareExchange(exchangeName, exchange); + declareExchange(exchangeName, beanNameQualifier, exchange); } Binding binding = null; for (String requiredGroupName : producerProperties.getRequiredGroups()) { @@ -124,7 +129,7 @@ public class RabbitExchangeQueueProvisioner .isQueueNameGroupOnly() ? requiredGroupName : (exchangeName + "." + requiredGroupName); if (!producerProperties.isPartitioned()) { - autoBindDLQ(baseQueueName, baseQueueName, + autoBindDLQ(baseQueueName, baseQueueName, requiredGroupName, producerProperties.getExtension()); if (producerProperties.getExtension().isBindQueue()) { Queue queue = new Queue(baseQueueName, true, false, false, queueArgs( @@ -148,7 +153,7 @@ public class RabbitExchangeQueueProvisioner for (int i = 0; i < producerProperties.getPartitionCount(); i++) { String partitionSuffix = "-" + i; String partitionQueueName = baseQueueName + partitionSuffix; - autoBindDLQ(baseQueueName, baseQueueName + partitionSuffix, + autoBindDLQ(baseQueueName, baseQueueName + partitionSuffix, requiredGroupName, producerProperties.getExtension()); if (producerProperties.getExtension().isBindQueue()) { Queue queue = new Queue(partitionQueueName, true, false, false, @@ -173,7 +178,7 @@ public class RabbitExchangeQueueProvisioner } } } - return new RabbitProducerDestination(exchange, binding); + return new RabbitProducerDestination(exchange, binding, beanNameQualifier); } @Override @@ -209,7 +214,7 @@ public class RabbitExchangeQueueProvisioner .toArray(String[]::new); consumerDestination = new RabbitConsumerDestination( StringUtils.arrayToCommaDelimitedString(provisionedDestinations), - null); + null, group, name); } return consumerDestination; } @@ -218,21 +223,19 @@ public class RabbitExchangeQueueProvisioner ExtendedConsumerProperties properties) { boolean anonymous = !StringUtils.hasText(group); - Base64UrlNamingStrategy anonQueueNameGenerator = null; + String anonymousGroup = null; if (anonymous) { - anonQueueNameGenerator = new Base64UrlNamingStrategy( + anonymousGroup = new Base64UrlNamingStrategy( properties.getExtension().getAnonymousGroupPrefix() == null ? "" - : properties.getExtension().getAnonymousGroupPrefix()); + : properties.getExtension().getAnonymousGroupPrefix()).generateName(); } String baseQueueName; if (properties.getExtension().isQueueNameGroupOnly()) { - baseQueueName = anonymous ? anonQueueNameGenerator.generateName() - : group; + baseQueueName = anonymous ? anonymousGroup : group; } else { - baseQueueName = groupedName(name, - anonymous ? anonQueueNameGenerator.generateName() : group); + baseQueueName = groupedName(name, anonymous ? anonymousGroup : group); } if (this.logger.isInfoEnabled()) { this.logger.info("declaring queue for inbound: " + baseQueueName @@ -242,7 +245,7 @@ public class RabbitExchangeQueueProvisioner final String exchangeName = applyPrefix(prefix, name); Exchange exchange = buildExchange(properties.getExtension(), exchangeName); if (properties.getExtension().isDeclareExchange()) { - declareExchange(exchangeName, exchange); + declareExchange(exchangeName, anonymous ? anonymousGroup : group, exchange); } String queueName = applyPrefix(prefix, baseQueueName); boolean partitioned = !anonymous && properties.isPartitioned(); @@ -285,9 +288,9 @@ public class RabbitExchangeQueueProvisioner } if (durable) { autoBindDLQ(applyPrefix(properties.getExtension().getPrefix(), baseQueueName), - queueName, properties.getExtension()); + queueName, group, properties.getExtension()); } - return new RabbitConsumerDestination(queue.getName(), binding); + return new RabbitConsumerDestination(queue.getName(), binding, anonymous ? baseQueueName : group, name); } /** @@ -402,9 +405,10 @@ public class RabbitExchangeQueueProvisioner * @param baseQueueName The base name for the queue (including the binder prefix, if * any). * @param routingKey The routing key for the queue. + * @param group The consumer group. * @param properties the properties. */ - private void autoBindDLQ(final String baseQueueName, String routingKey, + private void autoBindDLQ(final String baseQueueName, String routingKey, String group, RabbitCommonProperties properties) { boolean autoBindDlq = properties.isAutoBindDlq(); if (this.logger.isDebugEnabled()) { @@ -423,7 +427,7 @@ public class RabbitExchangeQueueProvisioner declareQueue(dlqName, dlq); String dlxName = deadLetterExchangeName(properties); if (properties.isDeclareDlx()) { - declareExchange(dlxName, + declareExchange(dlxName, group, new ExchangeBuilder(dlxName, properties.getDeadLetterExchangeType()).durable(true) .build()); @@ -440,7 +444,7 @@ public class RabbitExchangeQueueProvisioner * Also bind with the base queue name when republishToDlq is used, which * does not know about partitioning */ - declareBinding(dlqName, new Binding(dlq.getName(), DestinationType.QUEUE, + declareBinding(dlqName + ".2", new Binding(dlq.getName(), DestinationType.QUEUE, dlxName, baseQueueName, arguments)); } } @@ -600,7 +604,7 @@ public class RabbitExchangeQueueProvisioner } } - private void declareExchange(final String rootName, final Exchange exchangeArg) { + private void declareExchange(final String rootName, String group, final Exchange exchangeArg) { Exchange exchange = exchangeArg; for (DeclarableCustomizer customizer : this.customizers) { exchange = (Exchange) customizer.apply(exchange); @@ -625,7 +629,7 @@ public class RabbitExchangeQueueProvisioner e); } } - addToAutoDeclareContext(rootName + ".exchange", exchange); + addToAutoDeclareContext(rootName + "." + group + ".exchange", exchange); } private void addToAutoDeclareContext(String name, Object bean) { @@ -665,19 +669,93 @@ public class RabbitExchangeQueueProvisioner public void cleanAutoDeclareContext(ConsumerDestination destination, ExtendedConsumerProperties consumerProperties) { + synchronized (this.autoDeclareContext) { Stream.of(StringUtils.tokenizeToStringArray(destination.getName(), ",", true, true)).forEach(name -> { - name = name.trim(); - removeSingleton(name + ".binding"); - removeSingleton(name); - String dlq = name + ".dlq"; - removeSingleton(dlq + ".binding"); - removeSingleton(dlq); + String group = null; + String bindingName = null; + if (destination instanceof RabbitConsumerDestination) { + group = ((RabbitConsumerDestination) destination).getGroup(); + bindingName = ((RabbitConsumerDestination) destination).getBindingName(); + } + RabbitConsumerProperties properties = consumerProperties.getExtension(); + String toRemove = properties.isQueueNameGroupOnly() ? bindingName + "." + group : name.trim(); + boolean partitioned = consumerProperties.isPartitioned(); + if (partitioned) { + toRemove = removePartitionPart(toRemove); + } + removeSingleton(toRemove + ".exchange"); + removeQueueAndBindingBeans(properties, name.trim(), "", group, partitioned); }); } } + public void cleanAutoDeclareContext(ProducerDestination dest, + ExtendedProducerProperties properties) { + + synchronized (this.autoDeclareContext) { + if (dest instanceof RabbitProducerDestination) { + String qual = ((RabbitProducerDestination) dest).getBeanNameQualifier(); + removeSingleton(dest.getName() + "." + qual + ".exchange"); + String[] requiredGroups = properties.getRequiredGroups(); + if (!ObjectUtils.isEmpty(requiredGroups)) { + for (String group : requiredGroups) { + if (properties.isPartitioned()) { + for (int i = 0; i < properties.getPartitionCount(); i++) { + removeQueueAndBindingBeans(properties.getExtension(), + properties.getExtension().isQueueNameGroupOnly() ? "" : dest.getName(), + group + "-" + i, group, true); + } + } + else { + removeQueueAndBindingBeans(properties.getExtension(), dest.getName() + "." + group, "", + group, false); + } + } + } + } + } + } + + private void removeQueueAndBindingBeans(RabbitCommonProperties properties, String name, String suffix, + String group, boolean partitioned) { + + boolean suffixPresent = StringUtils.hasText(suffix); + String withSuffix = name + (suffixPresent ? ("." + suffix) : ""); + String nameDotOptional = name; + if (!StringUtils.hasText(name)) { + withSuffix = suffix; + } + else { + nameDotOptional = name + "."; + } + removeSingleton(withSuffix + ".binding"); + removeSingleton(withSuffix); + String dlq = (suffixPresent ? nameDotOptional + group : withSuffix) + ".dlq"; // only one DLQ when partitioned + if (StringUtils.hasText(properties.getDeadLetterQueueName())) { + dlq = properties.getDeadLetterQueueName(); + } + else if (partitioned) { + String removedPart = removePartitionPart(dlq); + if (!removedPart.endsWith(".dlq")) { + dlq = removedPart + ".dlq"; + } + } + removeSingleton(dlq + ".binding"); + removeSingleton(dlq + ".2.binding"); + removeSingleton(dlq); + removeSingleton(deadLetterExchangeName(properties) + "." + group + ".exchange"); + } + + private String removePartitionPart(String toRemove) { + int finalHyphen = toRemove.lastIndexOf("-"); + if (finalHyphen > 0) { + return toRemove.substring(0, finalHyphen); + } + return toRemove; + } + private void removeSingleton(String name) { if (this.autoDeclareContext.containsBean(name)) { ConfigurableListableBeanFactory beanFactory = this.autoDeclareContext @@ -699,10 +777,13 @@ public class RabbitExchangeQueueProvisioner private final Binding binding; - RabbitProducerDestination(Exchange exchange, Binding binding) { + private final String beanNameQualifier; + + RabbitProducerDestination(Exchange exchange, Binding binding, String beanNameQualifier) { Assert.notNull(exchange, "exchange must not be null"); this.exchange = exchange; this.binding = binding; + this.beanNameQualifier = beanNameQualifier; } @Override @@ -715,10 +796,15 @@ public class RabbitExchangeQueueProvisioner return this.exchange.getName(); } + @Nullable + String getBeanNameQualifier() { + return this.beanNameQualifier; + } + @Override public String toString() { - return "RabbitProducerDestination{" + "exchange=" + exchange + ", binding=" - + binding + '}'; + return "RabbitProducerDestination{" + "exchange=" + this.exchange + ", binding=" + + this.binding + '}'; } } @@ -729,16 +815,16 @@ public class RabbitExchangeQueueProvisioner private final Binding binding; - RabbitConsumerDestination(String queue, Binding binding) { + private final String group; + + private final String bindingName; + + RabbitConsumerDestination(String queue, Binding binding, String group, String bindingName) { Assert.notNull(queue, "queue must not be null"); this.queue = queue; this.binding = binding; - } - - @Override - public String toString() { - return "RabbitConsumerDestination{" + "queue=" + queue + ", binding=" - + binding + '}'; + this.group = group; + this.bindingName = bindingName; } @Override @@ -746,6 +832,20 @@ public class RabbitExchangeQueueProvisioner return this.queue; } + String getGroup() { + return this.group; + } + + String getBindingName() { + return this.bindingName; + } + + @Override + public String toString() { + return "RabbitConsumerDestination{" + "queue=" + this.queue + ", binding=" + + this.binding + ", group=" + this.group + ", bindingName=" + this.bindingName + '}'; + } + } } diff --git a/spring-cloud-stream-binder-rabbit-core/src/test/java/org/springframework/cloud/stream/binder/rabbit/provisioning/RabbitExchangeQueueProvisionerTests.java b/spring-cloud-stream-binder-rabbit-core/src/test/java/org/springframework/cloud/stream/binder/rabbit/provisioning/RabbitExchangeQueueProvisionerTests.java new file mode 100644 index 000000000..6ac9a26ff --- /dev/null +++ b/spring-cloud-stream-binder-rabbit-core/src/test/java/org/springframework/cloud/stream/binder/rabbit/provisioning/RabbitExchangeQueueProvisionerTests.java @@ -0,0 +1,221 @@ +/* + * Copyright 2022-2022 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 + * + * https://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.rabbit.provisioning; + +import java.io.IOException; +import java.util.Set; + +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.impl.AMQImpl.Queue.DeclareOk; +import org.junit.jupiter.api.Test; + +import org.springframework.amqp.core.Declarable; +import org.springframework.amqp.rabbit.connection.Connection; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.utils.test.TestUtils; +import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; +import org.springframework.cloud.stream.binder.ExtendedProducerProperties; +import org.springframework.cloud.stream.binder.rabbit.properties.RabbitConsumerProperties; +import org.springframework.cloud.stream.binder.rabbit.properties.RabbitProducerProperties; +import org.springframework.cloud.stream.provisioning.ConsumerDestination; +import org.springframework.cloud.stream.provisioning.ProducerDestination; +import org.springframework.context.ApplicationContext; +import org.springframework.expression.common.LiteralExpression; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.mock; + +/** + * @author Gary Russell + * @since 3.2.3 + * + */ +public class RabbitExchangeQueueProvisionerTests { + + @Test + void consumerDeclarationsWithDlq() throws IOException { + ConnectionFactory cf = mock(ConnectionFactory.class); + Connection conn = mock(Connection.class); + given(cf.createConnection()).willReturn(conn); + Channel channel = mock(Channel.class); + willReturn(new DeclareOk("x", 0, 0)) + .given(channel).queueDeclare(any(), eq(Boolean.TRUE), eq(Boolean.FALSE), eq(Boolean.FALSE), any()); + given(conn.createChannel(anyBoolean())).willReturn(channel); + RabbitExchangeQueueProvisioner provisioner = new RabbitExchangeQueueProvisioner(cf); + + RabbitConsumerProperties props = new RabbitConsumerProperties(); + props.setAutoBindDlq(true); + ExtendedConsumerProperties properties = + new ExtendedConsumerProperties(props); + ConsumerDestination dest = provisioner.provisionConsumerDestination("foo", "group", properties); + ApplicationContext ctx = + TestUtils.getPropertyValue(provisioner, "autoDeclareContext", ApplicationContext.class); + Set declarables = ctx.getBeansOfType(Declarable.class).keySet(); + assertThat(declarables).contains("foo.group.exchange", "foo.group", "foo.group.binding", "foo.group.dlq", + "DLX.group.exchange", "foo.group.dlq.binding", "foo.group.dlq.2.binding"); + provisioner.cleanAutoDeclareContext(dest, properties); + assertThat(ctx.getBeansOfType(Declarable.class)).isEmpty(); + } + + @Test + void consumerDeclarationsWithDlqQueueNameIsGroup() throws IOException { + ConnectionFactory cf = mock(ConnectionFactory.class); + Connection conn = mock(Connection.class); + given(cf.createConnection()).willReturn(conn); + Channel channel = mock(Channel.class); + willReturn(new DeclareOk("x", 0, 0)) + .given(channel).queueDeclare(any(), eq(Boolean.TRUE), eq(Boolean.FALSE), eq(Boolean.FALSE), any()); + given(conn.createChannel(anyBoolean())).willReturn(channel); + RabbitExchangeQueueProvisioner provisioner = new RabbitExchangeQueueProvisioner(cf); + + RabbitConsumerProperties props = new RabbitConsumerProperties(); + props.setAutoBindDlq(true); + props.setQueueNameGroupOnly(true); + ExtendedConsumerProperties properties = + new ExtendedConsumerProperties(props); + ConsumerDestination dest = provisioner.provisionConsumerDestination("fiz", "group", properties); + ApplicationContext ctx = + TestUtils.getPropertyValue(provisioner, "autoDeclareContext", ApplicationContext.class); + Set declarables = ctx.getBeansOfType(Declarable.class).keySet(); + assertThat(declarables).contains("fiz.group.exchange", "group", "group.binding", "group.dlq", + "DLX.group.exchange", "group.dlq.binding", "group.dlq.2.binding"); + provisioner.cleanAutoDeclareContext(dest, properties); + assertThat(ctx.getBeansOfType(Declarable.class)).isEmpty(); + } + + @Test + void producerDeclarationsNoGroups() throws IOException { + ConnectionFactory cf = mock(ConnectionFactory.class); + Connection conn = mock(Connection.class); + given(cf.createConnection()).willReturn(conn); + Channel channel = mock(Channel.class); + willReturn(new DeclareOk("x", 0, 0)) + .given(channel).queueDeclare(any(), eq(Boolean.TRUE), eq(Boolean.FALSE), eq(Boolean.FALSE), any()); + given(conn.createChannel(anyBoolean())).willReturn(channel); + RabbitExchangeQueueProvisioner provisioner = new RabbitExchangeQueueProvisioner(cf); + + RabbitProducerProperties props = new RabbitProducerProperties(); + ExtendedProducerProperties properties = + new ExtendedProducerProperties(props); + ProducerDestination dest = provisioner.provisionProducerDestination("bar", properties); + ApplicationContext ctx = + TestUtils.getPropertyValue(provisioner, "autoDeclareContext", ApplicationContext.class); + Set declarables = ctx.getBeansOfType(Declarable.class).keySet(); + String qual = TestUtils.getPropertyValue(dest, "beanNameQualifier", String.class); + assertThat(declarables).contains("bar." + qual + ".exchange"); + provisioner.cleanAutoDeclareContext(dest, properties); + assertThat(ctx.getBeansOfType(Declarable.class)).isEmpty(); + } + + @Test + void producerDeclarationsWithGroupsAndDlq() throws IOException { + ConnectionFactory cf = mock(ConnectionFactory.class); + Connection conn = mock(Connection.class); + given(cf.createConnection()).willReturn(conn); + Channel channel = mock(Channel.class); + willReturn(new DeclareOk("x", 0, 0)) + .given(channel).queueDeclare(any(), eq(Boolean.TRUE), eq(Boolean.FALSE), eq(Boolean.FALSE), any()); + given(conn.createChannel(anyBoolean())).willReturn(channel); + RabbitExchangeQueueProvisioner provisioner = new RabbitExchangeQueueProvisioner(cf); + + RabbitProducerProperties props = new RabbitProducerProperties(); + props.setAutoBindDlq(true); + ExtendedProducerProperties properties = + new ExtendedProducerProperties(props); + properties.setRequiredGroups("group1", "group2"); + ProducerDestination dest = provisioner.provisionProducerDestination("baz", properties); + ApplicationContext ctx = + TestUtils.getPropertyValue(provisioner, "autoDeclareContext", ApplicationContext.class); + Set declarables = ctx.getBeansOfType(Declarable.class).keySet(); + String qual = TestUtils.getPropertyValue(dest, "beanNameQualifier", String.class); + assertThat(declarables).contains("baz." + qual + ".exchange", "baz.group1", "baz.group1.binding", + "baz.group1.dlq", "DLX.group1.exchange", "baz.group1.dlq.binding", "baz.group2", "baz.group2.binding", + "baz.group2.dlq", "DLX.group2.exchange", "baz.group2.dlq.binding"); + provisioner.cleanAutoDeclareContext(dest, properties); + assertThat(ctx.getBeansOfType(Declarable.class)).isEmpty(); + } + + @Test + void producerDeclarationsWithGroupsAndDlqAndPartitions() throws IOException { + ConnectionFactory cf = mock(ConnectionFactory.class); + Connection conn = mock(Connection.class); + given(cf.createConnection()).willReturn(conn); + Channel channel = mock(Channel.class); + willReturn(new DeclareOk("x", 0, 0)) + .given(channel).queueDeclare(any(), eq(Boolean.TRUE), eq(Boolean.FALSE), eq(Boolean.FALSE), any()); + given(conn.createChannel(anyBoolean())).willReturn(channel); + RabbitExchangeQueueProvisioner provisioner = new RabbitExchangeQueueProvisioner(cf); + + RabbitProducerProperties props = new RabbitProducerProperties(); + props.setAutoBindDlq(true); + ExtendedProducerProperties properties = + new ExtendedProducerProperties(props); + properties.setRequiredGroups("group1", "group2"); + properties.setPartitionKeyExpression(new LiteralExpression("foo")); + properties.setPartitionCount(2); + ProducerDestination dest = provisioner.provisionProducerDestination("qux", properties); + ApplicationContext ctx = + TestUtils.getPropertyValue(provisioner, "autoDeclareContext", ApplicationContext.class); + Set declarables = ctx.getBeansOfType(Declarable.class).keySet(); + String qual = TestUtils.getPropertyValue(dest, "beanNameQualifier", String.class); + assertThat(declarables).contains("qux." + qual + ".exchange", "qux.group1-0", "qux.group1-0.binding", + "qux.group1-1", "qux.group1-1.binding", "qux.group1.dlq", "DLX.group1.exchange", + "qux.group1.dlq.binding", "qux.group2-0", + "qux.group2-0.binding", "qux.group2-1", "qux.group2-1.binding", "qux.group2.dlq", "DLX.group2.exchange", + "qux.group2.dlq.binding"); + provisioner.cleanAutoDeclareContext(dest, properties); + assertThat(ctx.getBeansOfType(Declarable.class)).isEmpty(); + } + + @Test + void producerDeclarationsWithGroupsAndDlqAndPartitionsQueueNameIsGroup() throws IOException { + ConnectionFactory cf = mock(ConnectionFactory.class); + Connection conn = mock(Connection.class); + given(cf.createConnection()).willReturn(conn); + Channel channel = mock(Channel.class); + willReturn(new DeclareOk("x", 0, 0)) + .given(channel).queueDeclare(any(), eq(Boolean.TRUE), eq(Boolean.FALSE), eq(Boolean.FALSE), any()); + given(conn.createChannel(anyBoolean())).willReturn(channel); + RabbitExchangeQueueProvisioner provisioner = new RabbitExchangeQueueProvisioner(cf); + + RabbitProducerProperties props = new RabbitProducerProperties(); + props.setAutoBindDlq(true); + props.setQueueNameGroupOnly(true); + ExtendedProducerProperties properties = + new ExtendedProducerProperties(props); + properties.setRequiredGroups("group1", "group2"); + properties.setPartitionKeyExpression(new LiteralExpression("foo")); + properties.setPartitionCount(2); + ProducerDestination dest = provisioner.provisionProducerDestination("qux", properties); + ApplicationContext ctx = + TestUtils.getPropertyValue(provisioner, "autoDeclareContext", ApplicationContext.class); + Set declarables = ctx.getBeansOfType(Declarable.class).keySet(); + String qual = TestUtils.getPropertyValue(dest, "beanNameQualifier", String.class); + assertThat(declarables).contains("qux." + qual + ".exchange", "group1-0", "group1-0.binding", "group1-1", + "group1-1.binding", "group1.dlq", "DLX.group1.exchange", "group1.dlq.binding", "group2-0", + "group2-0.binding", "group2-1", "group2-1.binding", "group2.dlq", "DLX.group2.exchange", + "group2.dlq.binding"); + provisioner.cleanAutoDeclareContext(dest, properties); + assertThat(ctx.getBeansOfType(Declarable.class)).isEmpty(); + } + +} diff --git a/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/RabbitMessageChannelBinder.java b/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/RabbitMessageChannelBinder.java index 6c4ac6e11..6b4fd1633 100644 --- a/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/RabbitMessageChannelBinder.java +++ b/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/RabbitMessageChannelBinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2019 the original author or authors. + * Copyright 2013-2022 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. @@ -217,10 +217,6 @@ public class RabbitMessageChannelBinder extends this.decompressingPostProcessor = decompressingPostProcessor; } - public String getProtocolIdentifier() { - return "amqp"; - } - /** * Set a {@link org.springframework.amqp.core.MessagePostProcessor} to compress * messages. Defaults to a @@ -964,13 +960,20 @@ public class RabbitMessageChannelBinder extends } @Override - protected void afterUnbindConsumer(ConsumerDestination consumerDestination, - String group, + protected void afterUnbindConsumer(ConsumerDestination consumerDestination, String group, ExtendedConsumerProperties consumerProperties) { - provisioningProvider.cleanAutoDeclareContext(consumerDestination, + + this.provisioningProvider.cleanAutoDeclareContext(consumerDestination, consumerProperties); } + @Override + protected void afterUnbindProducer(ProducerDestination destination, + ExtendedProducerProperties producerProperties) { + + this.provisioningProvider.cleanAutoDeclareContext(destination, producerProperties); + } + private RabbitTemplate buildRabbitTemplate(RabbitProducerProperties properties, boolean mandatory) { RabbitTemplate rabbitTemplate; if (properties.isBatchingEnabled()) { diff --git a/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitBinderTests.java b/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitBinderTests.java index 917e46a23..fe2724561 100644 --- a/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitBinderTests.java +++ b/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitBinderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2019 the original author or authors. + * Copyright 2013-2022 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. @@ -57,6 +57,7 @@ import org.springframework.amqp.core.AmqpTemplate; import org.springframework.amqp.core.AnonymousQueue; import org.springframework.amqp.core.Binding.DestinationType; import org.springframework.amqp.core.BindingBuilder; +import org.springframework.amqp.core.Declarable; import org.springframework.amqp.core.DirectExchange; import org.springframework.amqp.core.ExchangeTypes; import org.springframework.amqp.core.MessageDeliveryMode; @@ -241,6 +242,8 @@ public class RabbitBinderTests extends assertThat(event.get()).isNotNull(); producerBinding.unbind(); consumerBinding.unbind(); + + verifyAutoDeclareContextClear(binder); } @Test @@ -322,6 +325,8 @@ public class RabbitBinderTests extends assertThat(nack.getCorrelationData()).isEqualTo(message); assertThat(nack.getFailedMessage()).isEqualTo(message); producerBinding.unbind(); + + verifyAutoDeclareContextClear(binder); } @Test @@ -351,6 +356,8 @@ public class RabbitBinderTests extends assertThat(confirmLatch.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(confirm.get().getPayload()).isEqualTo("acksMessage".getBytes()); producerBinding.unbind(); + + verifyAutoDeclareContextClear(binder); } @Test @@ -375,6 +382,8 @@ public class RabbitBinderTests extends assertThat(confirm.isAck()).isTrue(); assertThat(correlation.getReturnedMessage()).isNotNull(); producerBinding.unbind(); + + verifyAutoDeclareContextClear(binder); } @Test @@ -454,6 +463,8 @@ public class RabbitBinderTests extends consumerBinding.unbind(); assertThat(endpoint.isRunning()).isFalse(); + + verifyAutoDeclareContextClear(binder); } @Test @@ -543,8 +554,11 @@ public class RabbitBinderTests extends assertThat(container.isRunning()).isTrue(); consumerBinding.unbind(); assertThat(container.isRunning()).isFalse(); + + verifyAutoDeclareContextClear(binder); } + @Test public void testAnonWithBuiltInExchangeCustomPrefix() throws Exception { RabbitTestBinder binder = getBinder(); @@ -563,8 +577,11 @@ public class RabbitBinderTests extends assertThat(container.isRunning()).isTrue(); consumerBinding.unbind(); assertThat(container.isRunning()).isFalse(); + + verifyAutoDeclareContextClear(binder); } + @Test public void testConsumerPropertiesWithUserInfrastructureCustomExchangeAndRK() throws Exception { @@ -611,8 +628,11 @@ public class RabbitBinderTests extends assertThat(exchange.getType()).isEqualTo("direct"); assertThat(exchange.isDurable()).isEqualTo(true); assertThat(exchange.isAutoDelete()).isEqualTo(false); + + verifyAutoDeclareContextClear(binder); } + @Test public void testConsumerPropertiesWithUserInfrastructureCustomQueueArgs() throws Exception { @@ -738,8 +758,11 @@ public class RabbitBinderTests extends consumerBinding.unbind(); assertThat(container.isRunning()).isFalse(); + + verifyAutoDeclareContextClear(binder); } + @Test public void testConsumerPropertiesWithHeaderExchanges() throws Exception { RabbitTestBinder binder = getBinder(); @@ -788,8 +811,11 @@ public class RabbitBinderTests extends assertThat(bindings.get(0).getDestination()).isEqualTo("propsHeader." + group + ".dlq"); assertThat(bindings.get(0).getArguments()).hasEntrySatisfying("x-match", v -> assertThat(v).isEqualTo("any")); assertThat(bindings.get(0).getArguments()).hasEntrySatisfying("foo", v -> assertThat(v).isEqualTo("bar")); + + verifyAutoDeclareContextClear(binder); } + @Test public void testProducerProperties(TestInfo testInfo) throws Exception { RabbitTestBinder binder = getBinder(); @@ -863,8 +889,11 @@ public class RabbitBinderTests extends producerBinding.unbind(); assertThat(endpoint.isRunning()).isFalse(); + + verifyAutoDeclareContextClear(binder); } + @Test public void testDurablePubSubWithAutoBindDLQ() throws Exception { RabbitAdmin admin = new RabbitAdmin(this.rabbitAvailableRule.getResource()); @@ -909,8 +938,11 @@ public class RabbitBinderTests extends consumerBinding.unbind(); assertThat(admin.getQueueProperties(TEST_PREFIX + "durabletest.0.tgroup.dlq")) .isNotNull(); + + verifyAutoDeclareContextClear(binder); } + @Test public void testNonDurablePubSubWithAutoBindDLQ() throws Exception { RabbitAdmin admin = new RabbitAdmin(this.rabbitAvailableRule.getResource()); @@ -940,8 +972,11 @@ public class RabbitBinderTests extends consumerBinding.unbind(); assertThat(admin.getQueueProperties(TEST_PREFIX + "nondurabletest.0.dlq")) .isNull(); + + verifyAutoDeclareContextClear(binder); } + @Test public void testAutoBindDLQ() throws Exception { RabbitTestBinder binder = getBinder(); @@ -1012,8 +1047,11 @@ public class RabbitBinderTests extends assertThat(context.containsBean(TEST_PREFIX + "dlqtest.default.dlq.binding")) .isFalse(); assertThat(context.containsBean(TEST_PREFIX + "dlqtest.default.dlq")).isFalse(); + + verifyAutoDeclareContextClear(binder); } + @Test public void testAutoBindDLQManualAcks() throws Exception { RabbitTestBinder binder = getBinder(); @@ -1087,8 +1125,11 @@ public class RabbitBinderTests extends assertThat(context.containsBean(TEST_PREFIX + "dlqTestManual.default.dlq.binding")) .isFalse(); assertThat(context.containsBean(TEST_PREFIX + "dlqTestManual.default.dlq")).isFalse(); + + verifyAutoDeclareContextClear(binder); } + @Test public void testAutoBindDLQPartionedConsumerFirst(TestInfo testInfo) throws Exception { RabbitTestBinder binder = getBinder(); @@ -1189,8 +1230,11 @@ public class RabbitBinderTests extends defaultConsumerBinding1.unbind(); defaultConsumerBinding2.unbind(); outputBinding.unbind(); + + verifyAutoDeclareContextClear(binder); } + @Test @Disabled public void testAutoBindDLQPartitionedConsumerFirstWithRepublishNoRetry(TestInfo testInfo) @@ -1347,6 +1391,8 @@ public class RabbitBinderTests extends defaultConsumerBinding1.unbind(); defaultConsumerBinding2.unbind(); outputBinding.unbind(); + + verifyAutoDeclareContextClear(binder); } @Test @@ -1451,8 +1497,11 @@ public class RabbitBinderTests extends defaultConsumerBinding1.unbind(); defaultConsumerBinding2.unbind(); outputBinding.unbind(); + + verifyAutoDeclareContextClear(binder); } + @Test public void testAutoBindDLQwithRepublish() throws Exception { this.maxStackTraceSize = RabbitUtils @@ -1519,8 +1568,11 @@ public class RabbitBinderTests extends assertThat(template.receive(TEST_PREFIX + "foo.dlqpubtest2.foo.dlq")).isNull(); consumerBinding.unbind(); + + verifyAutoDeclareContextClear(binder); } + @SuppressWarnings("unchecked") @Test public void testAutoBindDLQwithRepublishTx() throws Exception { @@ -1565,8 +1617,11 @@ public class RabbitBinderTests extends assertThat(TestUtils.getPropertyValue(errorHandler.get(0), "confirmType", ConfirmType.class)) .isEqualTo(ConfirmType.NONE); consumerBinding.unbind(); + + verifyAutoDeclareContextClear(binder); } + @SuppressWarnings("unchecked") @Test public void testAutoBindDLQwithRepublishSimpleConfirms() throws Exception { @@ -1613,8 +1668,11 @@ public class RabbitBinderTests extends assertThat(TestUtils.getPropertyValue(errorHandler.get(0), "confirmType", ConfirmType.class)) .isEqualTo(ConfirmType.SIMPLE); consumerBinding.unbind(); + + verifyAutoDeclareContextClear(binder); } + @SuppressWarnings("unchecked") @Test public void testAutoBindDLQwithRepublishCorrelatedConfirms() throws Exception { @@ -1661,8 +1719,11 @@ public class RabbitBinderTests extends assertThat(TestUtils.getPropertyValue(errorHandler.get(0), "confirmType", ConfirmType.class)) .isEqualTo(ConfirmType.CORRELATED); consumerBinding.unbind(); + + verifyAutoDeclareContextClear(binder); } + @SuppressWarnings("unchecked") @Test public void testBatchingAndCompression(TestInfo testInfo) throws Exception { @@ -1723,8 +1784,11 @@ public class RabbitBinderTests extends producerBinding.unbind(); consumerBinding.unbind(); + + verifyAutoDeclareContextClear(binder); } + @SuppressWarnings("unchecked") @Test public void testProducerBatching(TestInfo testInfo) throws Exception { @@ -1765,8 +1829,11 @@ public class RabbitBinderTests extends producerBinding.unbind(); consumerBinding.unbind(); + + verifyAutoDeclareContextClear(binder); } + @SuppressWarnings("unchecked") @Test public void testConsumerBatching(TestInfo testInfo) throws Exception { @@ -1803,8 +1870,11 @@ public class RabbitBinderTests extends producerBinding.unbind(); consumerBinding.unbind(); + + verifyAutoDeclareContextClear(binder); } + @SuppressWarnings("unchecked") @Test public void testInternalHeadersNotPropagated(TestInfo testInfo) throws Exception { @@ -1842,8 +1912,11 @@ public class RabbitBinderTests extends producerBinding.unbind(); consumerBinding.unbind(); admin.deleteQueue("propagate"); + + verifyAutoDeclareContextClear(binder); } + /* * Test late binding due to broker down; queues with and without DLQs, and partitioned * queues. @@ -1980,8 +2053,11 @@ public class RabbitBinderTests extends cf.destroy(); this.rabbitAvailableRule.getResource().destroy(); + + verifyAutoDeclareContextClear(binder); } + @Test public void testBadUserDeclarationsFatal() throws Exception { RabbitTestBinder binder = getBinder(); @@ -2019,8 +2095,11 @@ public class RabbitBinderTests extends binding.unbind(); } } + + verifyAutoDeclareContextClear(binder); } + @Test public void testRoutingKeyExpression(TestInfo testInfo) throws Exception { RabbitTestBinder binder = getBinder(); @@ -2062,8 +2141,46 @@ public class RabbitBinderTests extends .isEqualTo("{\"field\":\"rkeTest\"}"); producerBinding.unbind(); + + verifyAutoDeclareContextClear(binder); } + + @Test + public void testRoutingKey(TestInfo testInfo) throws Exception { + String routingKey = "static.key"; + RabbitTestBinder binder = getBinder(); + ExtendedProducerProperties producerProperties = createProducerProperties(testInfo); + producerProperties.getExtension().setRoutingKey(routingKey); + + DirectChannel output = createBindableChannel("output", + createProducerBindingProperties(producerProperties)); + output.setBeanName("rkeProducer"); + Binding producerBinding = binder.bindProducer("rke", output, + producerProperties); + + RabbitAdmin admin = new RabbitAdmin(this.rabbitAvailableRule.getResource()); + Queue queue = new AnonymousQueue(); + DirectExchange exchange = new DirectExchange("rke"); + org.springframework.amqp.core.Binding binding = BindingBuilder.bind(queue) + .to(exchange).with(routingKey); + admin.declareQueue(queue); + admin.declareBinding(binding); + + output.send(new GenericMessage<>(new Pojo("rkeTest"))); + + Object out = spyOn(queue.getName()).receive(false); + assertThat(out).isInstanceOf(byte[].class); + assertThat(new String((byte[]) out, StandardCharsets.UTF_8)) + .isEqualTo("{\"field\":\"rkeTest\"}"); + + + producerBinding.unbind(); + + verifyAutoDeclareContextClear(binder); + } + + @Test public void testRoutingKeyExpressionPartitionedAndDelay(TestInfo testInfo) throws Exception { RabbitTestBinder binder = getBinder(); @@ -2113,8 +2230,11 @@ public class RabbitBinderTests extends .isEqualTo("{\"field\":\"rkepTest\"}"); producerBinding.unbind(); + + verifyAutoDeclareContextClear(binder); } + @Test public void testPolledConsumer() throws Exception { RabbitTestBinder binder = getBinder(); @@ -2136,8 +2256,11 @@ public class RabbitBinderTests extends } assertThat(polled).isTrue(); binding.unbind(); + + verifyAutoDeclareContextClear(binder); } + @Test public void testPolledConsumerRequeue() throws Exception { RabbitTestBinder binder = getBinder(); @@ -2167,8 +2290,11 @@ public class RabbitBinderTests extends }); assertThat(polled).isTrue(); binding.unbind(); + + verifyAutoDeclareContextClear(binder); } + @Test public void testPolledConsumerWithDlq() throws Exception { RabbitTestBinder binder = getBinder(); @@ -2201,8 +2327,11 @@ public class RabbitBinderTests extends .receive("pollableDlq.group.dlq", 10_000); assertThat(deadLetter).isNotNull(); binding.unbind(); + + verifyAutoDeclareContextClear(binder); } + @Test public void testPolledConsumerWithDlqNoRetry() throws Exception { RabbitTestBinder binder = getBinder(); @@ -2233,8 +2362,11 @@ public class RabbitBinderTests extends .receive("pollableDlqNoRetry.group.dlq", 10_000); assertThat(deadLetter).isNotNull(); binding.unbind(); + + verifyAutoDeclareContextClear(binder); } + @Test public void testPolledConsumerWithDlqRePub() throws Exception { RabbitTestBinder binder = getBinder(); @@ -2263,8 +2395,11 @@ public class RabbitBinderTests extends .receive("pollableDlqRePub.group.dlq", 10_000); assertThat(deadLetter).isNotNull(); binding.unbind(); + + verifyAutoDeclareContextClear(binder); } + @Test public void testCustomBatchingStrategy(TestInfo testInfo) throws Exception { RabbitTestBinder binder = getBinder(); @@ -2300,8 +2435,11 @@ public class RabbitBinderTests extends assertThat(new String((byte[]) out)).isEqualTo("0\u0000\n1\u0000\n2\u0000\n3\u0000\n4\u0000\n"); producerBinding.unbind(); + + verifyAutoDeclareContextClear(binder); } + private SimpleMessageListenerContainer verifyContainer(Lifecycle endpoint) { SimpleMessageListenerContainer container; RetryTemplate retry; @@ -2415,6 +2553,13 @@ public class RabbitBinderTests extends return stringWriter.getBuffer().toString(); } + private void verifyAutoDeclareContextClear(RabbitTestBinder binder) { + ApplicationContext ctx = + TestUtils.getPropertyValue(binder, "binder.provisioningProvider.autoDeclareContext", + ApplicationContext.class); + assertThat(ctx.getBeansOfType(Declarable.class)).isEmpty(); + } + public static class TestPartitionKeyExtractorClass implements PartitionKeyExtractorStrategy {