Commit 941d1637 authored by Greg Turnquist's avatar Greg Turnquist Committed by Dave Syer

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
parent fa6e6fde
......@@ -116,6 +116,11 @@
<artifactId>spring-security-acl</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf</artifactId>
......
/*
* 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;
}
}
}
# 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,\
......
/*
* 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);
}
}
}
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()
}
}
/*
* 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 {
}
}
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
......
......@@ -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}
......
......@@ -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"));
}
}
......@@ -39,6 +39,7 @@
<spring-batch.version>2.2.0.RELEASE</spring-batch.version>
<spring-data-jpa.version>1.4.1.RELEASE</spring-data-jpa.version>
<spring-data-mongo.version>1.3.1.RELEASE</spring-data-mongo.version>
<spring-rabbit.version>1.2.0.RELEASE</spring-rabbit.version>
<thymeleaf.version>2.0.16</thymeleaf.version>
<thymeleaf-extras-springsecurity3.version>2.0.0</thymeleaf-extras-springsecurity3.version>
<thymeleaf-layout-dialect.version>1.1.1</thymeleaf-layout-dialect.version>
......@@ -420,6 +421,11 @@
<artifactId>spring-security-acl</artifactId>
<version>${spring-security.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit</artifactId>
<version>${spring-rabbit.version}</version>
</dependency>
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf</artifactId>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment