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 <mkammerer@vmware.com>
Co-Authored-By: Phillip Webb <pwebb@vmware.com>
pull/34759/head
Andy Wilkinson 2 years ago
parent de8fb04814
commit 69f31cb6c0

@ -16,6 +16,8 @@
package org.springframework.boot.autoconfigure.amqp; package org.springframework.boot.autoconfigure.amqp;
import java.util.stream.Collectors;
import org.springframework.amqp.rabbit.connection.AbstractConnectionFactory; import org.springframework.amqp.rabbit.connection.AbstractConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionNameStrategy; import org.springframework.amqp.rabbit.connection.ConnectionNameStrategy;
import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.boot.context.properties.PropertyMapper;
@ -27,6 +29,9 @@ import org.springframework.util.Assert;
* *
* @param <T> the connection factory type. * @param <T> the connection factory type.
* @author Chris Bono * @author Chris Bono
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
* @since 2.6.0 * @since 2.6.0
*/ */
public abstract class AbstractConnectionFactoryConfigurer<T extends AbstractConnectionFactory> { public abstract class AbstractConnectionFactoryConfigurer<T extends AbstractConnectionFactory> {
@ -35,9 +40,31 @@ public abstract class AbstractConnectionFactoryConfigurer<T extends AbstractConn
private ConnectionNameStrategy connectionNameStrategy; private ConnectionNameStrategy connectionNameStrategy;
private final RabbitConnectionDetails connectionDetails;
/**
* 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
*/
protected AbstractConnectionFactoryConfigurer(RabbitProperties properties) { protected AbstractConnectionFactoryConfigurer(RabbitProperties properties) {
Assert.notNull(properties, "RabbitProperties must not be null"); 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
*/
protected AbstractConnectionFactoryConfigurer(RabbitProperties properties,
RabbitConnectionDetails connectionDetails) {
Assert.notNull(properties, "Properties must not be null");
Assert.notNull(connectionDetails, "ConnectionDetails must not be null");
this.rabbitProperties = properties; this.rabbitProperties = properties;
this.connectionDetails = connectionDetails;
} }
protected final ConnectionNameStrategy getConnectionNameStrategy() { protected final ConnectionNameStrategy getConnectionNameStrategy() {
@ -55,7 +82,11 @@ public abstract class AbstractConnectionFactoryConfigurer<T extends AbstractConn
public final void configure(T connectionFactory) { public final void configure(T connectionFactory) {
Assert.notNull(connectionFactory, "ConnectionFactory must not be null"); Assert.notNull(connectionFactory, "ConnectionFactory must not be null");
PropertyMapper map = PropertyMapper.get(); PropertyMapper map = PropertyMapper.get();
map.from(this.rabbitProperties::determineAddresses).to(connectionFactory::setAddresses); String addresses = this.connectionDetails.getAddresses()
.stream()
.map((address) -> address.host() + ":" + address.port())
.collect(Collectors.joining(","));
map.from(addresses).to(connectionFactory::setAddresses);
map.from(this.rabbitProperties::getAddressShuffleMode) map.from(this.rabbitProperties::getAddressShuffleMode)
.whenNonNull() .whenNonNull()
.to(connectionFactory::setAddressShuffleMode); .to(connectionFactory::setAddressShuffleMode);

@ -25,12 +25,32 @@ import org.springframework.boot.context.properties.PropertyMapper;
* Configures Rabbit {@link CachingConnectionFactory} with sensible defaults. * Configures Rabbit {@link CachingConnectionFactory} with sensible defaults.
* *
* @author Chris Bono * @author Chris Bono
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
* @since 2.6.0 * @since 2.6.0
*/ */
public class CachingConnectionFactoryConfigurer extends AbstractConnectionFactoryConfigurer<CachingConnectionFactory> { public class CachingConnectionFactoryConfigurer extends AbstractConnectionFactoryConfigurer<CachingConnectionFactory> {
/**
* 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) { 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 @Override

@ -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<Address> getAddresses() {
List<Address> 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;
}
}

@ -59,21 +59,6 @@ import org.springframework.core.io.ResourceLoader;
* <li>{@link org.springframework.amqp.core.AmqpAdmin } instance as long as * <li>{@link org.springframework.amqp.core.AmqpAdmin } instance as long as
* {@literal spring.rabbitmq.dynamic=true}.</li> * {@literal spring.rabbitmq.dynamic=true}.</li>
* </ul> * </ul>
* <p>
* The {@link org.springframework.amqp.rabbit.connection.CachingConnectionFactory} honors
* the following properties:
* <ul>
* <li>{@literal spring.rabbitmq.port} is used to specify the port to which the client
* should connect, and defaults to 5672.</li>
* <li>{@literal spring.rabbitmq.username} is used to specify the (optional) username.
* </li>
* <li>{@literal spring.rabbitmq.password} is used to specify the (optional) password.
* </li>
* <li>{@literal spring.rabbitmq.host} is used to specify the host, and defaults to
* {@literal localhost}.</li>
* <li>{@literal spring.rabbitmq.virtualHost} is used to specify the (optional) virtual
* host to which the client should connect.</li>
* </ul>
* *
* @author Greg Turnquist * @author Greg Turnquist
* @author Josh Long * @author Josh Long
@ -82,6 +67,8 @@ import org.springframework.core.io.ResourceLoader;
* @author Phillip Webb * @author Phillip Webb
* @author Artsiom Yudovin * @author Artsiom Yudovin
* @author Chris Bono * @author Chris Bono
* @author Moritz Halbritter
* @author Andy Wilkinson
* @since 1.0.0 * @since 1.0.0
*/ */
@AutoConfiguration @AutoConfiguration
@ -93,13 +80,24 @@ public class RabbitAutoConfiguration {
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
protected static class RabbitConnectionFactoryCreator { protected static class RabbitConnectionFactoryCreator {
private final RabbitProperties properties;
private final RabbitConnectionDetails connectionDetails;
protected RabbitConnectionFactoryCreator(RabbitProperties properties,
ObjectProvider<RabbitConnectionDetails> connectionDetails) {
this.properties = properties;
this.connectionDetails = connectionDetails
.getIfAvailable(() -> new PropertiesRabbitConnectionDetails(properties));
}
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
RabbitConnectionFactoryBeanConfigurer rabbitConnectionFactoryBeanConfigurer(RabbitProperties properties, RabbitConnectionFactoryBeanConfigurer rabbitConnectionFactoryBeanConfigurer(ResourceLoader resourceLoader,
ResourceLoader resourceLoader, ObjectProvider<CredentialsProvider> credentialsProvider, ObjectProvider<CredentialsProvider> credentialsProvider,
ObjectProvider<CredentialsRefreshService> credentialsRefreshService) { ObjectProvider<CredentialsRefreshService> credentialsRefreshService) {
RabbitConnectionFactoryBeanConfigurer configurer = new RabbitConnectionFactoryBeanConfigurer(resourceLoader, RabbitConnectionFactoryBeanConfigurer configurer = new RabbitConnectionFactoryBeanConfigurer(resourceLoader,
properties); this.properties, this.connectionDetails);
configurer.setCredentialsProvider(credentialsProvider.getIfUnique()); configurer.setCredentialsProvider(credentialsProvider.getIfUnique());
configurer.setCredentialsRefreshService(credentialsRefreshService.getIfUnique()); configurer.setCredentialsRefreshService(credentialsRefreshService.getIfUnique());
return configurer; return configurer;
@ -107,9 +105,10 @@ public class RabbitAutoConfiguration {
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
CachingConnectionFactoryConfigurer rabbitConnectionFactoryConfigurer(RabbitProperties rabbitProperties, CachingConnectionFactoryConfigurer rabbitConnectionFactoryConfigurer(
ObjectProvider<ConnectionNameStrategy> connectionNameStrategy) { ObjectProvider<ConnectionNameStrategy> connectionNameStrategy) {
CachingConnectionFactoryConfigurer configurer = new CachingConnectionFactoryConfigurer(rabbitProperties); CachingConnectionFactoryConfigurer configurer = new CachingConnectionFactoryConfigurer(this.properties,
this.connectionDetails);
configurer.setConnectionNameStrategy(connectionNameStrategy.getIfUnique()); configurer.setConnectionNameStrategy(connectionNameStrategy.getIfUnique());
return configurer; return configurer;
} }

@ -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<Address> getAddresses();
/**
* Returns the first address.
* @return the first address
* @throws IllegalStateException if the address list is empty
*/
default Address getFirstAddress() {
List<Address> 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) {
}
}

@ -22,6 +22,7 @@ import com.rabbitmq.client.impl.CredentialsProvider;
import com.rabbitmq.client.impl.CredentialsRefreshService; import com.rabbitmq.client.impl.CredentialsRefreshService;
import org.springframework.amqp.rabbit.connection.RabbitConnectionFactoryBean; 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.boot.context.properties.PropertyMapper;
import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.ResourceLoader;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -30,6 +31,9 @@ import org.springframework.util.Assert;
* Configures {@link RabbitConnectionFactoryBean} with sensible defaults. * Configures {@link RabbitConnectionFactoryBean} with sensible defaults.
* *
* @author Chris Bono * @author Chris Bono
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
* @since 2.6.0 * @since 2.6.0
*/ */
public class RabbitConnectionFactoryBeanConfigurer { public class RabbitConnectionFactoryBeanConfigurer {
@ -38,13 +42,39 @@ public class RabbitConnectionFactoryBeanConfigurer {
private final ResourceLoader resourceLoader; private final ResourceLoader resourceLoader;
private final RabbitConnectionDetails connectionDetails;
private CredentialsProvider credentialsProvider; private CredentialsProvider credentialsProvider;
private CredentialsRefreshService credentialsRefreshService; 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) { 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.resourceLoader = resourceLoader;
this.rabbitProperties = properties; this.rabbitProperties = properties;
this.connectionDetails = connectionDetails;
} }
public void setCredentialsProvider(CredentialsProvider credentialsProvider) { public void setCredentialsProvider(CredentialsProvider credentialsProvider) {
@ -65,12 +95,13 @@ public class RabbitConnectionFactoryBeanConfigurer {
public void configure(RabbitConnectionFactoryBean factory) { public void configure(RabbitConnectionFactoryBean factory) {
Assert.notNull(factory, "RabbitConnectionFactoryBean must not be null"); Assert.notNull(factory, "RabbitConnectionFactoryBean must not be null");
factory.setResourceLoader(this.resourceLoader); factory.setResourceLoader(this.resourceLoader);
Address address = this.connectionDetails.getFirstAddress();
PropertyMapper map = PropertyMapper.get(); PropertyMapper map = PropertyMapper.get();
map.from(this.rabbitProperties::determineHost).whenNonNull().to(factory::setHost); map.from(address::host).whenNonNull().to(factory::setHost);
map.from(this.rabbitProperties::determinePort).to(factory::setPort); map.from(address::port).to(factory::setPort);
map.from(this.rabbitProperties::determineUsername).whenNonNull().to(factory::setUsername); map.from(this.connectionDetails::getUsername).whenNonNull().to(factory::setUsername);
map.from(this.rabbitProperties::determinePassword).whenNonNull().to(factory::setPassword); map.from(this.connectionDetails::getPassword).whenNonNull().to(factory::setPassword);
map.from(this.rabbitProperties::determineVirtualHost).whenNonNull().to(factory::setVirtualHost); map.from(this.connectionDetails::getVirtualHost).whenNonNull().to(factory::setVirtualHost);
map.from(this.rabbitProperties::getRequestedHeartbeat) map.from(this.rabbitProperties::getRequestedHeartbeat)
.whenNonNull() .whenNonNull()
.asInt(Duration::getSeconds) .asInt(Duration::getSeconds)

@ -96,6 +96,9 @@ import static org.mockito.Mockito.mock;
* @author Gary Russell * @author Gary Russell
* @author HaiTao Zhang * @author HaiTao Zhang
* @author Franjo Zilic * @author Franjo Zilic
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
*/ */
@ExtendWith(OutputCaptureExtension.class) @ExtendWith(OutputCaptureExtension.class)
class RabbitAutoConfigurationTests { 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<Address> addresses = (List<Address>) ReflectionTestUtils.getField(connectionFactory, "addresses");
assertThat(addresses).containsExactly(new Address("rabbit.example.com", 12345),
new Address("rabbit2.example.com", 23456));
});
}
@Test @Test
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
void testConnectionFactoryWithCustomConnectionNameStrategy() { 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<Address> getAddresses() {
return List.of(new Address("rabbit.example.com", 12345), new Address("rabbit2.example.com", 23456));
}
};
}
}
static class TestListener { static class TestListener {
@RabbitListener(queues = "test", autoStartup = "false") @RabbitListener(queues = "test", autoStartup = "false")

Loading…
Cancel
Save