From 69f31cb6c078d6bd6811512794acb637f1470513 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 23 Mar 2023 23:20:20 -0700 Subject: [PATCH] Add ConnectionDetail support to Rabbit auto-configuration Update Rabbit auto-configuration so that `RabbitConnectionDetails` beans may be optionally used to provide connection details. See gh-34657 Co-Authored-By: Mortitz Halbritter Co-Authored-By: Phillip Webb --- .../AbstractConnectionFactoryConfigurer.java | 35 +++++++- .../CachingConnectionFactoryConfigurer.java | 22 ++++- .../PropertiesRabbitConnectionDetails.java | 63 ++++++++++++++ .../amqp/RabbitAutoConfiguration.java | 39 +++++---- .../amqp/RabbitConnectionDetails.java | 85 +++++++++++++++++++ ...RabbitConnectionFactoryBeanConfigurer.java | 41 +++++++-- .../amqp/RabbitAutoConfigurationTests.java | 55 ++++++++++++ 7 files changed, 312 insertions(+), 28 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/PropertiesRabbitConnectionDetails.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitConnectionDetails.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/AbstractConnectionFactoryConfigurer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/AbstractConnectionFactoryConfigurer.java index 3a25c78554..d5f1a12c66 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/AbstractConnectionFactoryConfigurer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/AbstractConnectionFactoryConfigurer.java @@ -16,6 +16,8 @@ package org.springframework.boot.autoconfigure.amqp; +import java.util.stream.Collectors; + import org.springframework.amqp.rabbit.connection.AbstractConnectionFactory; import org.springframework.amqp.rabbit.connection.ConnectionNameStrategy; import org.springframework.boot.context.properties.PropertyMapper; @@ -27,6 +29,9 @@ import org.springframework.util.Assert; * * @param the connection factory type. * @author Chris Bono + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb * @since 2.6.0 */ public abstract class AbstractConnectionFactoryConfigurer { @@ -35,9 +40,31 @@ public abstract class AbstractConnectionFactoryConfigurer address.host() + ":" + address.port()) + .collect(Collectors.joining(",")); + map.from(addresses).to(connectionFactory::setAddresses); map.from(this.rabbitProperties::getAddressShuffleMode) .whenNonNull() .to(connectionFactory::setAddressShuffleMode); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/CachingConnectionFactoryConfigurer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/CachingConnectionFactoryConfigurer.java index 97ab3d661b..677ddfc3c2 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/CachingConnectionFactoryConfigurer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/CachingConnectionFactoryConfigurer.java @@ -25,12 +25,32 @@ import org.springframework.boot.context.properties.PropertyMapper; * Configures Rabbit {@link CachingConnectionFactory} with sensible defaults. * * @author Chris Bono + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb * @since 2.6.0 */ public class CachingConnectionFactoryConfigurer extends AbstractConnectionFactoryConfigurer { + /** + * Creates a new configurer that will configure the connection factory using the given + * {@code properties}. + * @param properties the properties to use to configure the connection factory + */ public CachingConnectionFactoryConfigurer(RabbitProperties properties) { - super(properties); + this(properties, new PropertiesRabbitConnectionDetails(properties)); + } + + /** + * Creates a new configurer that will configure the connection factory using the given + * {@code properties} and {@code connectionDetails}, with the latter taking priority. + * @param properties the properties to use to configure the connection factory + * @param connectionDetails the connection details to use to configure the connection + * factory + * @since 3.1.0 + */ + public CachingConnectionFactoryConfigurer(RabbitProperties properties, RabbitConnectionDetails connectionDetails) { + super(properties, connectionDetails); } @Override diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/PropertiesRabbitConnectionDetails.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/PropertiesRabbitConnectionDetails.java new file mode 100644 index 0000000000..1b3359afc0 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/PropertiesRabbitConnectionDetails.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012-2023 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.boot.autoconfigure.amqp; + +import java.util.ArrayList; +import java.util.List; + +/** + * Adapts {@link RabbitProperties} to {@link RabbitConnectionDetails}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.1.0 + */ +public class PropertiesRabbitConnectionDetails implements RabbitConnectionDetails { + + private final RabbitProperties properties; + + public PropertiesRabbitConnectionDetails(RabbitProperties properties) { + this.properties = properties; + } + + @Override + public String getUsername() { + return this.properties.determineUsername(); + } + + @Override + public String getPassword() { + return this.properties.determinePassword(); + } + + @Override + public String getVirtualHost() { + return this.properties.determineVirtualHost(); + } + + @Override + public List
getAddresses() { + List
addresses = new ArrayList<>(); + for (String address : this.properties.determineAddresses().split(",")) { + String[] components = address.split(":"); + addresses.add(new Address(components[0], Integer.parseInt(components[1]))); + } + return addresses; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfiguration.java index c14a74c9eb..78477031a2 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfiguration.java @@ -59,21 +59,6 @@ import org.springframework.core.io.ResourceLoader; *
  • {@link org.springframework.amqp.core.AmqpAdmin } instance as long as * {@literal spring.rabbitmq.dynamic=true}.
  • * - *

    - * The {@link org.springframework.amqp.rabbit.connection.CachingConnectionFactory} honors - * the following properties: - *

      - *
    • {@literal spring.rabbitmq.port} is used to specify the port to which the client - * should connect, and defaults to 5672.
    • - *
    • {@literal spring.rabbitmq.username} is used to specify the (optional) username. - *
    • - *
    • {@literal spring.rabbitmq.password} is used to specify the (optional) password. - *
    • - *
    • {@literal spring.rabbitmq.host} is used to specify the host, and defaults to - * {@literal localhost}.
    • - *
    • {@literal spring.rabbitmq.virtualHost} is used to specify the (optional) virtual - * host to which the client should connect.
    • - *
    * * @author Greg Turnquist * @author Josh Long @@ -82,6 +67,8 @@ import org.springframework.core.io.ResourceLoader; * @author Phillip Webb * @author Artsiom Yudovin * @author Chris Bono + * @author Moritz Halbritter + * @author Andy Wilkinson * @since 1.0.0 */ @AutoConfiguration @@ -93,13 +80,24 @@ public class RabbitAutoConfiguration { @Configuration(proxyBeanMethods = false) protected static class RabbitConnectionFactoryCreator { + private final RabbitProperties properties; + + private final RabbitConnectionDetails connectionDetails; + + protected RabbitConnectionFactoryCreator(RabbitProperties properties, + ObjectProvider connectionDetails) { + this.properties = properties; + this.connectionDetails = connectionDetails + .getIfAvailable(() -> new PropertiesRabbitConnectionDetails(properties)); + } + @Bean @ConditionalOnMissingBean - RabbitConnectionFactoryBeanConfigurer rabbitConnectionFactoryBeanConfigurer(RabbitProperties properties, - ResourceLoader resourceLoader, ObjectProvider credentialsProvider, + RabbitConnectionFactoryBeanConfigurer rabbitConnectionFactoryBeanConfigurer(ResourceLoader resourceLoader, + ObjectProvider credentialsProvider, ObjectProvider credentialsRefreshService) { RabbitConnectionFactoryBeanConfigurer configurer = new RabbitConnectionFactoryBeanConfigurer(resourceLoader, - properties); + this.properties, this.connectionDetails); configurer.setCredentialsProvider(credentialsProvider.getIfUnique()); configurer.setCredentialsRefreshService(credentialsRefreshService.getIfUnique()); return configurer; @@ -107,9 +105,10 @@ public class RabbitAutoConfiguration { @Bean @ConditionalOnMissingBean - CachingConnectionFactoryConfigurer rabbitConnectionFactoryConfigurer(RabbitProperties rabbitProperties, + CachingConnectionFactoryConfigurer rabbitConnectionFactoryConfigurer( ObjectProvider connectionNameStrategy) { - CachingConnectionFactoryConfigurer configurer = new CachingConnectionFactoryConfigurer(rabbitProperties); + CachingConnectionFactoryConfigurer configurer = new CachingConnectionFactoryConfigurer(this.properties, + this.connectionDetails); configurer.setConnectionNameStrategy(connectionNameStrategy.getIfUnique()); return configurer; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitConnectionDetails.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitConnectionDetails.java new file mode 100644 index 0000000000..4cdc51c215 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitConnectionDetails.java @@ -0,0 +1,85 @@ +/* + * Copyright 2012-2023 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.boot.autoconfigure.amqp; + +import java.util.List; + +import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; +import org.springframework.util.Assert; + +/** + * Details required to establish a connection to a RabbitMQ service. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.1.0 + */ +public interface RabbitConnectionDetails extends ConnectionDetails { + + /** + * Login user to authenticate to the broker. + * @return the login user to authenticate to the broker or {@code null} + */ + default String getUsername() { + return null; + } + + /** + * Login to authenticate against the broker. + * @return the login to authenticate against the broker or {@code null} + */ + default String getPassword() { + return null; + } + + /** + * Virtual host to use when connecting to the broker. + * @return the virtual host to use when connecting to the broker or {@code null} + */ + default String getVirtualHost() { + return null; + } + + /** + * List of addresses to which the client should connect. Must return at least one + * address. + * @return the list of addresses to which the client should connect + */ + List
    getAddresses(); + + /** + * Returns the first address. + * @return the first address + * @throws IllegalStateException if the address list is empty + */ + default Address getFirstAddress() { + List
    addresses = getAddresses(); + Assert.state(!addresses.isEmpty(), "Address list is empty"); + return addresses.get(0); + } + + /** + * A RabbitMQ address. + * + * @param host the host + * @param port the port + */ + record Address(String host, int port) { + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitConnectionFactoryBeanConfigurer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitConnectionFactoryBeanConfigurer.java index 2c2149bc3e..f54e91ace5 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitConnectionFactoryBeanConfigurer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitConnectionFactoryBeanConfigurer.java @@ -22,6 +22,7 @@ import com.rabbitmq.client.impl.CredentialsProvider; import com.rabbitmq.client.impl.CredentialsRefreshService; import org.springframework.amqp.rabbit.connection.RabbitConnectionFactoryBean; +import org.springframework.boot.autoconfigure.amqp.RabbitConnectionDetails.Address; import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.core.io.ResourceLoader; import org.springframework.util.Assert; @@ -30,6 +31,9 @@ import org.springframework.util.Assert; * Configures {@link RabbitConnectionFactoryBean} with sensible defaults. * * @author Chris Bono + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb * @since 2.6.0 */ public class RabbitConnectionFactoryBeanConfigurer { @@ -38,13 +42,39 @@ public class RabbitConnectionFactoryBeanConfigurer { private final ResourceLoader resourceLoader; + private final RabbitConnectionDetails connectionDetails; + private CredentialsProvider credentialsProvider; private CredentialsRefreshService credentialsRefreshService; + /** + * Creates a new configurer that will use the given {@code resourceLoader} and + * {@code properties}. + * @param resourceLoader the resource loader + * @param properties the properties + */ public RabbitConnectionFactoryBeanConfigurer(ResourceLoader resourceLoader, RabbitProperties properties) { + this(resourceLoader, properties, new PropertiesRabbitConnectionDetails(properties)); + } + + /** + * Creates a new configurer that will use the given {@code resourceLoader}, + * {@code properties}, and {@code connectionDetails}. The connection details have + * priority over the properties. + * @param resourceLoader the resource loader + * @param properties the properties + * @param connectionDetails the connection details. + * @since 3.1.0 + */ + public RabbitConnectionFactoryBeanConfigurer(ResourceLoader resourceLoader, RabbitProperties properties, + RabbitConnectionDetails connectionDetails) { + Assert.notNull(resourceLoader, "ResourceLoader must not be null"); + Assert.notNull(properties, "Properties must not be null"); + Assert.notNull(connectionDetails, "ConnectionDetails must not be null"); this.resourceLoader = resourceLoader; this.rabbitProperties = properties; + this.connectionDetails = connectionDetails; } public void setCredentialsProvider(CredentialsProvider credentialsProvider) { @@ -65,12 +95,13 @@ public class RabbitConnectionFactoryBeanConfigurer { public void configure(RabbitConnectionFactoryBean factory) { Assert.notNull(factory, "RabbitConnectionFactoryBean must not be null"); factory.setResourceLoader(this.resourceLoader); + Address address = this.connectionDetails.getFirstAddress(); PropertyMapper map = PropertyMapper.get(); - map.from(this.rabbitProperties::determineHost).whenNonNull().to(factory::setHost); - map.from(this.rabbitProperties::determinePort).to(factory::setPort); - map.from(this.rabbitProperties::determineUsername).whenNonNull().to(factory::setUsername); - map.from(this.rabbitProperties::determinePassword).whenNonNull().to(factory::setPassword); - map.from(this.rabbitProperties::determineVirtualHost).whenNonNull().to(factory::setVirtualHost); + map.from(address::host).whenNonNull().to(factory::setHost); + map.from(address::port).to(factory::setPort); + map.from(this.connectionDetails::getUsername).whenNonNull().to(factory::setUsername); + map.from(this.connectionDetails::getPassword).whenNonNull().to(factory::setPassword); + map.from(this.connectionDetails::getVirtualHost).whenNonNull().to(factory::setVirtualHost); map.from(this.rabbitProperties::getRequestedHeartbeat) .whenNonNull() .asInt(Duration::getSeconds) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java index 9244a0c097..6c3352c96a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java @@ -96,6 +96,9 @@ import static org.mockito.Mockito.mock; * @author Gary Russell * @author HaiTao Zhang * @author Franjo Zilic + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb */ @ExtendWith(OutputCaptureExtension.class) class RabbitAutoConfigurationTests { @@ -169,6 +172,26 @@ class RabbitAutoConfigurationTests { }); } + @Test + @SuppressWarnings("unchecked") + void testConnectionFactoryWithOverridesWhenUsingConnectionDetails() { + this.contextRunner.withUserConfiguration(TestConfiguration.class, ConnectionDetailsConfiguration.class) + .withPropertyValues("spring.rabbitmq.host:remote-server", "spring.rabbitmq.port:9000", + "spring.rabbitmq.username:alice", "spring.rabbitmq.password:secret", + "spring.rabbitmq.virtual_host:/vhost") + .run((context) -> { + CachingConnectionFactory connectionFactory = context.getBean(CachingConnectionFactory.class); + assertThat(connectionFactory.getHost()).isEqualTo("rabbit.example.com"); + assertThat(connectionFactory.getPort()).isEqualTo(12345); + assertThat(connectionFactory.getVirtualHost()).isEqualTo("/vhost-1"); + assertThat(connectionFactory.getUsername()).isEqualTo("user-1"); + assertThat(connectionFactory.getRabbitConnectionFactory().getPassword()).isEqualTo("password-1"); + List
    addresses = (List
    ) ReflectionTestUtils.getField(connectionFactory, "addresses"); + assertThat(addresses).containsExactly(new Address("rabbit.example.com", 12345), + new Address("rabbit2.example.com", 23456)); + }); + } + @Test @SuppressWarnings("unchecked") void testConnectionFactoryWithCustomConnectionNameStrategy() { @@ -1218,6 +1241,38 @@ class RabbitAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class ConnectionDetailsConfiguration { + + @Bean + RabbitConnectionDetails rabbitConnectionDetails() { + return new RabbitConnectionDetails() { + + @Override + public String getUsername() { + return "user-1"; + } + + @Override + public String getPassword() { + return "password-1"; + } + + @Override + public String getVirtualHost() { + return "/vhost-1"; + } + + @Override + public List
    getAddresses() { + return List.of(new Address("rabbit.example.com", 12345), new Address("rabbit2.example.com", 23456)); + } + + }; + } + + } + static class TestListener { @RabbitListener(queues = "test", autoStartup = "false")