From 941d163709060d837becb21879e7d802ae4038a6 Mon Sep 17 00:00:00 2001 From: Greg Turnquist Date: Wed, 25 Sep 2013 10:31:03 -0500 Subject: [PATCH] Add support for Spring Rabbit (via Spring AMQP) to Boot - If RabbitTemplate is on the classpath, turn on autodetection. - Create a RabbitTemplate, a Rabbit ConnectionFactory, and a RabbitAdmin is spring.rabbitmq.dynamic:true - Enable some **spring.rabbitmq** properties like host, port, username, password, and dynamic - Add tests to verify functionality - Add Groovy CLI functionality. Base it on @EnableRabbitMessaging. Add spring-amqp to the path. - Create rabbit.groovy test to prove it all works. - Make Queue and TopicExchange top-level Spring beans in rabbit.groovy test script --- spring-boot-autoconfigure/pom.xml | 5 + .../amqp/RabbitTemplateAutoConfiguration.java | 140 ++++++++++++++++++ .../main/resources/META-INF/spring.factories | 1 + .../RabbitTemplateAutoconfigurationTests.java | 106 +++++++++++++ spring-boot-cli/samples/rabbit.groovy | 64 ++++++++ .../RabbitCompilerAutoConfiguration.java | 67 +++++++++ ...oot.cli.compiler.CompilerAutoConfiguration | 1 + .../resources/META-INF/springcli.properties | 1 + .../boot/cli/SampleIntegrationTests.java | 15 +- spring-boot-dependencies/pom.xml | 6 + 10 files changed, 401 insertions(+), 5 deletions(-) create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitTemplateAutoConfiguration.java create mode 100644 spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitTemplateAutoconfigurationTests.java create mode 100644 spring-boot-cli/samples/rabbit.groovy create mode 100644 spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/autoconfigure/RabbitCompilerAutoConfiguration.java diff --git a/spring-boot-autoconfigure/pom.xml b/spring-boot-autoconfigure/pom.xml index 309b5d597b4..1b0eead1286 100644 --- a/spring-boot-autoconfigure/pom.xml +++ b/spring-boot-autoconfigure/pom.xml @@ -116,6 +116,11 @@ spring-security-acl true + + org.springframework.amqp + spring-rabbit + true + org.thymeleaf thymeleaf diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitTemplateAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitTemplateAutoConfiguration.java new file mode 100644 index 00000000000..e05aee56981 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitTemplateAutoConfiguration.java @@ -0,0 +1,140 @@ +/* + * Copyright 2012-2013 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 + * + * http://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 org.springframework.amqp.core.AmqpAdmin; +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitAdmin; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for {@link RabbitTemplate}. + * + * @author Greg Turnquist + */ +@Configuration +@ConditionalOnClass({ RabbitTemplate.class }) +@EnableConfigurationProperties +public class RabbitTemplateAutoConfiguration { + + @Bean + @ConditionalOnExpression("${spring.rabbitmq.dynamic:true}") + @ConditionalOnMissingBean(AmqpAdmin.class) + public AmqpAdmin amqpAdmin(CachingConnectionFactory connectionFactory) { + return new RabbitAdmin(connectionFactory); + } + + @Configuration + @ConditionalOnMissingBean(RabbitTemplate.class) + protected static class RabbitTemplateCreator { + + @Autowired + CachingConnectionFactory connectionFactory; + + @Bean + public RabbitTemplate rabbitTemplate() { + return new RabbitTemplate(this.connectionFactory); + } + + } + + @Configuration + @ConditionalOnMissingBean(CachingConnectionFactory.class) + @EnableConfigurationProperties(RabbitConnectionFactoryProperties.class) + protected static class RabbitConnectionFactoryCreator { + + @Autowired + private RabbitConnectionFactoryProperties config; + + @Bean + public CachingConnectionFactory connectionFactory() { + CachingConnectionFactory connectionFactory = new CachingConnectionFactory(this.config.getHost()); + connectionFactory.setPort(this.config.getPort()); + if (this.config.getUsername() != null) { + connectionFactory.setUsername(this.config.getUsername()); + } + if (this.config.getPassword() != null) { + connectionFactory.setPassword(this.config.getPassword()); + } + return connectionFactory; + } + } + + @ConfigurationProperties(name = "spring.rabbitmq") + public static class RabbitConnectionFactoryProperties { + + private String host = "localhost"; + + private int port = 5672; + + private String username; + + private String password; + + private boolean dynamic = true; + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public boolean isDynamic() { + return dynamic; + } + + public void setDynamic(boolean dynamic) { + this.dynamic = dynamic; + } + + } +} diff --git a/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index f150d12c560..e11abf2556d 100644 --- a/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -1,6 +1,7 @@ # Auto Configure org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\ +org.springframework.boot.autoconfigure.amqp.RabbitTemplateAutoConfiguration,\ org.springframework.boot.autoconfigure.MessageSourceAutoConfiguration,\ org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration,\ org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\ diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitTemplateAutoconfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitTemplateAutoconfigurationTests.java new file mode 100644 index 00000000000..4e0ebdf394b --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitTemplateAutoconfigurationTests.java @@ -0,0 +1,106 @@ +/* + * Copyright 2012-2013 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 + * + * http://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 org.junit.Test; +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.core.RabbitAdmin; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.boot.TestUtils; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.fail; + +/** + * Tests for {@link RabbitTemplateAutoConfiguration}. + * + * @author Greg Turnquist + */ +public class RabbitTemplateAutoconfigurationTests { + + private AnnotationConfigApplicationContext context; + + @Test + public void testDefaultRabbitTemplate() { + this.context = new AnnotationConfigApplicationContext(); + this.context.register(TestConfiguration.class, RabbitTemplateAutoConfiguration.class); + this.context.refresh(); + RabbitTemplate rabbitTemplate = this.context.getBean(RabbitTemplate.class); + CachingConnectionFactory connectionFactory = this.context.getBean(CachingConnectionFactory.class); + RabbitAdmin amqpAdmin = this.context.getBean(RabbitAdmin.class); + assertNotNull(rabbitTemplate); + assertNotNull(connectionFactory); + assertNotNull(amqpAdmin); + assertEquals(rabbitTemplate.getConnectionFactory(), connectionFactory); + assertEquals(connectionFactory.getHost(), "localhost"); + } + + @Test + public void testRabbitTemplateWithOverrides() { + this.context = new AnnotationConfigApplicationContext(); + this.context.register(TestConfiguration.class, RabbitTemplateAutoConfiguration.class); + TestUtils.addEnviroment(this.context, "spring.rabbitmq.host:remote-server", + "spring.rabbitmq.port:9000", "spring.rabbitmq.username:alice", "spring.rabbitmq.password:secret"); + this.context.refresh(); + CachingConnectionFactory connectionFactory = this.context.getBean(CachingConnectionFactory.class); + assertEquals(connectionFactory.getHost(), "remote-server"); + assertEquals(connectionFactory.getPort(), 9000); + } + + @Test + public void testConnectionFactoryBackoff() { + this.context = new AnnotationConfigApplicationContext(); + this.context.register(TestConfiguration2.class, RabbitTemplateAutoConfiguration.class); + this.context.refresh(); + RabbitTemplate rabbitTemplate = this.context.getBean(RabbitTemplate.class); + CachingConnectionFactory connectionFactory = this.context.getBean(CachingConnectionFactory.class); + assertEquals(rabbitTemplate.getConnectionFactory(), connectionFactory); + assertEquals(connectionFactory.getHost(), "otherserver"); + assertEquals(connectionFactory.getPort(), 8001); + } + + @Test + public void testStaticQueues() { + this.context = new AnnotationConfigApplicationContext(); + this.context.register(TestConfiguration.class, RabbitTemplateAutoConfiguration.class); + TestUtils.addEnviroment(this.context, "spring.rabbitmq.dynamic:false"); + this.context.refresh(); + try { + this.context.getBean(AmqpAdmin.class); + fail("There should NOT be an AmqpAdmin bean when dynamic is switch to false"); + } catch (Exception e) {} + } + + @Configuration + protected static class TestConfiguration { + + } + + @Configuration + protected static class TestConfiguration2 { + @Bean + ConnectionFactory aDifferentConnectionFactory() { + return new CachingConnectionFactory("otherserver", 8001); + } + } +} diff --git a/spring-boot-cli/samples/rabbit.groovy b/spring-boot-cli/samples/rabbit.groovy new file mode 100644 index 00000000000..2a307074ae0 --- /dev/null +++ b/spring-boot-cli/samples/rabbit.groovy @@ -0,0 +1,64 @@ +package org.test + +import java.util.concurrent.CountDownLatch + +@Log +@Configuration +@EnableRabbitMessaging +class RabbitExample implements CommandLineRunner { + + private CountDownLatch latch = new CountDownLatch(1) + + @Autowired + RabbitTemplate rabbitTemplate + + private String queueName = "spring-boot" + + @Bean + Queue queue() { + new Queue(queueName, false) + } + + @Bean + TopicExchange exchange() { + new TopicExchange("spring-boot-exchange") + } + + /** + * The queue and topic exchange cannot be inlined inside this method and have + * dynamic creation with Spring AMQP work properly. + */ + @Bean + Binding binding(Queue queue, TopicExchange exchange) { + BindingBuilder + .bind(queue) + .to(exchange) + .with("spring-boot") + } + + @Bean + SimpleMessageListenerContainer container(CachingConnectionFactory connectionFactory) { + return new SimpleMessageListenerContainer( + connectionFactory: connectionFactory, + queueNames: [queueName], + messageListener: new MessageListenerAdapter(new Receiver(latch:latch), "receive") + ) + } + + void run(String... args) { + log.info "Sending RabbitMQ message..." + rabbitTemplate.convertAndSend(queueName, "Greetings from Spring Boot via RabbitMQ") + latch.await() + } + +} + +@Log +class Receiver { + CountDownLatch latch + + def receive(String message) { + log.info "Received ${message}" + latch.countDown() + } +} diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/autoconfigure/RabbitCompilerAutoConfiguration.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/autoconfigure/RabbitCompilerAutoConfiguration.java new file mode 100644 index 00000000000..8e41eeaf3fc --- /dev/null +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/autoconfigure/RabbitCompilerAutoConfiguration.java @@ -0,0 +1,67 @@ +/* + * Copyright 2012-2013 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 + * + * http://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.cli.compiler.autoconfigure; + +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.control.CompilationFailedException; +import org.codehaus.groovy.control.customizers.ImportCustomizer; +import org.springframework.boot.cli.compiler.AstUtils; +import org.springframework.boot.cli.compiler.CompilerAutoConfiguration; +import org.springframework.boot.cli.compiler.DependencyCustomizer; + +import java.lang.annotation.*; + +/** + * {@link CompilerAutoConfiguration} for Spring Rabbit. + * + * @author Greg Turnquist + */ +public class RabbitCompilerAutoConfiguration extends CompilerAutoConfiguration { + + @Override + public boolean matches(ClassNode classNode) { + // Slightly weird detection algorithm because there is no @Enable annotation for + // Integration + return AstUtils.hasAtLeastOneAnnotation(classNode, "EnableRabbitMessaging"); + } + + @Override + public void applyDependencies(DependencyCustomizer dependencies) + throws CompilationFailedException { + dependencies.add("org.springframework.amqp", "spring-rabbit", + dependencies.getProperty("spring-rabbit.version")); + + } + + @Override + public void applyImports(ImportCustomizer imports) throws CompilationFailedException { + imports.addStarImports("org.springframework.amqp.rabbit.core", + "org.springframework.amqp.rabbit.connection", + "org.springframework.amqp.rabbit.listener", + "org.springframework.amqp.rabbit.listener.adapter", + "org.springframework.amqp.core").addImports(EnableRabbitMessaging.class.getCanonicalName()); + } + + @Target(ElementType.TYPE) + @Documented + @Retention(RetentionPolicy.RUNTIME) + public static @interface EnableRabbitMessaging { + + } + + +} diff --git a/spring-boot-cli/src/main/resources/META-INF/services/org.springframework.boot.cli.compiler.CompilerAutoConfiguration b/spring-boot-cli/src/main/resources/META-INF/services/org.springframework.boot.cli.compiler.CompilerAutoConfiguration index 0f28cef01e8..8d12d2dfe29 100644 --- a/spring-boot-cli/src/main/resources/META-INF/services/org.springframework.boot.cli.compiler.CompilerAutoConfiguration +++ b/spring-boot-cli/src/main/resources/META-INF/services/org.springframework.boot.cli.compiler.CompilerAutoConfiguration @@ -1,6 +1,7 @@ org.springframework.boot.cli.compiler.autoconfigure.SpringBootCompilerAutoConfiguration org.springframework.boot.cli.compiler.autoconfigure.SpringMvcCompilerAutoConfiguration org.springframework.boot.cli.compiler.autoconfigure.SpringBatchCompilerAutoConfiguration +org.springframework.boot.cli.compiler.autoconfigure.RabbitCompilerAutoConfiguration org.springframework.boot.cli.compiler.autoconfigure.ReactorCompilerAutoConfiguration org.springframework.boot.cli.compiler.autoconfigure.JdbcCompilerAutoConfiguration org.springframework.boot.cli.compiler.autoconfigure.JmsCompilerAutoConfiguration diff --git a/spring-boot-cli/src/main/resources/META-INF/springcli.properties b/spring-boot-cli/src/main/resources/META-INF/springcli.properties index c6e53292aeb..10cab5549d1 100644 --- a/spring-boot-cli/src/main/resources/META-INF/springcli.properties +++ b/spring-boot-cli/src/main/resources/META-INF/springcli.properties @@ -4,6 +4,7 @@ reactor.version: ${reactor.version} spring.version: ${spring.version} spring-batch.version: ${spring-batch.version} spring-boot.version: ${project.version} +spring-rabbit.version: ${spring-rabbit.version} spring-security.version: ${spring-security.version} spring-integration.version: ${spring-integration.version} spring-integration-groovydsl.version: ${spring-integration-groovydsl.version} diff --git a/spring-boot-cli/src/test/java/org/springframework/boot/cli/SampleIntegrationTests.java b/spring-boot-cli/src/test/java/org/springframework/boot/cli/SampleIntegrationTests.java index 3559c3d9c51..7cdd5aece5b 100644 --- a/spring-boot-cli/src/test/java/org/springframework/boot/cli/SampleIntegrationTests.java +++ b/spring-boot-cli/src/test/java/org/springframework/boot/cli/SampleIntegrationTests.java @@ -24,11 +24,7 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import org.apache.ivy.util.FileUtil; -import org.junit.After; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Rule; -import org.junit.Test; +import org.junit.*; import org.springframework.boot.OutputCapture; import org.springframework.boot.cli.command.CleanCommand; import org.springframework.boot.cli.command.RunCommand; @@ -196,4 +192,13 @@ public class SampleIntegrationTests { FileUtil.forceDelete(new File("activemq-data")); // cleanup ActiveMQ cruft } + @Test + @Ignore // this test requires RabbitMQ to be run, so disable it be default + public void rabbitSample() throws Exception { + start("samples/rabbit.groovy"); + String output = this.outputCapture.getOutputAndRelease(); + assertTrue("Wrong output: " + output, + output.contains("Received Greetings from Spring Boot via RabbitMQ")); + } + } diff --git a/spring-boot-dependencies/pom.xml b/spring-boot-dependencies/pom.xml index 53f98617bd1..9c854413067 100644 --- a/spring-boot-dependencies/pom.xml +++ b/spring-boot-dependencies/pom.xml @@ -39,6 +39,7 @@ 2.2.0.RELEASE 1.4.1.RELEASE 1.3.1.RELEASE + 1.2.0.RELEASE 2.0.16 2.0.0 1.1.1 @@ -420,6 +421,11 @@ spring-security-acl ${spring-security.version} + + org.springframework.amqp + spring-rabbit + ${spring-rabbit.version} + org.thymeleaf thymeleaf