GH-1074: Admin explicitDeclarationsOnly Property

Resolves https://github.com/spring-projects/spring-amqp/issues/1074

Cherry-pick to 2.1.x
This commit is contained in:
Gary Russell
2019-08-22 12:37:02 -04:00
committed by Artem Bilan
parent 4b19b61d1e
commit accd202a1a
8 changed files with 138 additions and 32 deletions

View File

@@ -73,5 +73,7 @@ class AdminParser extends AbstractSingleBeanDefinitionParser {
}
NamespaceUtils.setValueIfAttributeDefined(builder, element, IGNORE_DECLARATION_EXCEPTIONS);
NamespaceUtils.setValueIfAttributeDefined(builder, element, "explicit-declarations-only");
}
}

View File

@@ -138,6 +138,8 @@ public class RabbitAdmin implements AmqpAdmin, ApplicationContextAware, Applicat
private TaskExecutor taskExecutor = new SimpleAsyncTaskExecutor();
private boolean explicitDeclarationsOnly;
private volatile boolean running = false;
private volatile DeclarationExceptionEvent lastDeclarationExceptionEvent;
@@ -429,6 +431,17 @@ public class RabbitAdmin implements AmqpAdmin, ApplicationContextAware, Applicat
});
}
/**
* Set to true to only declare {@link Declarable} beans that are explicitly configured
* to be declared by this admin.
* @param explicitDeclarationsOnly true to ignore beans with no admin declaration
* configuration.
* @since 2.1.9
*/
public void setExplicitDeclarationsOnly(boolean explicitDeclarationsOnly) {
this.explicitDeclarationsOnly = explicitDeclarationsOnly;
}
/**
* Set a retry template for auto declarations. There is a race condition with
* auto-delete, exclusive queues in that the queue might still exist for a short time,
@@ -622,12 +635,16 @@ public class RabbitAdmin implements AmqpAdmin, ApplicationContextAware, Applicat
*/
private <T extends Declarable> Collection<T> filterDeclarables(Collection<T> declarables) {
return declarables.stream()
.filter(d -> d.shouldDeclare() // NOSONAR boolean complexity
&& (d.getDeclaringAdmins().isEmpty() || d.getDeclaringAdmins().contains(this)
|| (this.beanName != null && d.getDeclaringAdmins().contains(this.beanName))))
.filter(dec -> dec.shouldDeclare() && declarableByMe(dec))
.collect(Collectors.toList());
}
private <T extends Declarable> boolean declarableByMe(T dec) {
return (dec.getDeclaringAdmins().isEmpty() && !this.explicitDeclarationsOnly) // NOSONAR boolean complexity
|| dec.getDeclaringAdmins().contains(this)
|| (this.beanName != null && dec.getDeclaringAdmins().contains(this.beanName));
}
// private methods for declaring Exchanges, Queues, and Bindings on a Channel
private void declareExchanges(final Channel channel, final Exchange... exchanges) throws IOException {

View File

@@ -1069,6 +1069,19 @@
<xsd:union memberTypes="xsd:boolean xsd:string" />
</xsd:simpleType>
</xsd:attribute>
<xsd:attribute name="explicit-declarations-only" default="false">
<xsd:annotation>
<xsd:documentation><![CDATA[
If automatic declaration is enabled (see 'auto-startup'), if this is set to 'true', only beans
(queues, exchanges, bindings) that are explicity configured to be declared by this admin (see
'declared-by') will be declared.
Default value is 'false' which means all beans with no explicit 'declared-by' will be declared.
]]></xsd:documentation>
</xsd:annotation>
<xsd:simpleType>
<xsd:union memberTypes="xsd:boolean xsd:string" />
</xsd:simpleType>
</xsd:attribute>
</xsd:complexType>
</xsd:element>

View File

@@ -26,6 +26,7 @@ import org.junit.Test;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.utils.test.TestUtils;
import org.springframework.beans.factory.parsing.BeanDefinitionParsingException;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
@@ -56,20 +57,28 @@ public final class AdminParserTests {
private boolean initialisedWithTemplate;
@Test
public void testInvalid() throws Exception {
contextIndex = 1;
validContext = false;
doTest();
public void testValid0() throws Exception {
this.expectedAutoStartup = true;
this.contextIndex = 0;
this.validContext = true;
doTest(false);
}
@Test
public void testValid() throws Exception {
contextIndex = 2;
validContext = true;
doTest();
public void testInvalid1() throws Exception {
this.contextIndex = 1;
this.validContext = false;
doTest(false);
}
private void doTest() throws Exception {
@Test
public void testValid2() throws Exception {
this.contextIndex = 2;
this.validContext = true;
doTest(true);
}
private void doTest(boolean explicit) throws Exception {
// Create context
DefaultListableBeanFactory beanFactory = loadContext();
if (beanFactory == null) {
@@ -79,19 +88,20 @@ public final class AdminParserTests {
// Validate values
RabbitAdmin admin;
if (StringUtils.hasText(adminBeanName)) {
admin = beanFactory.getBean(adminBeanName, RabbitAdmin.class);
if (StringUtils.hasText(this.adminBeanName)) {
admin = beanFactory.getBean(this.adminBeanName, RabbitAdmin.class);
}
else {
admin = beanFactory.getBean(RabbitAdmin.class);
}
assertThat(admin.isAutoStartup()).isEqualTo(expectedAutoStartup);
assertThat(admin.getRabbitTemplate().getConnectionFactory()).isEqualTo(beanFactory.getBean(ConnectionFactory.class));
assertThat(admin.isAutoStartup()).isEqualTo(this.expectedAutoStartup);
assertThat(admin.getRabbitTemplate().getConnectionFactory())
.isEqualTo(beanFactory.getBean(ConnectionFactory.class));
if (initialisedWithTemplate) {
if (this.initialisedWithTemplate) {
assertThat(admin.getRabbitTemplate()).isEqualTo(beanFactory.getBean(RabbitTemplate.class));
}
assertThat(TestUtils.getPropertyValue(admin, "explicitDeclarationsOnly", Boolean.class)).isEqualTo(explicit);
}
/**
@@ -107,12 +117,12 @@ public final class AdminParserTests {
beanFactory = new DefaultListableBeanFactory();
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory);
reader.loadBeanDefinitions(resource);
if (!validContext) {
if (!this.validContext) {
fail("Context " + resource + " failed to load");
}
}
catch (BeanDefinitionParsingException e) {
if (validContext) {
if (this.validContext) {
// Context expected to be valid - throw an exception up
throw e;
}

View File

@@ -25,6 +25,7 @@ import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.BDDMockito.willReturn;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
@@ -261,16 +262,31 @@ public class RabbitAdminDeclarationTests {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
Config.listener1.onCreate(Config.conn1);
verify(Config.channel1).queueDeclare("foo", true, false, false, new HashMap<>());
verify(Config.channel1, never()).queueDeclare("baz", true, false, false, new HashMap<>());
verify(Config.channel1).queueDeclare("qux", true, false, false, new HashMap<>());
verify(Config.channel1).exchangeDeclare("bar", "direct", true, false, true, new HashMap<String, Object>());
verify(Config.channel1).queueBind("foo", "bar", "foo", null);
Config.listener2.onCreate(Config.conn2);
verify(Config.channel2, never())
.queueDeclare(eq("foo"), anyBoolean(), anyBoolean(), anyBoolean(), isNull());
verify(Config.channel1, never()).queueDeclare("baz", true, false, false, new HashMap<>());
verify(Config.channel2).queueDeclare("qux", true, false, false, new HashMap<>());
verify(Config.channel2, never())
.exchangeDeclare(eq("bar"), eq("direct"), anyBoolean(), anyBoolean(),
anyBoolean(), anyMap());
verify(Config.channel2, never()).queueBind(eq("foo"), eq("bar"), eq("foo"), anyMap());
Config.listener3.onCreate(Config.conn3);
verify(Config.channel3, never())
.queueDeclare(eq("foo"), anyBoolean(), anyBoolean(), anyBoolean(), isNull());
verify(Config.channel3).queueDeclare("baz", true, false, false, new HashMap<>());
verify(Config.channel3, never()).queueDeclare("qux", true, false, false, new HashMap<>());
verify(Config.channel3, never())
.exchangeDeclare(eq("bar"), eq("direct"), anyBoolean(), anyBoolean(),
anyBoolean(), anyMap());
verify(Config.channel3, never()).queueBind(eq("foo"), eq("bar"), eq("foo"), anyMap());
context.close();
}
@@ -334,21 +350,28 @@ public class RabbitAdminDeclarationTests {
private static Connection conn2 = mock(Connection.class);
private static Connection conn3 = mock(Connection.class);
private static Channel channel1 = mock(Channel.class);
private static Channel channel2 = mock(Channel.class);
private static Channel channel3 = mock(Channel.class);
private static ConnectionListener listener1;
private static ConnectionListener listener2;
private static ConnectionListener listener3;
@Bean
public ConnectionFactory cf1() throws IOException {
ConnectionFactory connectionFactory = mock(ConnectionFactory.class);
when(connectionFactory.createConnection()).thenReturn(conn1);
when(conn1.createChannel(false)).thenReturn(channel1);
when(channel1.queueDeclare("foo", true, false, false, new HashMap<>()))
.thenReturn(new AMQImpl.Queue.DeclareOk("foo", 0, 0));
willAnswer(inv -> {
return new AMQImpl.Queue.DeclareOk(inv.getArgument(0), 0, 0);
}).given(channel1).queueDeclare(anyString(), anyBoolean(), anyBoolean(), anyBoolean(), any());
doAnswer(invocation -> {
listener1 = invocation.getArgument(0);
return null;
@@ -361,8 +384,9 @@ public class RabbitAdminDeclarationTests {
ConnectionFactory connectionFactory = mock(ConnectionFactory.class);
when(connectionFactory.createConnection()).thenReturn(conn2);
when(conn2.createChannel(false)).thenReturn(channel2);
when(channel2.queueDeclare("foo", true, false, false, null))
.thenReturn(new AMQImpl.Queue.DeclareOk("foo", 0, 0));
willAnswer(inv -> {
return new AMQImpl.Queue.DeclareOk(inv.getArgument(0), 0, 0);
}).given(channel2).queueDeclare(anyString(), anyBoolean(), anyBoolean(), anyBoolean(), any());
doAnswer(invocation -> {
listener2 = invocation.getArgument(0);
return null;
@@ -370,27 +394,59 @@ public class RabbitAdminDeclarationTests {
return connectionFactory;
}
@Bean
public ConnectionFactory cf3() throws IOException {
ConnectionFactory connectionFactory = mock(ConnectionFactory.class);
when(connectionFactory.createConnection()).thenReturn(conn3);
when(conn3.createChannel(false)).thenReturn(channel3);
willAnswer(inv -> {
return new AMQImpl.Queue.DeclareOk(inv.getArgument(0), 0, 0);
}).given(channel3).queueDeclare(anyString(), anyBoolean(), anyBoolean(), anyBoolean(), any());
doAnswer(invocation -> {
listener3 = invocation.getArgument(0);
return null;
}).when(connectionFactory).addConnectionListener(any(ConnectionListener.class));
return connectionFactory;
}
@Bean
public RabbitAdmin admin1() throws IOException {
RabbitAdmin rabbitAdmin = new RabbitAdmin(cf1());
rabbitAdmin.afterPropertiesSet();
return rabbitAdmin;
}
@Bean
public RabbitAdmin admin2() throws IOException {
RabbitAdmin rabbitAdmin = new RabbitAdmin(cf2());
rabbitAdmin.afterPropertiesSet();
return rabbitAdmin;
}
@Bean
public Queue queue() throws IOException {
public RabbitAdmin admin3() throws IOException {
RabbitAdmin rabbitAdmin = new RabbitAdmin(cf3());
rabbitAdmin.setExplicitDeclarationsOnly(true);
return rabbitAdmin;
}
@Bean
public Queue queueFoo() throws IOException {
Queue queue = new Queue("foo");
queue.setAdminsThatShouldDeclare(admin1());
return queue;
}
@Bean
public Queue queueBaz() throws IOException {
Queue queue = new Queue("baz");
queue.setAdminsThatShouldDeclare(admin3());
return queue;
}
@Bean
public Queue queueQux() {
return new Queue("qux");
}
@Bean
public Exchange exchange() throws IOException {
DirectExchange exchange = new DirectExchange("bar");

View File

@@ -8,6 +8,7 @@
<rabbit:connection-factory id="connectionFactory" host="localhost" />
<!-- Valid configuration -->
<rabbit:admin id="admin-test" connection-factory="connectionFactory" auto-startup="false"/>
<rabbit:admin id="admin-test" connection-factory="connectionFactory" auto-startup="false"
explicit-declarations-only="true"/>
</beans>

View File

@@ -4611,6 +4611,8 @@ public SimpleMessageListenerContainer container(ConnectionFactory connectionFact
By default, all queues, exchanges, and bindings are declared by all `RabbitAdmin` instances (assuming they have `auto-startup="true"`) in the application context.
Starting with version 2.1.9, the `RabbitAdmin` has a new property `explicitDeclarationsOnly` (which is `false` by default); when this is set to `true`, the admin will only declare beans that are explicitly configured to be declared by that admin.
NOTE: Starting with the 1.2 release, you can conditionally declare these elements.
This is particularly useful when an application connects to multiple brokers and needs to specify with which brokers a particular element should be declared.
@@ -4626,13 +4628,15 @@ The properties are available as attributes in the namespace, as shown in the fol
<rabbit:admin id="admin2" connection-factory="CF2" />
<rabbit:queue id="declaredByBothAdminsImplicitly" />
<rabbit:admin id="admin3" connection-factory="CF3" explicit-declarations-only="true" />
<rabbit:queue id="declaredByBothAdmins" declared-by="admin1, admin2" />
<rabbit:queue id="declaredByAdmin1AndAdmin2Implicitly" />
<rabbit:queue id="declaredByAdmin1AndAdmin2" declared-by="admin1, admin2" />
<rabbit:queue id="declaredByAdmin1Only" declared-by="admin1" />
<rabbit:queue id="notDeclaredByAny" auto-declare="false" />
<rabbit:queue id="notDeclaredByAllExceptAdmin3" auto-declare="false" />
<rabbit:direct-exchange name="direct" declared-by="admin1, admin2">
<rabbit:bindings>
@@ -4642,7 +4646,7 @@ The properties are available as attributes in the namespace, as shown in the fol
----
====
NOTE: By default, the `auto-declare` attribute is `true` and, if the `declared-by` is not supplied (or is empty), then all `RabbitAdmin` instances declare the object (as long as the admin's `auto-startup` attribute is `true`, the default).
NOTE: By default, the `auto-declare` attribute is `true` and, if the `declared-by` is not supplied (or is empty), then all `RabbitAdmin` instances declare the object (as long as the admin's `auto-startup` attribute is `true`, the default, and the admin's `explicit-declarations-only` attribute is false).
Similarly, you can use Java-based `@Configuration` to achieve the same effect.
In the following example, the components are declared by `admin1` but not by`admin2`:

View File

@@ -75,6 +75,9 @@ See <<message-listener-adapter>> for more information.
The `ExchangeBuilder` and `QueueBuilder` fluent APIs used to create `Exchange` and `Queue` objects for declaration by `RabbitAdmin` now support "well known" arguments.
See <<builder-api>> for more information.
The `RabbitAdmin` has a new property `explicitDeclarationsOnly`.
See <<conditional-declaration>> for more information.
===== Connection Factory Changes
The `CachingConnectionFactory` has a new property `shuffleAddresses`.