From e3a124d0f9c8a819b9148dcdfb4f5ce5a6efd31f Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 26 May 2015 21:32:58 +0200 Subject: [PATCH] Expose additional RabbitMQ settings Allow SSL to be configured via standard configuration as well as the requestedHeartbeat. Switch to RabbitConnectionFactoryBean. Closes gh-2655, gh-2676 --- .../amqp/RabbitAutoConfiguration.java | 33 ++++- .../autoconfigure/amqp/RabbitProperties.java | 117 +++++++++++++++++- .../amqp/RabbitAutoConfigurationTests.java | 47 ++++++- .../appendix-application-properties.adoc | 14 ++- 4 files changed, 199 insertions(+), 12 deletions(-) diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfiguration.java index b9b30d44ab..53ae47e1cc 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2014 the original author or authors. + * Copyright 2012-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,13 @@ package org.springframework.boot.autoconfigure.amqp; +import java.io.ByteArrayOutputStream; +import java.util.Properties; + import org.springframework.amqp.core.AmqpAdmin; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.connection.RabbitConnectionFactoryBean; import org.springframework.amqp.rabbit.core.RabbitAdmin; import org.springframework.amqp.rabbit.core.RabbitMessagingTemplate; import org.springframework.amqp.rabbit.core.RabbitTemplate; @@ -31,6 +35,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.core.io.ByteArrayResource; import com.rabbitmq.client.Channel; @@ -72,6 +77,7 @@ import com.rabbitmq.client.Channel; * * @author Greg Turnquist * @author Josh Long + * @author Stephane Nicoll */ @Configuration @ConditionalOnClass({ RabbitTemplate.class, Channel.class }) @@ -100,10 +106,8 @@ public class RabbitAutoConfiguration { protected static class RabbitConnectionFactoryCreator { @Bean - public ConnectionFactory rabbitConnectionFactory(RabbitProperties config) { - CachingConnectionFactory factory = new CachingConnectionFactory(); - String addresses = config.getAddresses(); - factory.setAddresses(addresses); + public ConnectionFactory rabbitConnectionFactory(RabbitProperties config) throws Exception { + RabbitConnectionFactoryBean factory = new RabbitConnectionFactoryBean(); if (config.getHost() != null) { factory.setHost(config.getHost()); factory.setPort(config.getPort()); @@ -117,7 +121,24 @@ public class RabbitAutoConfiguration { if (config.getVirtualHost() != null) { factory.setVirtualHost(config.getVirtualHost()); } - return factory; + if (config.getRequestedHeartbeat() != null) { + factory.setRequestedHeartbeat(config.getRequestedHeartbeat()); + } + RabbitProperties.Ssl ssl = config.getSsl(); + if (ssl.isEnabled()) { + factory.setUseSSL(true); + if (ssl.getKeyStore() != null || ssl.getTrustStore() != null) { + Properties properties = ssl.createSslProperties(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + properties.store(outputStream, "SSL config"); + factory.setSslPropertiesLocation( + new ByteArrayResource(outputStream.toByteArray())); + } + } + factory.afterPropertiesSet(); + CachingConnectionFactory connectionFactory = new CachingConnectionFactory(factory.getObject()); + connectionFactory.setAddresses(config.getAddresses()); + return connectionFactory; } } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitProperties.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitProperties.java index 72b2e58552..2c29915b6c 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitProperties.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2014 the original author or authors. + * Copyright 2012-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.boot.autoconfigure.amqp; import java.util.LinkedHashSet; +import java.util.Properties; import java.util.Set; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -27,6 +28,7 @@ import org.springframework.util.StringUtils; * * @author Greg Turnquist * @author Dave Syer + * @author Stephane Nicoll */ @ConfigurationProperties(prefix = "spring.rabbitmq") public class RabbitProperties { @@ -51,6 +53,11 @@ public class RabbitProperties { */ private String password; + /** + * SSL configuration. + */ + private final Ssl ssl = new Ssl(); + /** * Virtual host to use when connecting to the broker. */ @@ -61,6 +68,12 @@ public class RabbitProperties { */ private String addresses; + /** + * Requested heartbeat timeout, in seconds; zero for none. + */ + private Integer requestedHeartbeat; + + public String getHost() { if (this.addresses == null) { return this.host; @@ -147,6 +160,10 @@ public class RabbitProperties { this.password = password; } + public Ssl getSsl() { + return ssl; + } + public String getVirtualHost() { return this.virtualHost; } @@ -155,4 +172,102 @@ public class RabbitProperties { this.virtualHost = ("".equals(virtualHost) ? "/" : virtualHost); } + public Integer getRequestedHeartbeat() { + return requestedHeartbeat; + } + + public void setRequestedHeartbeat(Integer requestedHeartbeat) { + this.requestedHeartbeat = requestedHeartbeat; + } + + public static class Ssl { + + /** + * Enable SSL support. + */ + private boolean enabled; + + /** + * Path to the key store that holds the SSL certificate. + */ + private String keyStore; + + /** + * Password used to access the key store. + */ + private String keyStorePassword; + + /** + * Trust store that holds SSL certificates. + */ + private String trustStore; + + /** + * Password used to access the trust store. + */ + private String trustStorePassword; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getKeyStore() { + return keyStore; + } + + public void setKeyStore(String keyStore) { + this.keyStore = keyStore; + } + + public String getKeyStorePassword() { + return keyStorePassword; + } + + public void setKeyStorePassword(String keyStorePassword) { + this.keyStorePassword = keyStorePassword; + } + + public String getTrustStore() { + return trustStore; + } + + public void setTrustStore(String trustStore) { + this.trustStore = trustStore; + } + + public String getTrustStorePassword() { + return trustStorePassword; + } + + public void setTrustStorePassword(String trustStorePassword) { + this.trustStorePassword = trustStorePassword; + } + + /** + * Create the ssl configuration as expected by the + * {@link org.springframework.amqp.rabbit.connection.RabbitConnectionFactoryBean RabbitConnectionFactoryBean}. + * @return the ssl configuration + */ + public Properties createSslProperties() { + Properties properties = new Properties(); + if (getKeyStore() != null) { + properties.put("keyStore", getKeyStore()); + } + if (getKeyStorePassword() != null) { + properties.put("keyStore.passPhrase", getKeyStorePassword()); + } + if (getTrustStore() != null) { + properties.put("trustStore", getTrustStore()); + } + if (getTrustStorePassword() != null) { + properties.put("trustStore.passPhrase", getTrustStorePassword()); + } + return properties; + } + + } } diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java index bd1cf77667..6c4bc97611 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2014 the original author or authors. + * Copyright 2012-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,9 @@ package org.springframework.boot.autoconfigure.amqp; +import javax.net.SocketFactory; +import javax.net.ssl.SSLSocketFactory; + import org.junit.After; import org.junit.Rule; import org.junit.Test; @@ -31,6 +34,7 @@ import org.springframework.amqp.rabbit.core.RabbitMessagingTemplate; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory; import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.beans.DirectFieldAccessor; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.boot.test.EnvironmentTestUtils; import org.springframework.context.annotation.AnnotationConfigApplicationContext; @@ -47,6 +51,7 @@ import static org.mockito.Mockito.verify; * Tests for {@link RabbitAutoConfiguration}. * * @author Greg Turnquist + * @author Stephane Nicoll */ public class RabbitAutoConfigurationTests { @@ -188,6 +193,46 @@ public class RabbitAutoConfigurationTests { ctx.getBean(RabbitListenerConfigUtils.RABBIT_LISTENER_ENDPOINT_REGISTRY_BEAN_NAME); } + @Test + public void customizeRequestedHeartBeat() { + load(TestConfiguration.class, "spring.rabbitmq.requestedHeartbeat:20"); + com.rabbitmq.client.ConnectionFactory rabbitConnectionFactory = getTargetConnectionFactory(); + assertEquals(20, rabbitConnectionFactory.getRequestedHeartbeat()); + } + + @Test + public void noSslByDefault() { + load(TestConfiguration.class); + com.rabbitmq.client.ConnectionFactory rabbitConnectionFactory = getTargetConnectionFactory(); + assertEquals("Must use default SocketFactory", SocketFactory.getDefault(), + rabbitConnectionFactory.getSocketFactory()); + } + + @Test + public void enableSsl() { + load(TestConfiguration.class, "spring.rabbitmq.ssl.enabled:true"); + com.rabbitmq.client.ConnectionFactory rabbitConnectionFactory = getTargetConnectionFactory(); + assertTrue("SocketFactory must use SSL", rabbitConnectionFactory.getSocketFactory() instanceof SSLSocketFactory); + } + + @Test // Make sure that we at least attempt to load the store + public void enableSslWithExtraConfig() { + thrown.expectMessage("foo"); + thrown.expectMessage("does not exist"); + load(TestConfiguration.class, "spring.rabbitmq.ssl.enabled:true", + "spring.rabbitmq.ssl.keyStore=foo", + "spring.rabbitmq.ssl.keyStorePassword=secret", + "spring.rabbitmq.ssl.trustStore=bar", + "spring.rabbitmq.ssl.trustStorePassword=secret"); + } + + private com.rabbitmq.client.ConnectionFactory getTargetConnectionFactory() { + CachingConnectionFactory connectionFactory = this.context + .getBean(CachingConnectionFactory.class); + return (com.rabbitmq.client.ConnectionFactory) + new DirectFieldAccessor(connectionFactory).getPropertyValue("rabbitConnectionFactory"); + } + private void load(Class config, String... environment) { this.context = doLoad(new Class[] { config }, environment); } diff --git a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc index 203069796f..517de871e3 100644 --- a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -428,13 +428,19 @@ content into your application; rather pick only the properties that you need. spring.jmx.enabled=true # Expose MBeans from Spring # RABBIT ({sc-spring-boot-autoconfigure}/amqp/RabbitProperties.{sc-ext}[RabbitProperties]) + spring.rabbitmq.addresses= # connection addresses (e.g. myhost:9999,otherhost:1111) + spring.rabbitmq.dynamic=true # create an AmqpAdmin bean spring.rabbitmq.host= # connection host spring.rabbitmq.port= # connection port - spring.rabbitmq.addresses= # connection addresses (e.g. myhost:9999,otherhost:1111) - spring.rabbitmq.username= # login user spring.rabbitmq.password= # login password - spring.rabbitmq.virtual-host= - spring.rabbitmq.dynamic= + spring.rabbitmq.requested-heartbeat= # requested heartbeat timeout, in seconds; zero for none + spring.rabbitmq.ssl.enabled=false # enable SSL support + spring.rabbitmq.ssl.key-store= # path to the key store that holds the SSL certificate + spring.rabbitmq.ssl.key-store-password= # password used to access the key store + spring.rabbitmq.ssl.trust-store= # trust store that holds SSL certificates + spring.rabbitmq.ssl.trust-store-password= # password used to access the trust store + spring.rabbitmq.username= # login user + spring.rabbitmq.virtual-host= # virtual host to use when connecting to the broker # REDIS ({sc-spring-boot-autoconfigure}/redis/RedisProperties.{sc-ext}[RedisProperties]) spring.redis.database= # database name