diff --git a/module/spring-boot-amqp/build.gradle b/module/spring-boot-amqp/build.gradle index 38d9ce92c820..9a1901534343 100644 --- a/module/spring-boot-amqp/build.gradle +++ b/module/spring-boot-amqp/build.gradle @@ -28,7 +28,7 @@ description = "Spring Boot AMQP" dependencies { api(project(":core:spring-boot")) api("org.springframework:spring-messaging") - api("org.springframework.amqp:spring-rabbit") + api("org.springframework.amqp:spring-rabbitmq-client") compileOnly("com.fasterxml.jackson.core:jackson-annotations") diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpEnvironmentBuilderCustomizer.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpEnvironmentBuilderCustomizer.java new file mode 100644 index 000000000000..3a8bbb77b443 --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpEnvironmentBuilderCustomizer.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-present 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.amqp.autoconfigure; + +import com.rabbitmq.client.amqp.Environment; +import com.rabbitmq.client.amqp.impl.AmqpEnvironmentBuilder; + +/** + * Callback interface that can be implemented by beans wishing to customize the + * auto-configured {@link Environment} that is created by an + * {@link AmqpEnvironmentBuilder}. + * + * @author Eddú Meléndez + * @since 4.0.0 + */ +@FunctionalInterface +public interface AmqpEnvironmentBuilderCustomizer { + + /** + * Customize the {@code AmqpEnvironmentBuilder}. + * @param builder the builder to customize + */ + void customize(AmqpEnvironmentBuilder builder); + +} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpAutoConfiguration.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpAutoConfiguration.java new file mode 100644 index 000000000000..5170d2ef872e --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpAutoConfiguration.java @@ -0,0 +1,136 @@ +/* + * Copyright 2012-present 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.amqp.autoconfigure; + +import com.rabbitmq.client.amqp.Connection; +import com.rabbitmq.client.amqp.CredentialsProvider; +import com.rabbitmq.client.amqp.Environment; +import com.rabbitmq.client.amqp.impl.AmqpEnvironmentBuilder; +import com.rabbitmq.client.amqp.impl.AmqpEnvironmentBuilder.EnvironmentConnectionSettings; + +import org.springframework.amqp.rabbit.config.ContainerCustomizer; +import org.springframework.amqp.rabbit.retry.MessageRecoverer; +import org.springframework.amqp.rabbitmq.client.AmqpConnectionFactory; +import org.springframework.amqp.rabbitmq.client.RabbitAmqpAdmin; +import org.springframework.amqp.rabbitmq.client.RabbitAmqpTemplate; +import org.springframework.amqp.rabbitmq.client.SingleAmqpConnectionFactory; +import org.springframework.amqp.rabbitmq.client.config.RabbitAmqpListenerContainerFactory; +import org.springframework.amqp.rabbitmq.client.listener.RabbitAmqpListenerContainer; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.amqp.autoconfigure.RabbitConnectionDetails.Address; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.boot.ssl.SslBundles; +import org.springframework.context.annotation.Bean; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for {@link RabbitAmqpTemplate}. + * + * @author Eddú Meléndez + * @since 4.0.0 + */ +@AutoConfiguration(before = RabbitAutoConfiguration.class) +@ConditionalOnClass({ RabbitAmqpTemplate.class, Connection.class }) +@EnableConfigurationProperties(RabbitProperties.class) +public final class RabbitAmqpAutoConfiguration { + + private final RabbitProperties properties; + + RabbitAmqpAutoConfiguration(RabbitProperties properties) { + this.properties = properties; + } + + @Bean + @ConditionalOnMissingBean + RabbitConnectionDetails rabbitConnectionDetails(ObjectProvider sslBundles) { + return new PropertiesRabbitConnectionDetails(this.properties, sslBundles.getIfAvailable()); + } + + @Bean(name = "rabbitListenerContainerFactory") + @ConditionalOnMissingBean(name = "rabbitListenerContainerFactory") + RabbitAmqpListenerContainerFactory rabbitAmqpListenerContainerFactory(AmqpConnectionFactory connectionFactory, + ObjectProvider> amqpContainerCustomizer, + ObjectProvider retryTemplateCustomizers, + ObjectProvider messageRecoverer) { + RabbitAmqpListenerContainerFactory factory = new RabbitAmqpListenerContainerFactory(connectionFactory); + amqpContainerCustomizer.ifUnique(factory::setContainerCustomizer); + + RabbitProperties.AmqpContainer configuration = this.properties.getListener().getSimple(); + factory.setObservationEnabled(configuration.isObservationEnabled()); + return factory; + } + + @Bean + @ConditionalOnMissingBean + Environment rabbitAmqpEnvironment(RabbitConnectionDetails connectionDetails, + ObjectProvider customizers, + ObjectProvider credentialsProvider) { + PropertyMapper map = PropertyMapper.get(); + EnvironmentConnectionSettings environmentConnectionSettings = new AmqpEnvironmentBuilder().connectionSettings(); + Address address = connectionDetails.getFirstAddress(); + map.from(address::host).whenNonNull().to(environmentConnectionSettings::host); + map.from(address::port).to(environmentConnectionSettings::port); + map.from(connectionDetails::getUsername).whenNonNull().to(environmentConnectionSettings::username); + map.from(connectionDetails::getPassword).whenNonNull().to(environmentConnectionSettings::password); + map.from(connectionDetails::getVirtualHost).whenNonNull().to(environmentConnectionSettings::virtualHost); + map.from(credentialsProvider::getIfAvailable) + .whenNonNull() + .to(environmentConnectionSettings::credentialsProvider); + + AmqpEnvironmentBuilder builder = environmentConnectionSettings.environmentBuilder(); + customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); + return builder.build(); + } + + @Bean + @ConditionalOnMissingBean + AmqpConnectionFactory amqpConnectionFactory(Environment environment) { + return new SingleAmqpConnectionFactory(environment); + } + + @Bean + @ConditionalOnMissingBean + RabbitAmqpTemplate rabbitAmqpTemplate(AmqpConnectionFactory connectionFactory, + ObjectProvider customizers, + ObjectProvider messageConverter) { + RabbitAmqpTemplate rabbitAmqpTemplate = new RabbitAmqpTemplate(connectionFactory); + if (messageConverter.getIfAvailable() != null) { + rabbitAmqpTemplate.setMessageConverter(messageConverter.getIfAvailable()); + } + RabbitProperties.Template templateProperties = this.properties.getTemplate(); + + PropertyMapper map = PropertyMapper.get(); + map.from(templateProperties::getDefaultReceiveQueue).whenNonNull().to(rabbitAmqpTemplate::setReceiveQueue); + map.from(templateProperties::getExchange).whenNonNull().to(rabbitAmqpTemplate::setExchange); + map.from(templateProperties::getRoutingKey).to(rabbitAmqpTemplate::setRoutingKey); + + customizers.orderedStream().forEach((customizer) -> customizer.customize(rabbitAmqpTemplate)); + return rabbitAmqpTemplate; + } + + @Bean + @ConditionalOnMissingBean + RabbitAmqpAdmin rabbitAmqpAdmin(AmqpConnectionFactory connectionFactory) { + return new RabbitAmqpAdmin(connectionFactory); + } + +} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpTemplateCustomizer.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpTemplateCustomizer.java new file mode 100644 index 000000000000..e1eb2404cbb2 --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpTemplateCustomizer.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-present 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.amqp.autoconfigure; + +import org.springframework.amqp.rabbitmq.client.RabbitAmqpTemplate; + +/** + * Callback interface that can be used to customize a {@link RabbitAmqpTemplate}. + * + * @author Eddú Meléndez + * @since 4.0.0 + */ +@FunctionalInterface +public interface RabbitAmqpTemplateCustomizer { + + /** + * Callback to customize a {@link RabbitAmqpTemplate} instance. + * @param rabbitAmqpTemplate the rabbitAmqpTemplate to customize + */ + void customize(RabbitAmqpTemplate rabbitAmqpTemplate); + +} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAutoConfiguration.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAutoConfiguration.java index 159245f42cfc..616418ac68c4 100644 --- a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAutoConfiguration.java +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAutoConfiguration.java @@ -71,10 +71,12 @@ * @author Moritz Halbritter * @author Andy Wilkinson * @author Scott Frederick + * @author Eddú Meléndez * @since 4.0.0 */ @AutoConfiguration @ConditionalOnClass({ RabbitTemplate.class, Channel.class }) +@ConditionalOnMissingBean(type = "org.springframework.amqp.rabbitmq.client.RabbitAmqpTemplate") @EnableConfigurationProperties(RabbitProperties.class) @Import({ RabbitAnnotationDrivenConfiguration.class, RabbitStreamConfiguration.class }) public final class RabbitAutoConfiguration { diff --git a/module/spring-boot-amqp/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/module/spring-boot-amqp/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index daf8ae3ba45f..febc373b38a8 100644 --- a/module/spring-boot-amqp/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/module/spring-boot-amqp/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,3 +1,4 @@ +org.springframework.boot.amqp.autoconfigure.RabbitAmqpAutoConfiguration org.springframework.boot.amqp.autoconfigure.RabbitAutoConfiguration org.springframework.boot.amqp.autoconfigure.health.RabbitHealthContributorAutoConfiguration org.springframework.boot.amqp.autoconfigure.metrics.RabbitMetricsAutoConfiguration \ No newline at end of file diff --git a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpAutoConfigurationTests.java b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpAutoConfigurationTests.java new file mode 100644 index 000000000000..fbc7a2b75f2f --- /dev/null +++ b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpAutoConfigurationTests.java @@ -0,0 +1,192 @@ +/* + * Copyright 2012-present 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.amqp.autoconfigure; + +import org.aopalliance.aop.Advice; +import org.junit.jupiter.api.Test; +import org.mockito.InOrder; + +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageProperties; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.rabbit.config.BaseRabbitListenerContainerFactory; +import org.springframework.amqp.rabbit.config.ContainerCustomizer; +import org.springframework.amqp.rabbitmq.client.AmqpConnectionFactory; +import org.springframework.amqp.rabbitmq.client.RabbitAmqpAdmin; +import org.springframework.amqp.rabbitmq.client.RabbitAmqpTemplate; +import org.springframework.amqp.rabbitmq.client.config.RabbitAmqpListenerContainerFactory; +import org.springframework.amqp.rabbitmq.client.listener.RabbitAmqpListenerContainer; +import org.springframework.amqp.support.converter.MessageConversionException; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.retry.RetryPolicy; +import org.springframework.retry.policy.NeverRetryPolicy; +import org.springframework.retry.support.RetryTemplate; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link RabbitAmqpAutoConfiguration}. + * + * @author Eddú Meléndez + */ +class RabbitAmqpAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(RabbitAmqpAutoConfiguration.class)); + + @Test + void testDefaultRabbitAmqpConfiguration() { + this.contextRunner.run((context) -> { + RabbitAmqpListenerContainerFactory listenerContainerFactory = context + .getBean(RabbitAmqpListenerContainerFactory.class); + AmqpConnectionFactory connectionFactory = context.getBean(AmqpConnectionFactory.class); + RabbitAmqpTemplate rabbitAmqpTemplate = context.getBean(RabbitAmqpTemplate.class); + RabbitAmqpAdmin rabbitAmqpAdmin = context.getBean(RabbitAmqpAdmin.class); + + assertThat(listenerContainerFactory).isNotNull(); + assertThat(listenerContainerFactory).extracting("containerCustomizer").isNull(); + assertThat(connectionFactory).isNotNull(); + assertThat(rabbitAmqpTemplate).isNotNull(); + assertThat(rabbitAmqpAdmin).isNotNull(); + }); + } + + @Test + void whenMultipleRabbitAmqpTemplateCustomizersAreDefinedThenTheyAreCalledInOrder() { + this.contextRunner.withUserConfiguration(MultipleRabbitAmqpTemplateCustomizersConfiguration.class) + .run((context) -> { + RabbitAmqpTemplateCustomizer firstCustomizer = context.getBean("firstCustomizer", + RabbitAmqpTemplateCustomizer.class); + RabbitAmqpTemplateCustomizer secondCustomizer = context.getBean("secondCustomizer", + RabbitAmqpTemplateCustomizer.class); + InOrder inOrder = inOrder(firstCustomizer, secondCustomizer); + RabbitAmqpTemplate template = context.getBean(RabbitAmqpTemplate.class); + then(firstCustomizer).should(inOrder).customize(template); + then(secondCustomizer).should(inOrder).customize(template); + inOrder.verifyNoMoreInteractions(); + }); + } + + private void assertListenerRetryTemplate(BaseRabbitListenerContainerFactory rabbitListenerContainerFactory, + RetryPolicy retryPolicy) { + Advice[] adviceChain = rabbitListenerContainerFactory.getAdviceChain(); + assertThat(adviceChain).isNotNull(); + assertThat(adviceChain).hasSize(1); + Advice advice = adviceChain[0]; + RetryTemplate retryTemplate = (RetryTemplate) ReflectionTestUtils.getField(advice, "retryOperations"); + assertThat(retryTemplate).hasFieldOrPropertyWithValue("retryPolicy", retryPolicy); + } + + @Test + void testListenerContainerFactoryWithContainerCustomizer() { + this.contextRunner.withUserConfiguration(AmqpContainerCustomizerConfiguration.class).run((context) -> { + RabbitAmqpListenerContainerFactory listenerContainerFactory = context + .getBean(RabbitAmqpListenerContainerFactory.class); + + assertThat(listenerContainerFactory).isNotNull(); + assertThat(listenerContainerFactory).extracting("containerCustomizer").isNotNull(); + }); + } + + @Configuration(proxyBeanMethods = false) + static class MultipleRabbitAmqpTemplateCustomizersConfiguration { + + @Bean + @Order(Ordered.LOWEST_PRECEDENCE) + RabbitAmqpTemplateCustomizer secondCustomizer() { + return mock(RabbitAmqpTemplateCustomizer.class); + } + + @Bean + @Order(0) + RabbitAmqpTemplateCustomizer firstCustomizer() { + return mock(RabbitAmqpTemplateCustomizer.class); + } + + } + + @Configuration(proxyBeanMethods = false) + static class RabbitRetryTemplateCustomizerConfiguration { + + private final RetryPolicy retryPolicy = new NeverRetryPolicy(); + + @Bean + RabbitRetryTemplateCustomizer rabbitListenerRetryTemplateCustomizer() { + return (target, template) -> { + if (target.equals(RabbitRetryTemplateCustomizer.Target.LISTENER)) { + template.setRetryPolicy(this.retryPolicy); + } + }; + } + + } + + @Import(TestListener.class) + @Configuration(proxyBeanMethods = false) + static class AmqpContainerCustomizerConfiguration { + + @Bean + @SuppressWarnings("unchecked") + ContainerCustomizer customizer() { + return mock(ContainerCustomizer.class); + } + + } + + @Configuration + static class CustomMessageConverterConfiguration { + + @Bean + MessageConverter messageConverter() { + return new MessageConverter() { + + @Override + public Message toMessage(Object object, MessageProperties messageProperties) + throws MessageConversionException { + return new Message(object.toString().getBytes()); + } + + @Override + public Object fromMessage(Message message) throws MessageConversionException { + return new String(message.getBody()); + } + + }; + } + + } + + static class TestListener { + + @RabbitListener(queues = "test", autoStartup = "false") + void listen(String in) { + } + + } + +} diff --git a/settings.gradle b/settings.gradle index 65e82571407d..850d472783b0 100644 --- a/settings.gradle +++ b/settings.gradle @@ -234,6 +234,7 @@ include "starter:spring-boot-starter-pulsar" include "starter:spring-boot-starter-pulsar-reactive" include "starter:spring-boot-starter-quartz" include "starter:spring-boot-starter-r2dbc" +include "starter:spring-boot-starter-rabbitmq" include "starter:spring-boot-starter-reactor" include "starter:spring-boot-starter-reactor-netty" include "starter:spring-boot-starter-restclient" @@ -331,6 +332,7 @@ include ":smoke-test:spring-boot-smoke-test-prometheus" include ":smoke-test:spring-boot-smoke-test-property-validation" include ":smoke-test:spring-boot-smoke-test-pulsar" include ":smoke-test:spring-boot-smoke-test-quartz" +include ":smoke-test:spring-boot-smoke-test-rabbit-amqp" include ":smoke-test:spring-boot-smoke-test-reactive-oauth2-client" include ":smoke-test:spring-boot-smoke-test-reactive-oauth2-resource-server" include ":smoke-test:spring-boot-smoke-test-rsocket" diff --git a/smoke-test/spring-boot-smoke-test-amqp/build.gradle b/smoke-test/spring-boot-smoke-test-amqp/build.gradle index edf318600ce7..1616a33fa649 100644 --- a/smoke-test/spring-boot-smoke-test-amqp/build.gradle +++ b/smoke-test/spring-boot-smoke-test-amqp/build.gradle @@ -22,7 +22,7 @@ plugins { description = "Spring Boot AMQP smoke test" dependencies { - implementation(project(":starter:spring-boot-starter-amqp")) + implementation(project(":starter:spring-boot-starter-rabbitmq")) dockerTestImplementation(project(":starter:spring-boot-starter-test")) dockerTestImplementation(project(":core:spring-boot-testcontainers")) diff --git a/smoke-test/spring-boot-smoke-test-rabbit-amqp/build.gradle b/smoke-test/spring-boot-smoke-test-rabbit-amqp/build.gradle new file mode 100644 index 000000000000..d4d9a57a60a1 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-rabbit-amqp/build.gradle @@ -0,0 +1,33 @@ +/* + * Copyright 2012-present 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. + */ + +plugins { + id "java" + id "org.springframework.boot.docker-test" +} + +description = "Spring Boot RabbitMQ AMQP smoke test" + +dependencies { + implementation(project(":starter:spring-boot-starter-amqp")) + + dockerTestImplementation(project(":starter:spring-boot-starter-test")) + dockerTestImplementation(project(":core:spring-boot-testcontainers")) + dockerTestImplementation(project(":test-support:spring-boot-docker-test-support")) + dockerTestImplementation("org.awaitility:awaitility") + dockerTestImplementation("org.testcontainers:junit-jupiter") + dockerTestImplementation("org.testcontainers:rabbitmq") +} \ No newline at end of file diff --git a/smoke-test/spring-boot-smoke-test-rabbit-amqp/src/dockerTest/java/smoketest/amqp/SampleRabbitAmqpSimpleApplicationTests.java b/smoke-test/spring-boot-smoke-test-rabbit-amqp/src/dockerTest/java/smoketest/amqp/SampleRabbitAmqpSimpleApplicationTests.java new file mode 100644 index 000000000000..d5ef51eff469 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-rabbit-amqp/src/dockerTest/java/smoketest/amqp/SampleRabbitAmqpSimpleApplicationTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012-present 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 smoketest.amqp; + +import java.time.Duration; + +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.testcontainers.containers.RabbitMQContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@Testcontainers(disabledWithoutDocker = true) +@ExtendWith(OutputCaptureExtension.class) +class SampleRabbitAmqpSimpleApplicationTests { + + @Container + @ServiceConnection + static final RabbitMQContainer rabbit = new RabbitMQContainer("rabbitmq:4.0-management-alpine"); + + @Autowired + private Sender sender; + + @Test + void sendSimpleMessage(CapturedOutput output) { + this.sender.send("Test message"); + Awaitility.waitAtMost(Duration.ofMinutes(1)).untilAsserted(() -> assertThat(output).contains("Test message")); + } + +} diff --git a/smoke-test/spring-boot-smoke-test-rabbit-amqp/src/main/java/smoketest/amqp/SampleRabbitAmqpSimpleApplication.java b/smoke-test/spring-boot-smoke-test-rabbit-amqp/src/main/java/smoketest/amqp/SampleRabbitAmqpSimpleApplication.java new file mode 100644 index 000000000000..258131766e21 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-rabbit-amqp/src/main/java/smoketest/amqp/SampleRabbitAmqpSimpleApplication.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012-present 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 smoketest.amqp; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.rabbit.annotation.EnableRabbit; +import org.springframework.amqp.rabbit.annotation.RabbitHandler; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.boot.ApplicationRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.messaging.handler.annotation.Payload; + +@SpringBootApplication +@EnableRabbit +@RabbitListener(queues = "foo") +public class SampleRabbitAmqpSimpleApplication { + + private static final Log logger = LogFactory.getLog(SampleRabbitAmqpSimpleApplication.class); + + @Bean + public Sender mySender() { + return new Sender(); + } + + @Bean + public Queue fooQueue() { + return new Queue("foo"); + } + + @RabbitHandler + public void process(@Payload String foo) { + logger.info(foo); + } + + @Bean + public ApplicationRunner runner(Sender sender) { + return (args) -> sender.send("Hello"); + } + + public static void main(String[] args) { + SpringApplication.run(SampleRabbitAmqpSimpleApplication.class, args); + } + +} diff --git a/smoke-test/spring-boot-smoke-test-rabbit-amqp/src/main/java/smoketest/amqp/Sender.java b/smoke-test/spring-boot-smoke-test-rabbit-amqp/src/main/java/smoketest/amqp/Sender.java new file mode 100644 index 000000000000..6639bc0316ce --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-rabbit-amqp/src/main/java/smoketest/amqp/Sender.java @@ -0,0 +1,31 @@ +/* + * Copyright 2012-present 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 smoketest.amqp; + +import org.springframework.amqp.rabbitmq.client.RabbitAmqpTemplate; +import org.springframework.beans.factory.annotation.Autowired; + +public class Sender { + + @Autowired + private RabbitAmqpTemplate rabbitAmqpTemplate; + + public void send(String message) { + this.rabbitAmqpTemplate.convertAndSend("foo", message); + } + +} diff --git a/starter/spring-boot-starter-amqp/build.gradle b/starter/spring-boot-starter-amqp/build.gradle index bd8d7c906e19..13a1e0d9974c 100644 --- a/starter/spring-boot-starter-amqp/build.gradle +++ b/starter/spring-boot-starter-amqp/build.gradle @@ -18,7 +18,7 @@ plugins { id "org.springframework.boot.starter" } -description = "Starter for using Spring AMQP and Rabbit MQ" +description = "Starter for using Spring AMQP with Rabbit MQ over AMQP 1.0 protocol" dependencies { api(project(":starter:spring-boot-starter")) diff --git a/starter/spring-boot-starter-rabbitmq/build.gradle b/starter/spring-boot-starter-rabbitmq/build.gradle new file mode 100644 index 000000000000..2f29ebb4d920 --- /dev/null +++ b/starter/spring-boot-starter-rabbitmq/build.gradle @@ -0,0 +1,30 @@ +/* + * Copyright 2012-present 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. + */ + +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for using Spring AMQP with Rabbit MQ over AMQP 0.9.1 protocol" + +dependencies { + api(project(":starter:spring-boot-starter")) + + api(project(":module:spring-boot-amqp")) { + exclude group: "org.springframework.amqp", module: "spring-rabbitmq-client" + } + api("org.springframework.amqp:spring-rabbit") +}