From 89e79e826e9a908d572f54af66f1e6a2ebe18e94 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 7 Jan 2022 12:35:46 +0000 Subject: [PATCH] Reinstate support for Apache Artemis See gh-29203 --- .../build.gradle | 11 + ...JmsHealthContributorAutoConfiguration.java | 3 + ...althContributorAutoConfigurationTests.java | 21 +- .../spring-boot-autoconfigure/build.gradle | 11 + .../jms/artemis/ArtemisAutoConfiguration.java | 56 ++ .../ArtemisConfigurationCustomizer.java | 41 ++ ...ArtemisConnectionFactoryConfiguration.java | 101 ++++ .../ArtemisConnectionFactoryFactory.java | 161 ++++++ .../ArtemisEmbeddedConfigurationFactory.java | 91 ++++ .../ArtemisEmbeddedServerConfiguration.java | 126 +++++ .../jms/artemis/ArtemisMode.java | 38 ++ .../artemis/ArtemisNoOpBindingRegistry.java | 48 ++ .../jms/artemis/ArtemisProperties.java | 268 ++++++++++ ...temisXAConnectionFactoryConfiguration.java | 60 +++ .../jms/artemis/package-info.java | 22 + .../transaction/jta/JtaAutoConfiguration.java | 4 +- .../main/resources/META-INF/spring.factories | 1 + .../jms/JmsAutoConfigurationTests.java | 63 ++- .../ArtemisAutoConfigurationTests.java | 501 ++++++++++++++++++ ...emisEmbeddedConfigurationFactoryTests.java | 92 ++++ .../spring-boot-cli/build.gradle | 1 + .../boot/cli/SampleIntegrationTests.java | 1 - .../spring-boot-dependencies/build.gradle | 26 + .../asciidoc/documentation/messaging.adoc | 2 +- .../src/docs/asciidoc/messaging/jms.adoc | 53 ++ .../spring-boot-starter-artemis/build.gradle | 13 + 26 files changed, 1782 insertions(+), 33 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisAutoConfiguration.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConfigurationCustomizer.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConnectionFactoryConfiguration.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConnectionFactoryFactory.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisEmbeddedConfigurationFactory.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisEmbeddedServerConfiguration.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisMode.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisNoOpBindingRegistry.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisProperties.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisXAConnectionFactoryConfiguration.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/package-info.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisAutoConfigurationTests.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisEmbeddedConfigurationFactoryTests.java create mode 100644 spring-boot-project/spring-boot-starters/spring-boot-starter-artemis/build.gradle diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle index 92df6ee4cb..f33932cf4f 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle @@ -145,6 +145,17 @@ dependencies { exclude group: "org.jboss.spec.javax.annotation", module: "jboss-annotations-api_1.3_spec" } testImplementation("jakarta.xml.bind:jakarta.xml.bind-api") + testImplementation("org.apache.activemq:artemis-jms-client") { + exclude group: "commons-logging", module: "commons-logging" + exclude group: "org.apache.geronimo.specs", module: "geronimo-jms_2.0_spec" + exclude group: "org.apache.geronimo.specs", module: "geronimo-json_1.0_spec" + } + testImplementation("org.apache.activemq:artemis-jms-server") { + exclude group: "commons-logging", module: "commons-logging" + exclude group: "org.apache.geronimo.specs", module: "geronimo-jms_2.0_spec" + exclude group: "org.apache.geronimo.specs", module: "geronimo-json_1.0_spec" + exclude group: "org.apache.geronimo.specs", module: "geronimo-jta_1.1_spec" + } testImplementation("org.apache.logging.log4j:log4j-to-slf4j") testImplementation("org.aspectj:aspectjrt") testImplementation("org.assertj:assertj-core") diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/jms/JmsHealthContributorAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/jms/JmsHealthContributorAutoConfiguration.java index 9735b4eb98..d2a005e8bc 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/jms/JmsHealthContributorAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/jms/JmsHealthContributorAutoConfiguration.java @@ -24,10 +24,12 @@ import org.springframework.boot.actuate.autoconfigure.health.CompositeHealthCont import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator; import org.springframework.boot.actuate.health.HealthContributor; import org.springframework.boot.actuate.jms.JmsHealthIndicator; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.jms.artemis.ArtemisAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -41,6 +43,7 @@ import org.springframework.context.annotation.Configuration; @ConditionalOnClass(ConnectionFactory.class) @ConditionalOnBean(ConnectionFactory.class) @ConditionalOnEnabledHealthIndicator("jms") +@AutoConfigureAfter(ArtemisAutoConfiguration.class) public class JmsHealthContributorAutoConfiguration extends CompositeHealthContributorConfiguration { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/jms/JmsHealthContributorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/jms/JmsHealthContributorAutoConfigurationTests.java index 2ee6acba2c..7d24a04c19 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/jms/JmsHealthContributorAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/jms/JmsHealthContributorAutoConfigurationTests.java @@ -16,20 +16,16 @@ package org.springframework.boot.actuate.autoconfigure.jms; -import jakarta.jms.ConnectionFactory; - import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.autoconfigure.health.HealthContributorAutoConfiguration; import org.springframework.boot.actuate.jms.JmsHealthIndicator; import org.springframework.boot.actuate.ldap.LdapHealthIndicator; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.jms.artemis.ArtemisAutoConfiguration; import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; /** * Tests for {@link JmsHealthContributorAutoConfiguration}. @@ -39,9 +35,8 @@ import static org.mockito.Mockito.mock; class JmsHealthContributorAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(JmsHealthContributorAutoConfiguration.class, - HealthContributorAutoConfiguration.class)) - .withUserConfiguration(TestConfiguration.class); + .withConfiguration(AutoConfigurations.of(ArtemisAutoConfiguration.class, + JmsHealthContributorAutoConfiguration.class, HealthContributorAutoConfiguration.class)); @Test void runShouldCreateIndicator() { @@ -54,14 +49,4 @@ class JmsHealthContributorAutoConfigurationTests { .run((context) -> assertThat(context).doesNotHaveBean(LdapHealthIndicator.class)); } - @Configuration(proxyBeanMethods = false) - static class TestConfiguration { - - @Bean - ConnectionFactory connectionFactory() { - return mock(ConnectionFactory.class); - } - - } - } diff --git a/spring-boot-project/spring-boot-autoconfigure/build.gradle b/spring-boot-project/spring-boot-autoconfigure/build.gradle index b406372ef3..babaea75ba 100644 --- a/spring-boot-project/spring-boot-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-autoconfigure/build.gradle @@ -48,6 +48,17 @@ dependencies { optional("jakarta.ws.rs:jakarta.ws.rs-api") optional("javax.cache:cache-api") optional("javax.money:money-api") + optional("org.apache.activemq:artemis-jms-client") { + exclude group: "commons-logging", module: "commons-logging" + exclude group: "org.apache.geronimo.specs", module: "geronimo-jms_2.0_spec" + exclude group: "org.apache.geronimo.specs", module: "geronimo-json_1.0_spec" + } + optional("org.apache.activemq:artemis-jms-server") { + exclude group: "commons-logging", module: "commons-logging" + exclude group: "org.apache.geronimo.specs", module: "geronimo-jms_2.0_spec" + exclude group: "org.apache.geronimo.specs", module: "geronimo-json_1.0_spec" + exclude group: "org.apache.geronimo.specs", module: "geronimo-jta_1.1_spec" + } optional("org.apache.commons:commons-dbcp2") { exclude group: "commons-logging", module: "commons-logging" } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisAutoConfiguration.java new file mode 100644 index 0000000000..712a310df0 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisAutoConfiguration.java @@ -0,0 +1,56 @@ +/* + * Copyright 2012-2019 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.jms.artemis; + +import javax.jms.ConnectionFactory; + +import org.apache.activemq.artemis.jms.client.ActiveMQConnectionFactory; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration; +import org.springframework.boot.autoconfigure.jms.JmsProperties; +import org.springframework.boot.autoconfigure.jms.JndiConnectionFactoryAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * {@link EnableAutoConfiguration Auto-configuration} to integrate with an Artemis broker. + * If the necessary classes are present, embed the broker in the application by default. + * Otherwise, connect to a broker available on the local machine with the default + * settings. + * + * @author Eddú Meléndez + * @author Stephane Nicoll + * @since 1.3.0 + * @see ArtemisProperties + */ +@Configuration(proxyBeanMethods = false) +@AutoConfigureBefore(JmsAutoConfiguration.class) +@AutoConfigureAfter({ JndiConnectionFactoryAutoConfiguration.class }) +@ConditionalOnClass({ ConnectionFactory.class, ActiveMQConnectionFactory.class }) +@ConditionalOnMissingBean(ConnectionFactory.class) +@EnableConfigurationProperties({ ArtemisProperties.class, JmsProperties.class }) +@Import({ ArtemisEmbeddedServerConfiguration.class, ArtemisXAConnectionFactoryConfiguration.class, + ArtemisConnectionFactoryConfiguration.class }) +public class ArtemisAutoConfiguration { + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConfigurationCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConfigurationCustomizer.java new file mode 100644 index 0000000000..59aee98817 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConfigurationCustomizer.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2020 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.jms.artemis; + +import org.apache.activemq.artemis.core.config.Configuration; +import org.apache.activemq.artemis.core.server.embedded.EmbeddedActiveMQ; + +/** + * Callback interface that can be implemented by beans wishing to customize the Artemis + * JMS server {@link Configuration} before it is used by an auto-configured + * {@link EmbeddedActiveMQ} instance. + * + * @author Eddú Meléndez + * @author Phillip Webb + * @since 1.3.0 + * @see ArtemisAutoConfiguration + */ +@FunctionalInterface +public interface ArtemisConfigurationCustomizer { + + /** + * Customize the configuration. + * @param configuration the configuration to customize + */ + void customize(Configuration configuration); + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConnectionFactoryConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConnectionFactoryConfiguration.java new file mode 100644 index 0000000000..33ca773b33 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConnectionFactoryConfiguration.java @@ -0,0 +1,101 @@ +/* + * Copyright 2012-2020 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.jms.artemis; + +import javax.jms.ConnectionFactory; + +import org.apache.activemq.artemis.jms.client.ActiveMQConnectionFactory; +import org.apache.commons.pool2.PooledObject; +import org.messaginghub.pooled.jms.JmsPoolConnectionFactory; + +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.jms.JmsPoolConnectionFactoryFactory; +import org.springframework.boot.autoconfigure.jms.JmsProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jms.connection.CachingConnectionFactory; + +/** + * Configuration for Artemis {@link ConnectionFactory}. + * + * @author Eddú Meléndez + * @author Phillip Webb + * @author Stephane Nicoll + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnMissingBean(ConnectionFactory.class) +class ArtemisConnectionFactoryConfiguration { + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(CachingConnectionFactory.class) + @ConditionalOnProperty(prefix = "spring.artemis.pool", name = "enabled", havingValue = "false", + matchIfMissing = true) + static class SimpleConnectionFactoryConfiguration { + + private final ArtemisProperties properties; + + private final ListableBeanFactory beanFactory; + + SimpleConnectionFactoryConfiguration(ArtemisProperties properties, ListableBeanFactory beanFactory) { + this.properties = properties; + this.beanFactory = beanFactory; + } + + @Bean(name = "jmsConnectionFactory") + @ConditionalOnProperty(prefix = "spring.jms.cache", name = "enabled", havingValue = "true", + matchIfMissing = true) + CachingConnectionFactory cachingJmsConnectionFactory(JmsProperties jmsProperties) { + JmsProperties.Cache cacheProperties = jmsProperties.getCache(); + CachingConnectionFactory connectionFactory = new CachingConnectionFactory(createConnectionFactory()); + connectionFactory.setCacheConsumers(cacheProperties.isConsumers()); + connectionFactory.setCacheProducers(cacheProperties.isProducers()); + connectionFactory.setSessionCacheSize(cacheProperties.getSessionCacheSize()); + return connectionFactory; + } + + @Bean(name = "jmsConnectionFactory") + @ConditionalOnProperty(prefix = "spring.jms.cache", name = "enabled", havingValue = "false") + ActiveMQConnectionFactory jmsConnectionFactory() { + return createConnectionFactory(); + } + + private ActiveMQConnectionFactory createConnectionFactory() { + return new ArtemisConnectionFactoryFactory(this.beanFactory, this.properties) + .createConnectionFactory(ActiveMQConnectionFactory.class); + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass({ JmsPoolConnectionFactory.class, PooledObject.class }) + @ConditionalOnProperty(prefix = "spring.artemis.pool", name = "enabled", havingValue = "true") + static class PooledConnectionFactoryConfiguration { + + @Bean(destroyMethod = "stop") + JmsPoolConnectionFactory jmsConnectionFactory(ListableBeanFactory beanFactory, ArtemisProperties properties) { + ActiveMQConnectionFactory connectionFactory = new ArtemisConnectionFactoryFactory(beanFactory, properties) + .createConnectionFactory(ActiveMQConnectionFactory.class); + return new JmsPoolConnectionFactoryFactory(properties.getPool()) + .createPooledConnectionFactory(connectionFactory); + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConnectionFactoryFactory.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConnectionFactoryFactory.java new file mode 100644 index 0000000000..62de6fb0ed --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConnectionFactoryFactory.java @@ -0,0 +1,161 @@ +/* + * Copyright 2012-2020 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.jms.artemis; + +import java.lang.reflect.Constructor; +import java.util.HashMap; +import java.util.Map; + +import org.apache.activemq.artemis.api.core.TransportConfiguration; +import org.apache.activemq.artemis.api.core.client.ActiveMQClient; +import org.apache.activemq.artemis.api.core.client.ServerLocator; +import org.apache.activemq.artemis.core.remoting.impl.invm.InVMConnectorFactory; +import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnectorFactory; +import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants; +import org.apache.activemq.artemis.jms.client.ActiveMQConnectionFactory; + +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * Factory to create an Artemis {@link ActiveMQConnectionFactory} instance from properties + * defined in {@link ArtemisProperties}. + * + * @author Eddú Meléndez + * @author Phillip Webb + * @author Stephane Nicoll + * @author Justin Bertram + */ +class ArtemisConnectionFactoryFactory { + + private static final String DEFAULT_BROKER_URL = "tcp://localhost:61616"; + + static final String[] EMBEDDED_JMS_CLASSES = { "org.apache.activemq.artemis.jms.server.embedded.EmbeddedJMS", + "org.apache.activemq.artemis.core.server.embedded.EmbeddedActiveMQ" }; + + private final ArtemisProperties properties; + + private final ListableBeanFactory beanFactory; + + ArtemisConnectionFactoryFactory(ListableBeanFactory beanFactory, ArtemisProperties properties) { + Assert.notNull(beanFactory, "BeanFactory must not be null"); + Assert.notNull(properties, "Properties must not be null"); + this.beanFactory = beanFactory; + this.properties = properties; + } + + T createConnectionFactory(Class factoryClass) { + try { + startEmbeddedJms(); + return doCreateConnectionFactory(factoryClass); + } + catch (Exception ex) { + throw new IllegalStateException("Unable to create ActiveMQConnectionFactory", ex); + } + } + + private void startEmbeddedJms() { + for (String embeddedJmsClass : EMBEDDED_JMS_CLASSES) { + if (ClassUtils.isPresent(embeddedJmsClass, null)) { + try { + this.beanFactory.getBeansOfType(Class.forName(embeddedJmsClass)); + } + catch (Exception ex) { + // Ignore + } + } + } + } + + private T doCreateConnectionFactory(Class factoryClass) throws Exception { + ArtemisMode mode = this.properties.getMode(); + if (mode == null) { + mode = deduceMode(); + } + if (mode == ArtemisMode.EMBEDDED) { + return createEmbeddedConnectionFactory(factoryClass); + } + return createNativeConnectionFactory(factoryClass); + } + + /** + * Deduce the {@link ArtemisMode} to use if none has been set. + * @return the mode + */ + private ArtemisMode deduceMode() { + if (this.properties.getEmbedded().isEnabled() && isEmbeddedJmsClassPresent()) { + return ArtemisMode.EMBEDDED; + } + return ArtemisMode.NATIVE; + } + + private boolean isEmbeddedJmsClassPresent() { + for (String embeddedJmsClass : EMBEDDED_JMS_CLASSES) { + if (ClassUtils.isPresent(embeddedJmsClass, null)) { + return true; + } + } + return false; + } + + private T createEmbeddedConnectionFactory(Class factoryClass) + throws Exception { + try { + TransportConfiguration transportConfiguration = new TransportConfiguration( + InVMConnectorFactory.class.getName(), this.properties.getEmbedded().generateTransportParameters()); + ServerLocator serviceLocator = ActiveMQClient.createServerLocatorWithoutHA(transportConfiguration); + return factoryClass.getConstructor(ServerLocator.class).newInstance(serviceLocator); + } + catch (NoClassDefFoundError ex) { + throw new IllegalStateException("Unable to create InVM " + + "Artemis connection, ensure that artemis-jms-server.jar is in the classpath", ex); + } + } + + private T createNativeConnectionFactory(Class factoryClass) + throws Exception { + T connectionFactory = newNativeConnectionFactory(factoryClass); + String user = this.properties.getUser(); + if (StringUtils.hasText(user)) { + connectionFactory.setUser(user); + connectionFactory.setPassword(this.properties.getPassword()); + } + return connectionFactory; + } + + @SuppressWarnings("deprecation") + private T newNativeConnectionFactory(Class factoryClass) throws Exception { + // Fallback if the broker url is not set + if (!StringUtils.hasText(this.properties.getBrokerUrl()) && StringUtils.hasText(this.properties.getHost())) { + Map params = new HashMap<>(); + params.put(TransportConstants.HOST_PROP_NAME, this.properties.getHost()); + params.put(TransportConstants.PORT_PROP_NAME, this.properties.getPort()); + TransportConfiguration transportConfiguration = new TransportConfiguration( + NettyConnectorFactory.class.getName(), params); + Constructor constructor = factoryClass.getConstructor(boolean.class, TransportConfiguration[].class); + return constructor.newInstance(false, new TransportConfiguration[] { transportConfiguration }); + } + String brokerUrl = StringUtils.hasText(this.properties.getBrokerUrl()) ? this.properties.getBrokerUrl() + : DEFAULT_BROKER_URL; + Constructor constructor = factoryClass.getConstructor(String.class); + return constructor.newInstance(brokerUrl); + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisEmbeddedConfigurationFactory.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisEmbeddedConfigurationFactory.java new file mode 100644 index 0000000000..6d47dcfd58 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisEmbeddedConfigurationFactory.java @@ -0,0 +1,91 @@ +/* + * Copyright 2012-2020 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.jms.artemis; + +import java.io.File; + +import org.apache.activemq.artemis.api.core.QueueConfiguration; +import org.apache.activemq.artemis.api.core.RoutingType; +import org.apache.activemq.artemis.api.core.SimpleString; +import org.apache.activemq.artemis.api.core.TransportConfiguration; +import org.apache.activemq.artemis.core.config.Configuration; +import org.apache.activemq.artemis.core.config.CoreAddressConfiguration; +import org.apache.activemq.artemis.core.config.impl.ConfigurationImpl; +import org.apache.activemq.artemis.core.remoting.impl.invm.InVMAcceptorFactory; +import org.apache.activemq.artemis.core.server.JournalType; +import org.apache.activemq.artemis.core.settings.impl.AddressSettings; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Configuration used to create the embedded Artemis server. + * + * @author Eddú Meléndez + * @author Stephane Nicoll + * @author Phillip Webb + */ +class ArtemisEmbeddedConfigurationFactory { + + private static final Log logger = LogFactory.getLog(ArtemisEmbeddedConfigurationFactory.class); + + private final ArtemisProperties.Embedded properties; + + ArtemisEmbeddedConfigurationFactory(ArtemisProperties properties) { + this.properties = properties.getEmbedded(); + } + + Configuration createConfiguration() { + ConfigurationImpl configuration = new ConfigurationImpl(); + configuration.setSecurityEnabled(false); + configuration.setPersistenceEnabled(this.properties.isPersistent()); + String dataDir = getDataDir(); + configuration.setJournalDirectory(dataDir + "/journal"); + if (this.properties.isPersistent()) { + configuration.setJournalType(JournalType.NIO); + configuration.setLargeMessagesDirectory(dataDir + "/largemessages"); + configuration.setBindingsDirectory(dataDir + "/bindings"); + configuration.setPagingDirectory(dataDir + "/paging"); + } + TransportConfiguration transportConfiguration = new TransportConfiguration(InVMAcceptorFactory.class.getName(), + this.properties.generateTransportParameters()); + configuration.getAcceptorConfigurations().add(transportConfiguration); + if (this.properties.isDefaultClusterPassword() && logger.isDebugEnabled()) { + logger.debug("Using default Artemis cluster password: " + this.properties.getClusterPassword()); + } + configuration.setClusterPassword(this.properties.getClusterPassword()); + configuration.addAddressConfiguration(createAddressConfiguration("DLQ")); + configuration.addAddressConfiguration(createAddressConfiguration("ExpiryQueue")); + configuration.addAddressesSetting("#", + new AddressSettings().setDeadLetterAddress(SimpleString.toSimpleString("DLQ")) + .setExpiryAddress(SimpleString.toSimpleString("ExpiryQueue"))); + return configuration; + } + + private CoreAddressConfiguration createAddressConfiguration(String name) { + return new CoreAddressConfiguration().setName(name).addRoutingType(RoutingType.ANYCAST).addQueueConfiguration( + new QueueConfiguration(name).setRoutingType(RoutingType.ANYCAST).setAddress(name)); + } + + private String getDataDir() { + if (this.properties.getDataDirectory() != null) { + return this.properties.getDataDirectory(); + } + String tempDirectory = System.getProperty("java.io.tmpdir"); + return new File(tempDirectory, "artemis-data").getAbsolutePath(); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisEmbeddedServerConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisEmbeddedServerConfiguration.java new file mode 100644 index 0000000000..336b87c695 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisEmbeddedServerConfiguration.java @@ -0,0 +1,126 @@ +/* + * Copyright 2012-2020 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.jms.artemis; + +import java.util.List; +import java.util.stream.Collectors; + +import org.apache.activemq.artemis.api.core.QueueConfiguration; +import org.apache.activemq.artemis.api.core.RoutingType; +import org.apache.activemq.artemis.core.config.CoreAddressConfiguration; +import org.apache.activemq.artemis.core.server.embedded.EmbeddedActiveMQ; +import org.apache.activemq.artemis.jms.server.config.JMSConfiguration; +import org.apache.activemq.artemis.jms.server.config.JMSQueueConfiguration; +import org.apache.activemq.artemis.jms.server.config.TopicConfiguration; +import org.apache.activemq.artemis.jms.server.config.impl.JMSConfigurationImpl; +import org.apache.activemq.artemis.jms.server.config.impl.JMSQueueConfigurationImpl; +import org.apache.activemq.artemis.jms.server.config.impl.TopicConfigurationImpl; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Configuration used to create the embedded Artemis server. + * + * @author Eddú Meléndez + * @author Phillip Webb + * @author Stephane Nicoll + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(EmbeddedActiveMQ.class) +@ConditionalOnProperty(prefix = "spring.artemis.embedded", name = "enabled", havingValue = "true", + matchIfMissing = true) +class ArtemisEmbeddedServerConfiguration { + + private final ArtemisProperties properties; + + ArtemisEmbeddedServerConfiguration(ArtemisProperties properties) { + this.properties = properties; + } + + @Bean + @ConditionalOnMissingBean + org.apache.activemq.artemis.core.config.Configuration artemisConfiguration() { + return new ArtemisEmbeddedConfigurationFactory(this.properties).createConfiguration(); + } + + @Bean(initMethod = "start", destroyMethod = "stop") + @ConditionalOnMissingBean + EmbeddedActiveMQ embeddedActiveMq(org.apache.activemq.artemis.core.config.Configuration configuration, + JMSConfiguration jmsConfiguration, ObjectProvider configurationCustomizers) + throws Exception { + for (JMSQueueConfiguration queueConfiguration : jmsConfiguration.getQueueConfigurations()) { + String queueName = queueConfiguration.getName(); + configuration.addAddressConfiguration( + new CoreAddressConfiguration().setName(queueName).addRoutingType(RoutingType.ANYCAST) + .addQueueConfiguration(new QueueConfiguration(queueName).setAddress(queueName) + .setFilterString(queueConfiguration.getSelector()) + .setDurable(queueConfiguration.isDurable()).setRoutingType(RoutingType.ANYCAST))); + } + for (TopicConfiguration topicConfiguration : jmsConfiguration.getTopicConfigurations()) { + configuration.addAddressConfiguration(new CoreAddressConfiguration().setName(topicConfiguration.getName()) + .addRoutingType(RoutingType.MULTICAST)); + } + configurationCustomizers.orderedStream().forEach((customizer) -> customizer.customize(configuration)); + EmbeddedActiveMQ embeddedActiveMq = new EmbeddedActiveMQ(); + embeddedActiveMq.setConfiguration(configuration); + return embeddedActiveMq; + } + + @Bean + @ConditionalOnMissingBean + JMSConfiguration artemisJmsConfiguration(ObjectProvider queuesConfiguration, + ObjectProvider topicsConfiguration) { + JMSConfiguration configuration = new JMSConfigurationImpl(); + addAll(configuration.getQueueConfigurations(), queuesConfiguration); + addAll(configuration.getTopicConfigurations(), topicsConfiguration); + addQueues(configuration, this.properties.getEmbedded().getQueues()); + addTopics(configuration, this.properties.getEmbedded().getTopics()); + return configuration; + } + + private void addAll(List list, ObjectProvider items) { + if (items != null) { + list.addAll(items.orderedStream().collect(Collectors.toList())); + } + } + + private void addQueues(JMSConfiguration configuration, String[] queues) { + boolean persistent = this.properties.getEmbedded().isPersistent(); + for (String queue : queues) { + JMSQueueConfigurationImpl jmsQueueConfiguration = new JMSQueueConfigurationImpl(); + jmsQueueConfiguration.setName(queue); + jmsQueueConfiguration.setDurable(persistent); + jmsQueueConfiguration.setBindings("/queue/" + queue); + configuration.getQueueConfigurations().add(jmsQueueConfiguration); + } + } + + private void addTopics(JMSConfiguration configuration, String[] topics) { + for (String topic : topics) { + TopicConfigurationImpl topicConfiguration = new TopicConfigurationImpl(); + topicConfiguration.setName(topic); + topicConfiguration.setBindings("/topic/" + topic); + configuration.getTopicConfigurations().add(topicConfiguration); + } + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisMode.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisMode.java new file mode 100644 index 0000000000..4237dbb0ae --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisMode.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2019 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.jms.artemis; + +/** + * Define the mode in which Artemis can operate. + * + * @author Eddú Meléndez + * @author Stephane Nicoll + * @since 1.3.0 + */ +public enum ArtemisMode { + + /** + * Connect to a broker using the native Artemis protocol (i.e. netty). + */ + NATIVE, + + /** + * Embed (i.e. start) the broker in the application. + */ + EMBEDDED + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisNoOpBindingRegistry.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisNoOpBindingRegistry.java new file mode 100644 index 0000000000..206c10dc60 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisNoOpBindingRegistry.java @@ -0,0 +1,48 @@ +/* + * Copyright 2012-2019 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.jms.artemis; + +import org.apache.activemq.artemis.spi.core.naming.BindingRegistry; + +/** + * A no-op implementation of the {@link BindingRegistry}. + * + * @author Eddú Meléndez + * @author Stephane Nicoll + * @since 1.3.0 + */ +public class ArtemisNoOpBindingRegistry implements BindingRegistry { + + @Override + public Object lookup(String s) { + return null; + } + + @Override + public boolean bind(String s, Object o) { + return false; + } + + @Override + public void unbind(String s) { + } + + @Override + public void close() { + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisProperties.java new file mode 100644 index 0000000000..2ec3381564 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisProperties.java @@ -0,0 +1,268 @@ +/* + * Copyright 2012-2021 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.jms.artemis; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.activemq.artemis.core.remoting.impl.invm.TransportConstants; + +import org.springframework.boot.autoconfigure.jms.JmsPoolConnectionFactoryProperties; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; +import org.springframework.boot.context.properties.NestedConfigurationProperty; + +/** + * Configuration properties for Artemis. + * + * @author Eddú Meléndez + * @author Stephane Nicoll + * @author Justin Bertram + * @since 1.3.0 + */ +@ConfigurationProperties(prefix = "spring.artemis") +public class ArtemisProperties { + + /** + * Artemis deployment mode, auto-detected by default. + */ + private ArtemisMode mode; + + /** + * Artemis broker port. + */ + private String brokerUrl; + + /** + * Artemis broker host. + */ + private String host; + + /** + * Artemis broker port. + */ + private int port = 61616; + + /** + * Login user of the broker. + */ + private String user; + + /** + * Login password of the broker. + */ + private String password; + + private final Embedded embedded = new Embedded(); + + @NestedConfigurationProperty + private final JmsPoolConnectionFactoryProperties pool = new JmsPoolConnectionFactoryProperties(); + + public ArtemisMode getMode() { + return this.mode; + } + + public void setMode(ArtemisMode mode) { + this.mode = mode; + } + + public String getBrokerUrl() { + return this.brokerUrl; + } + + public void setBrokerUrl(String brokerUrl) { + this.brokerUrl = brokerUrl; + } + + /** + * Return the host of the broker. + * @return the host + * @deprecated since 2.5.0 for removal in 2.7.0 in favor of broker url + */ + @Deprecated + @DeprecatedConfigurationProperty(replacement = "spring.artemis.broker-url") + public String getHost() { + return this.host; + } + + @Deprecated + public void setHost(String host) { + this.host = host; + } + + /** + * Return the port of the broker. + * @return the port + * @deprecated since 2.5.0 for removal in 2.7.0 in favor of broker url + */ + @Deprecated + @DeprecatedConfigurationProperty(replacement = "spring.artemis.broker-url") + public int getPort() { + return this.port; + } + + @Deprecated + public void setPort(int port) { + this.port = port; + } + + public String getUser() { + return this.user; + } + + public void setUser(String user) { + this.user = user; + } + + public String getPassword() { + return this.password; + } + + public void setPassword(String password) { + this.password = password; + } + + public Embedded getEmbedded() { + return this.embedded; + } + + public JmsPoolConnectionFactoryProperties getPool() { + return this.pool; + } + + /** + * Configuration for an embedded Artemis server. + */ + public static class Embedded { + + private static final AtomicInteger serverIdCounter = new AtomicInteger(); + + /** + * Server ID. By default, an auto-incremented counter is used. + */ + private int serverId = serverIdCounter.getAndIncrement(); + + /** + * Whether to enable embedded mode if the Artemis server APIs are available. + */ + private boolean enabled = true; + + /** + * Whether to enable persistent store. + */ + private boolean persistent; + + /** + * Journal file directory. Not necessary if persistence is turned off. + */ + private String dataDirectory; + + /** + * Comma-separated list of queues to create on startup. + */ + private String[] queues = new String[0]; + + /** + * Comma-separated list of topics to create on startup. + */ + private String[] topics = new String[0]; + + /** + * Cluster password. Randomly generated on startup by default. + */ + private String clusterPassword = UUID.randomUUID().toString(); + + private boolean defaultClusterPassword = true; + + public int getServerId() { + return this.serverId; + } + + public void setServerId(int serverId) { + this.serverId = serverId; + } + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public boolean isPersistent() { + return this.persistent; + } + + public void setPersistent(boolean persistent) { + this.persistent = persistent; + } + + public String getDataDirectory() { + return this.dataDirectory; + } + + public void setDataDirectory(String dataDirectory) { + this.dataDirectory = dataDirectory; + } + + public String[] getQueues() { + return this.queues; + } + + public void setQueues(String[] queues) { + this.queues = queues; + } + + public String[] getTopics() { + return this.topics; + } + + public void setTopics(String[] topics) { + this.topics = topics; + } + + public String getClusterPassword() { + return this.clusterPassword; + } + + public void setClusterPassword(String clusterPassword) { + this.clusterPassword = clusterPassword; + this.defaultClusterPassword = false; + } + + public boolean isDefaultClusterPassword() { + return this.defaultClusterPassword; + } + + /** + * Creates the minimal transport parameters for an embedded transport + * configuration. + * @return the transport parameters + * @see TransportConstants#SERVER_ID_PROP_NAME + */ + public Map generateTransportParameters() { + Map parameters = new HashMap<>(); + parameters.put(TransportConstants.SERVER_ID_PROP_NAME, getServerId()); + return parameters; + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisXAConnectionFactoryConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisXAConnectionFactoryConfiguration.java new file mode 100644 index 0000000000..77bff3666a --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisXAConnectionFactoryConfiguration.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-2019 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.jms.artemis; + +import javax.jms.ConnectionFactory; +import javax.transaction.TransactionManager; + +import org.apache.activemq.artemis.jms.client.ActiveMQXAConnectionFactory; + +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.jms.XAConnectionFactoryWrapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +/** + * Configuration for Artemis XA {@link ConnectionFactory}. + * + * @author Eddú Meléndez + * @author Phillip Webb + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnMissingBean(ConnectionFactory.class) +@ConditionalOnClass(TransactionManager.class) +@ConditionalOnBean(XAConnectionFactoryWrapper.class) +class ArtemisXAConnectionFactoryConfiguration { + + @Primary + @Bean(name = { "jmsConnectionFactory", "xaJmsConnectionFactory" }) + ConnectionFactory jmsConnectionFactory(ListableBeanFactory beanFactory, ArtemisProperties properties, + XAConnectionFactoryWrapper wrapper) throws Exception { + return wrapper.wrapConnectionFactory(new ArtemisConnectionFactoryFactory(beanFactory, properties) + .createConnectionFactory(ActiveMQXAConnectionFactory.class)); + } + + @Bean + ActiveMQXAConnectionFactory nonXaJmsConnectionFactory(ListableBeanFactory beanFactory, + ArtemisProperties properties) { + return new ArtemisConnectionFactoryFactory(beanFactory, properties) + .createConnectionFactory(ActiveMQXAConnectionFactory.class); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/package-info.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/package-info.java new file mode 100644 index 0000000000..9240970003 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright 2012-2019 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. + */ + +/** + * Auto-configuration for Artemis. + * + * @author Eddú Meléndez + */ +package org.springframework.boot.autoconfigure.jms.artemis; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/jta/JtaAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/jta/JtaAutoConfiguration.java index 3bd1853f74..532ccf70ab 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/jta/JtaAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/jta/JtaAutoConfiguration.java @@ -21,6 +21,7 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.jms.artemis.ArtemisAutoConfiguration; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @@ -36,7 +37,8 @@ import org.springframework.context.annotation.Import; @Configuration(proxyBeanMethods = false) @ConditionalOnClass(jakarta.transaction.Transaction.class) @ConditionalOnProperty(prefix = "spring.jta", value = "enabled", matchIfMissing = true) -@AutoConfigureBefore({ XADataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class }) +@AutoConfigureBefore({ XADataSourceAutoConfiguration.class, ArtemisAutoConfiguration.class, + HibernateJpaAutoConfiguration.class }) @Import(JndiJtaConfiguration.class) public class JtaAutoConfiguration { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index 367d9d890b..7305ba8c0a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -87,6 +87,7 @@ org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConf org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration,\ org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration,\ org.springframework.boot.autoconfigure.jms.JndiConnectionFactoryAutoConfiguration,\ +org.springframework.boot.autoconfigure.jms.artemis.ArtemisAutoConfiguration,\ org.springframework.boot.autoconfigure.jooq.JooqAutoConfiguration,\ org.springframework.boot.autoconfigure.jsonb.JsonbAutoConfiguration,\ org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration,\ diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java index efe991359a..6b88e80e26 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java @@ -22,10 +22,12 @@ import jakarta.jms.ConnectionFactory; import jakarta.jms.ExceptionListener; import jakarta.jms.Session; +import org.apache.activemq.artemis.jms.client.ActiveMQConnectionFactory; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.jms.artemis.ArtemisAutoConfiguration; import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; @@ -38,6 +40,7 @@ import org.springframework.jms.config.JmsListenerConfigUtils; import org.springframework.jms.config.JmsListenerContainerFactory; import org.springframework.jms.config.JmsListenerEndpoint; import org.springframework.jms.config.SimpleJmsListenerContainerFactory; +import org.springframework.jms.connection.CachingConnectionFactory; import org.springframework.jms.core.JmsMessagingTemplate; import org.springframework.jms.core.JmsTemplate; import org.springframework.jms.listener.DefaultMessageListenerContainer; @@ -59,8 +62,32 @@ import static org.mockito.Mockito.mock; class JmsAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(JmsAutoConfiguration.class)) - .withUserConfiguration(ConnectionFactoryConfiguration.class); + .withConfiguration(AutoConfigurations.of(ArtemisAutoConfiguration.class, JmsAutoConfiguration.class)); + + @Test + void testDefaultJmsConfiguration() { + this.contextRunner.withUserConfiguration(TestConfiguration.class).run(this::testDefaultJmsConfiguration); + } + + private void testDefaultJmsConfiguration(AssertableApplicationContext loaded) { + assertThat(loaded).hasSingleBean(ConnectionFactory.class); + assertThat(loaded).hasSingleBean(CachingConnectionFactory.class); + CachingConnectionFactory factory = loaded.getBean(CachingConnectionFactory.class); + assertThat(factory.getTargetConnectionFactory()).isInstanceOf(ActiveMQConnectionFactory.class); + JmsTemplate jmsTemplate = loaded.getBean(JmsTemplate.class); + JmsMessagingTemplate messagingTemplate = loaded.getBean(JmsMessagingTemplate.class); + assertThat(factory).isEqualTo(jmsTemplate.getConnectionFactory()); + assertThat(messagingTemplate.getJmsTemplate()).isEqualTo(jmsTemplate); + assertThat(getBrokerUrl(factory)).startsWith("vm://"); + assertThat(loaded.containsBean("jmsListenerContainerFactory")).isTrue(); + } + + @Test + void testConnectionFactoryBackOff() { + this.contextRunner.withUserConfiguration(TestConfiguration2.class) + .run((context) -> assertThat(context.getBeansOfType(ActiveMQConnectionFactory.class)) + .containsOnlyKeys("customConnectionFactory")); + } @Test void testJmsTemplateBackOff() { @@ -77,13 +104,15 @@ class JmsAutoConfigurationTests { @Test void testJmsTemplateBackOffEverything() { - this.contextRunner.withUserConfiguration(TestConfiguration3.class, TestConfiguration5.class) + this.contextRunner + .withUserConfiguration(TestConfiguration2.class, TestConfiguration3.class, TestConfiguration5.class) .run(this::testJmsTemplateBackOffEverything); } private void testJmsTemplateBackOffEverything(AssertableApplicationContext loaded) throws IOException { JmsTemplate jmsTemplate = loaded.getBean(JmsTemplate.class); assertThat(jmsTemplate.getPriority()).isEqualTo(999); + assertThat(loaded.getBeansOfType(ActiveMQConnectionFactory.class)).containsOnlyKeys("customConnectionFactory"); JmsMessagingTemplate messagingTemplate = loaded.getBean(JmsMessagingTemplate.class); assertThat(messagingTemplate.getDefaultDestinationName()).isEqualTo("fooBar"); assertThat(messagingTemplate.getJmsTemplate()).isEqualTo(jmsTemplate); @@ -288,6 +317,16 @@ class JmsAutoConfigurationTests { }); } + private String getBrokerUrl(CachingConnectionFactory connectionFactory) { + assertThat(connectionFactory.getTargetConnectionFactory()).isInstanceOf(ActiveMQConnectionFactory.class); + try { + return ((ActiveMQConnectionFactory) connectionFactory.getTargetConnectionFactory()).toURI().toString(); + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + } + @Test void enableJmsAutomatically() { this.contextRunner.withUserConfiguration(NoEnableJmsConfiguration.class) @@ -297,17 +336,17 @@ class JmsAutoConfigurationTests { } @Configuration(proxyBeanMethods = false) - static class ConnectionFactoryConfiguration { - - @Bean - ConnectionFactory connectionFactory() { - return mock(ConnectionFactory.class); - } + static class TestConfiguration { } @Configuration(proxyBeanMethods = false) - static class TestConfiguration { + static class TestConfiguration2 { + + @Bean + ConnectionFactory customConnectionFactory() { + return new ActiveMQConnectionFactory(); + } } @@ -437,12 +476,12 @@ class JmsAutoConfigurationTests { @Bean ConnectionFactory connectionFactory1() { - return mock(ConnectionFactory.class); + return new ActiveMQConnectionFactory(); } @Bean ConnectionFactory connectionFactory2() { - return mock(ConnectionFactory.class); + return new ActiveMQConnectionFactory(); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisAutoConfigurationTests.java new file mode 100644 index 0000000000..515a4306bf --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisAutoConfigurationTests.java @@ -0,0 +1,501 @@ +/* + * Copyright 2012-2020 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.jms.artemis; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.UUID; + +import javax.jms.ConnectionFactory; +import javax.jms.Message; +import javax.jms.TextMessage; + +import org.apache.activemq.artemis.api.core.RoutingType; +import org.apache.activemq.artemis.api.core.SimpleString; +import org.apache.activemq.artemis.api.core.TransportConfiguration; +import org.apache.activemq.artemis.core.remoting.impl.invm.InVMConnectorFactory; +import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnectorFactory; +import org.apache.activemq.artemis.core.server.ActiveMQServer; +import org.apache.activemq.artemis.core.server.BindingQueryResult; +import org.apache.activemq.artemis.core.server.embedded.EmbeddedActiveMQ; +import org.apache.activemq.artemis.jms.client.ActiveMQConnectionFactory; +import org.apache.activemq.artemis.jms.server.config.JMSConfiguration; +import org.apache.activemq.artemis.jms.server.config.JMSQueueConfiguration; +import org.apache.activemq.artemis.jms.server.config.TopicConfiguration; +import org.apache.activemq.artemis.jms.server.config.impl.JMSConfigurationImpl; +import org.apache.activemq.artemis.jms.server.config.impl.JMSQueueConfigurationImpl; +import org.apache.activemq.artemis.jms.server.config.impl.TopicConfigurationImpl; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.messaginghub.pooled.jms.JmsPoolConnectionFactory; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration; +import org.springframework.boot.test.context.assertj.AssertableApplicationContext; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jms.connection.CachingConnectionFactory; +import org.springframework.jms.core.JmsTemplate; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ArtemisAutoConfiguration}. + * + * @author Eddú Meléndez + * @author Stephane Nicoll + */ +class ArtemisAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ArtemisAutoConfiguration.class, JmsAutoConfiguration.class)); + + @Test + void connectionFactoryIsCachedByDefault() { + this.contextRunner.withUserConfiguration(EmptyConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(ConnectionFactory.class).hasSingleBean(CachingConnectionFactory.class) + .hasBean("jmsConnectionFactory"); + CachingConnectionFactory connectionFactory = context.getBean(CachingConnectionFactory.class); + assertThat(context.getBean("jmsConnectionFactory")).isSameAs(connectionFactory); + assertThat(connectionFactory.getTargetConnectionFactory()).isInstanceOf(ActiveMQConnectionFactory.class); + assertThat(connectionFactory.isCacheConsumers()).isFalse(); + assertThat(connectionFactory.isCacheProducers()).isTrue(); + assertThat(connectionFactory.getSessionCacheSize()).isEqualTo(1); + }); + } + + @Test + void connectionFactoryCachingCanBeCustomized() { + this.contextRunner.withUserConfiguration(EmptyConfiguration.class) + .withPropertyValues("spring.jms.cache.consumers=true", "spring.jms.cache.producers=false", + "spring.jms.cache.session-cache-size=10") + .run((context) -> { + assertThat(context).hasSingleBean(ConnectionFactory.class) + .hasSingleBean(CachingConnectionFactory.class).hasBean("jmsConnectionFactory"); + CachingConnectionFactory connectionFactory = context.getBean(CachingConnectionFactory.class); + assertThat(context.getBean("jmsConnectionFactory")).isSameAs(connectionFactory); + assertThat(connectionFactory.isCacheConsumers()).isTrue(); + assertThat(connectionFactory.isCacheProducers()).isFalse(); + assertThat(connectionFactory.getSessionCacheSize()).isEqualTo(10); + }); + } + + @Test + void connectionFactoryCachingCanBeDisabled() { + this.contextRunner.withUserConfiguration(EmptyConfiguration.class) + .withPropertyValues("spring.jms.cache.enabled=false").run((context) -> { + assertThat(context).doesNotHaveBean(CachingConnectionFactory.class); + ConnectionFactory connectionFactory = getConnectionFactory(context); + assertThat(connectionFactory).isInstanceOf(ActiveMQConnectionFactory.class); + }); + } + + @Test + void nativeConnectionFactory() { + this.contextRunner.withUserConfiguration(EmptyConfiguration.class) + .withPropertyValues("spring.artemis.mode:native").run((context) -> { + JmsTemplate jmsTemplate = context.getBean(JmsTemplate.class); + ConnectionFactory connectionFactory = getConnectionFactory(context); + assertThat(connectionFactory).isEqualTo(jmsTemplate.getConnectionFactory()); + ActiveMQConnectionFactory activeMQConnectionFactory = getActiveMQConnectionFactory( + connectionFactory); + assertNettyConnectionFactory(activeMQConnectionFactory, "localhost", 61616); + assertThat(activeMQConnectionFactory.getUser()).isNull(); + assertThat(activeMQConnectionFactory.getPassword()).isNull(); + }); + } + + @Test + void nativeConnectionFactoryCustomBrokerUrl() { + this.contextRunner.withUserConfiguration(EmptyConfiguration.class) + .withPropertyValues("spring.artemis.mode:native", "spring.artemis.broker-url:tcp://192.168.1.144:9876") + .run((context) -> assertNettyConnectionFactory( + getActiveMQConnectionFactory(getConnectionFactory(context)), "192.168.1.144", 9876)); + } + + @Test + @Deprecated + void nativeConnectionFactoryCustomHost() { + this.contextRunner.withUserConfiguration(EmptyConfiguration.class) + .withPropertyValues("spring.artemis.mode:native", "spring.artemis.host:192.168.1.144", + "spring.artemis.port:9876") + .run((context) -> assertNettyConnectionFactory( + getActiveMQConnectionFactory(getConnectionFactory(context)), "192.168.1.144", 9876)); + } + + @Test + @Deprecated + void nativeConnectionFactoryCustomBrokerUrlAndHost() { + this.contextRunner.withUserConfiguration(EmptyConfiguration.class) + .withPropertyValues("spring.artemis.mode:native", "spring.artemis.host:192.168.1.144", + "spring.artemis.port:9876", "spring.artemis.broker-url=tcp://192.168.1.221:6543") + .run((context) -> assertNettyConnectionFactory( + getActiveMQConnectionFactory(getConnectionFactory(context)), "192.168.1.221", 6543)); + } + + @Test + void nativeConnectionFactoryCredentials() { + this.contextRunner.withUserConfiguration(EmptyConfiguration.class) + .withPropertyValues("spring.artemis.mode:native", "spring.artemis.user:user", + "spring.artemis.password:secret") + .run((context) -> { + JmsTemplate jmsTemplate = context.getBean(JmsTemplate.class); + ConnectionFactory connectionFactory = getConnectionFactory(context); + assertThat(connectionFactory).isEqualTo(jmsTemplate.getConnectionFactory()); + ActiveMQConnectionFactory activeMQConnectionFactory = getActiveMQConnectionFactory( + connectionFactory); + assertNettyConnectionFactory(activeMQConnectionFactory, "localhost", 61616); + assertThat(activeMQConnectionFactory.getUser()).isEqualTo("user"); + assertThat(activeMQConnectionFactory.getPassword()).isEqualTo("secret"); + }); + } + + @Test + void embeddedConnectionFactory() { + this.contextRunner.withUserConfiguration(EmptyConfiguration.class) + .withPropertyValues("spring.artemis.mode:embedded").run((context) -> { + ArtemisProperties properties = context.getBean(ArtemisProperties.class); + assertThat(properties.getMode()).isEqualTo(ArtemisMode.EMBEDDED); + assertThat(context).hasSingleBean(EmbeddedActiveMQ.class); + org.apache.activemq.artemis.core.config.Configuration configuration = context + .getBean(org.apache.activemq.artemis.core.config.Configuration.class); + assertThat(configuration.isPersistenceEnabled()).isFalse(); + assertThat(configuration.isSecurityEnabled()).isFalse(); + assertInVmConnectionFactory(getActiveMQConnectionFactory(getConnectionFactory(context))); + }); + } + + @Test + void embeddedConnectionFactoryByDefault() { + // No mode is specified + this.contextRunner.withUserConfiguration(EmptyConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(EmbeddedActiveMQ.class); + org.apache.activemq.artemis.core.config.Configuration configuration = context + .getBean(org.apache.activemq.artemis.core.config.Configuration.class); + assertThat(configuration.isPersistenceEnabled()).isFalse(); + assertThat(configuration.isSecurityEnabled()).isFalse(); + assertInVmConnectionFactory(getActiveMQConnectionFactory(getConnectionFactory(context))); + }); + } + + @Test + void nativeConnectionFactoryIfEmbeddedServiceDisabledExplicitly() { + // No mode is specified + this.contextRunner.withUserConfiguration(EmptyConfiguration.class) + .withPropertyValues("spring.artemis.embedded.enabled:false").run((context) -> { + assertThat(context).doesNotHaveBean(ActiveMQServer.class); + assertNettyConnectionFactory(getActiveMQConnectionFactory(getConnectionFactory(context)), + "localhost", 61616); + }); + } + + @Test + void embeddedConnectionFactoryEvenIfEmbeddedServiceDisabled() { + // No mode is specified + this.contextRunner.withUserConfiguration(EmptyConfiguration.class) + .withPropertyValues("spring.artemis.mode:embedded", "spring.artemis.embedded.enabled:false") + .run((context) -> { + assertThat(context.getBeansOfType(ActiveMQServer.class)).isEmpty(); + assertInVmConnectionFactory(getActiveMQConnectionFactory(getConnectionFactory(context))); + }); + } + + @Test + void embeddedServerWithDestinations() { + this.contextRunner.withUserConfiguration(EmptyConfiguration.class) + .withPropertyValues("spring.artemis.embedded.queues=Queue1,Queue2", + "spring.artemis.embedded.topics=Topic1") + .run((context) -> { + DestinationChecker checker = new DestinationChecker(context); + checker.checkQueue("Queue1", true); + checker.checkQueue("Queue2", true); + checker.checkQueue("NonExistentQueue", false); + checker.checkTopic("Topic1", true); + checker.checkTopic("NonExistentTopic", false); + }); + } + + @Test + void embeddedServerWithDestinationConfig() { + this.contextRunner.withUserConfiguration(DestinationConfiguration.class).run((context) -> { + DestinationChecker checker = new DestinationChecker(context); + checker.checkQueue("sampleQueue", true); + checker.checkTopic("sampleTopic", true); + }); + } + + @Test + void embeddedServiceWithCustomJmsConfiguration() { + // Ignored with custom config + this.contextRunner.withUserConfiguration(CustomJmsConfiguration.class) + .withPropertyValues("spring.artemis.embedded.queues=Queue1,Queue2").run((context) -> { + DestinationChecker checker = new DestinationChecker(context); + checker.checkQueue("custom", true); // See CustomJmsConfiguration + checker.checkQueue("Queue1", false); + checker.checkQueue("Queue2", false); + }); + } + + @Test + void embeddedServiceWithCustomArtemisConfiguration() { + this.contextRunner.withUserConfiguration(CustomArtemisConfiguration.class) + .run((context) -> assertThat( + context.getBean(org.apache.activemq.artemis.core.config.Configuration.class).getName()) + .isEqualTo("customFooBar")); + } + + @Test + void embeddedWithPersistentMode(@TempDir Path temp) throws IOException { + File dataDirectory = Files.createTempDirectory(temp, null).toFile(); + final String messageId = UUID.randomUUID().toString(); + // Start the server and post a message to some queue + this.contextRunner.withUserConfiguration(EmptyConfiguration.class) + .withPropertyValues("spring.artemis.embedded.queues=TestQueue", + "spring.artemis.embedded.persistent:true", + "spring.artemis.embedded.dataDirectory:" + dataDirectory.getAbsolutePath()) + .run((context) -> context.getBean(JmsTemplate.class).send("TestQueue", + (session) -> session.createTextMessage(messageId))) + .run((context) -> { + // Start the server again and check if our message is still here + JmsTemplate jmsTemplate2 = context.getBean(JmsTemplate.class); + jmsTemplate2.setReceiveTimeout(1000L); + Message message = jmsTemplate2.receive("TestQueue"); + assertThat(message).isNotNull(); + assertThat(((TextMessage) message).getText()).isEqualTo(messageId); + }); + } + + @Test + void severalEmbeddedBrokers() { + this.contextRunner.withUserConfiguration(EmptyConfiguration.class) + .withPropertyValues("spring.artemis.embedded.queues=Queue1").run((first) -> { + this.contextRunner.withPropertyValues("spring.artemis.embedded.queues=Queue2").run((second) -> { + ArtemisProperties firstProperties = first.getBean(ArtemisProperties.class); + ArtemisProperties secondProperties = second.getBean(ArtemisProperties.class); + assertThat(firstProperties.getEmbedded().getServerId()) + .isLessThan(secondProperties.getEmbedded().getServerId()); + DestinationChecker firstChecker = new DestinationChecker(first); + firstChecker.checkQueue("Queue1", true); + firstChecker.checkQueue("Queue2", false); + DestinationChecker secondChecker = new DestinationChecker(second); + secondChecker.checkQueue("Queue1", false); + secondChecker.checkQueue("Queue2", true); + }); + }); + } + + @Test + void connectToASpecificEmbeddedBroker() { + this.contextRunner.withUserConfiguration(EmptyConfiguration.class) + .withPropertyValues("spring.artemis.embedded.serverId=93", "spring.artemis.embedded.queues=Queue1") + .run((first) -> { + this.contextRunner.withUserConfiguration(EmptyConfiguration.class) + .withPropertyValues("spring.artemis.mode=embedded", + // Connect to the "main" broker + "spring.artemis.embedded.serverId=93", + // Do not start a specific one + "spring.artemis.embedded.enabled=false") + .run((secondContext) -> { + first.getBean(JmsTemplate.class).convertAndSend("Queue1", "test"); + assertThat(secondContext.getBean(JmsTemplate.class).receiveAndConvert("Queue1")) + .isEqualTo("test"); + }); + }); + } + + @Test + void defaultPoolConnectionFactoryIsApplied() { + this.contextRunner.withPropertyValues("spring.artemis.pool.enabled=true").run((context) -> { + assertThat(context.getBeansOfType(JmsPoolConnectionFactory.class)).hasSize(1); + JmsPoolConnectionFactory connectionFactory = context.getBean(JmsPoolConnectionFactory.class); + JmsPoolConnectionFactory defaultFactory = new JmsPoolConnectionFactory(); + assertThat(connectionFactory.isBlockIfSessionPoolIsFull()) + .isEqualTo(defaultFactory.isBlockIfSessionPoolIsFull()); + assertThat(connectionFactory.getBlockIfSessionPoolIsFullTimeout()) + .isEqualTo(defaultFactory.getBlockIfSessionPoolIsFullTimeout()); + assertThat(connectionFactory.getConnectionIdleTimeout()) + .isEqualTo(defaultFactory.getConnectionIdleTimeout()); + assertThat(connectionFactory.getMaxConnections()).isEqualTo(defaultFactory.getMaxConnections()); + assertThat(connectionFactory.getMaxSessionsPerConnection()) + .isEqualTo(defaultFactory.getMaxSessionsPerConnection()); + assertThat(connectionFactory.getConnectionCheckInterval()) + .isEqualTo(defaultFactory.getConnectionCheckInterval()); + assertThat(connectionFactory.isUseAnonymousProducers()).isEqualTo(defaultFactory.isUseAnonymousProducers()); + }); + } + + @Test + void customPoolConnectionFactoryIsApplied() { + this.contextRunner + .withPropertyValues("spring.artemis.pool.enabled=true", "spring.artemis.pool.blockIfFull=false", + "spring.artemis.pool.blockIfFullTimeout=64", "spring.artemis.pool.idleTimeout=512", + "spring.artemis.pool.maxConnections=256", "spring.artemis.pool.maxSessionsPerConnection=1024", + "spring.artemis.pool.timeBetweenExpirationCheck=2048", + "spring.artemis.pool.useAnonymousProducers=false") + .run((context) -> { + assertThat(context.getBeansOfType(JmsPoolConnectionFactory.class)).hasSize(1); + JmsPoolConnectionFactory connectionFactory = context.getBean(JmsPoolConnectionFactory.class); + assertThat(connectionFactory.isBlockIfSessionPoolIsFull()).isFalse(); + assertThat(connectionFactory.getBlockIfSessionPoolIsFullTimeout()).isEqualTo(64); + assertThat(connectionFactory.getConnectionIdleTimeout()).isEqualTo(512); + assertThat(connectionFactory.getMaxConnections()).isEqualTo(256); + assertThat(connectionFactory.getMaxSessionsPerConnection()).isEqualTo(1024); + assertThat(connectionFactory.getConnectionCheckInterval()).isEqualTo(2048); + assertThat(connectionFactory.isUseAnonymousProducers()).isFalse(); + }); + } + + @Test + void poolConnectionFactoryConfiguration() { + this.contextRunner.withPropertyValues("spring.artemis.pool.enabled:true").run((context) -> { + ConnectionFactory factory = getConnectionFactory(context); + assertThat(factory).isInstanceOf(JmsPoolConnectionFactory.class); + context.getSourceApplicationContext().close(); + assertThat(factory.createConnection()).isNull(); + }); + } + + private ConnectionFactory getConnectionFactory(AssertableApplicationContext context) { + assertThat(context).hasSingleBean(ConnectionFactory.class).hasBean("jmsConnectionFactory"); + ConnectionFactory connectionFactory = context.getBean(ConnectionFactory.class); + assertThat(connectionFactory).isSameAs(context.getBean("jmsConnectionFactory")); + return connectionFactory; + } + + private ActiveMQConnectionFactory getActiveMQConnectionFactory(ConnectionFactory connectionFactory) { + assertThat(connectionFactory).isInstanceOf(CachingConnectionFactory.class); + return (ActiveMQConnectionFactory) ((CachingConnectionFactory) connectionFactory).getTargetConnectionFactory(); + } + + private TransportConfiguration assertInVmConnectionFactory(ActiveMQConnectionFactory connectionFactory) { + TransportConfiguration transportConfig = getSingleTransportConfiguration(connectionFactory); + assertThat(transportConfig.getFactoryClassName()).isEqualTo(InVMConnectorFactory.class.getName()); + return transportConfig; + } + + private TransportConfiguration assertNettyConnectionFactory(ActiveMQConnectionFactory connectionFactory, + String host, int port) { + TransportConfiguration transportConfig = getSingleTransportConfiguration(connectionFactory); + assertThat(transportConfig.getFactoryClassName()).isEqualTo(NettyConnectorFactory.class.getName()); + assertThat(transportConfig.getParams().get("host")).isEqualTo(host); + Object transportConfigPort = transportConfig.getParams().get("port"); + if (transportConfigPort instanceof String) { + transportConfigPort = Integer.parseInt((String) transportConfigPort); + } + assertThat(transportConfigPort).isEqualTo(port); + return transportConfig; + } + + private TransportConfiguration getSingleTransportConfiguration(ActiveMQConnectionFactory connectionFactory) { + TransportConfiguration[] transportConfigurations = connectionFactory.getServerLocator() + .getStaticTransportConfigurations(); + assertThat(transportConfigurations).hasSize(1); + return transportConfigurations[0]; + } + + private static final class DestinationChecker { + + private final ActiveMQServer server; + + private DestinationChecker(ApplicationContext applicationContext) { + this.server = applicationContext.getBean(EmbeddedActiveMQ.class).getActiveMQServer(); + } + + void checkQueue(String name, boolean shouldExist) { + checkDestination(name, RoutingType.ANYCAST, shouldExist); + } + + void checkTopic(String name, boolean shouldExist) { + checkDestination(name, RoutingType.MULTICAST, shouldExist); + } + + void checkDestination(String name, RoutingType routingType, boolean shouldExist) { + try { + BindingQueryResult result = this.server.bindingQuery(new SimpleString(name)); + assertThat(result.isExists()).isEqualTo(shouldExist); + if (shouldExist) { + assertThat(result.getAddressInfo().getRoutingType()).isEqualTo(routingType); + } + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + } + + @Configuration(proxyBeanMethods = false) + static class EmptyConfiguration { + + } + + @Configuration(proxyBeanMethods = false) + static class DestinationConfiguration { + + @Bean + JMSQueueConfiguration sampleQueueConfiguration() { + JMSQueueConfigurationImpl jmsQueueConfiguration = new JMSQueueConfigurationImpl(); + jmsQueueConfiguration.setName("sampleQueue"); + jmsQueueConfiguration.setSelector("foo=bar"); + jmsQueueConfiguration.setDurable(false); + jmsQueueConfiguration.setBindings("/queue/1"); + return jmsQueueConfiguration; + } + + @Bean + TopicConfiguration sampleTopicConfiguration() { + TopicConfigurationImpl topicConfiguration = new TopicConfigurationImpl(); + topicConfiguration.setName("sampleTopic"); + topicConfiguration.setBindings("/topic/1"); + return topicConfiguration; + } + + } + + @Configuration(proxyBeanMethods = false) + static class CustomJmsConfiguration { + + @Bean + JMSConfiguration myJmsConfiguration() { + JMSConfiguration config = new JMSConfigurationImpl(); + JMSQueueConfiguration jmsQueueConfiguration = new JMSQueueConfigurationImpl(); + jmsQueueConfiguration.setName("custom"); + jmsQueueConfiguration.setDurable(false); + config.getQueueConfigurations().add(jmsQueueConfiguration); + return config; + } + + } + + @Configuration(proxyBeanMethods = false) + static class CustomArtemisConfiguration { + + @Bean + ArtemisConfigurationCustomizer myArtemisCustomize() { + return (configuration) -> { + configuration.setClusterPassword("Foobar"); + configuration.setName("customFooBar"); + }; + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisEmbeddedConfigurationFactoryTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisEmbeddedConfigurationFactoryTests.java new file mode 100644 index 0000000000..f6030a87af --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisEmbeddedConfigurationFactoryTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2012-2019 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.jms.artemis; + +import java.util.List; +import java.util.Map; + +import org.apache.activemq.artemis.api.core.SimpleString; +import org.apache.activemq.artemis.core.config.Configuration; +import org.apache.activemq.artemis.core.config.CoreAddressConfiguration; +import org.apache.activemq.artemis.core.server.JournalType; +import org.apache.activemq.artemis.core.settings.impl.AddressSettings; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ArtemisEmbeddedConfigurationFactory} + * + * @author Eddú Meléndez + * @author Stephane Nicoll + * @author Phillip Webb + */ +class ArtemisEmbeddedConfigurationFactoryTests { + + @Test + void defaultDataDir() { + ArtemisProperties properties = new ArtemisProperties(); + properties.getEmbedded().setPersistent(true); + Configuration configuration = new ArtemisEmbeddedConfigurationFactory(properties).createConfiguration(); + assertThat(configuration.getJournalDirectory()).startsWith(System.getProperty("java.io.tmpdir")) + .endsWith("/journal"); + } + + @Test + void persistenceSetup() { + ArtemisProperties properties = new ArtemisProperties(); + properties.getEmbedded().setPersistent(true); + Configuration configuration = new ArtemisEmbeddedConfigurationFactory(properties).createConfiguration(); + assertThat(configuration.isPersistenceEnabled()).isTrue(); + assertThat(configuration.getJournalType()).isEqualTo(JournalType.NIO); + } + + @Test + void generatedClusterPassword() { + ArtemisProperties properties = new ArtemisProperties(); + Configuration configuration = new ArtemisEmbeddedConfigurationFactory(properties).createConfiguration(); + assertThat(configuration.getClusterPassword()).hasSize(36); + } + + @Test + void specificClusterPassword() { + ArtemisProperties properties = new ArtemisProperties(); + properties.getEmbedded().setClusterPassword("password"); + Configuration configuration = new ArtemisEmbeddedConfigurationFactory(properties).createConfiguration(); + assertThat(configuration.getClusterPassword()).isEqualTo("password"); + } + + @Test + void hasDlqExpiryQueueAddressSettingsConfigured() { + ArtemisProperties properties = new ArtemisProperties(); + Configuration configuration = new ArtemisEmbeddedConfigurationFactory(properties).createConfiguration(); + Map addressesSettings = configuration.getAddressesSettings(); + assertThat((Object) addressesSettings.get("#").getDeadLetterAddress()) + .isEqualTo(SimpleString.toSimpleString("DLQ")); + assertThat((Object) addressesSettings.get("#").getExpiryAddress()) + .isEqualTo(SimpleString.toSimpleString("ExpiryQueue")); + } + + @Test + void hasDlqExpiryQueueConfigured() { + ArtemisProperties properties = new ArtemisProperties(); + Configuration configuration = new ArtemisEmbeddedConfigurationFactory(properties).createConfiguration(); + List addressConfigurations = configuration.getAddressConfigurations(); + assertThat(addressConfigurations).hasSize(2); + } + +} diff --git a/spring-boot-project/spring-boot-cli/build.gradle b/spring-boot-project/spring-boot-cli/build.gradle index 0189ffe969..9608a448db 100644 --- a/spring-boot-project/spring-boot-cli/build.gradle +++ b/spring-boot-project/spring-boot-cli/build.gradle @@ -73,6 +73,7 @@ dependencies { testRepository(project(path: ":spring-boot-project:spring-boot-starters:spring-boot-starter-actuator", configuration: "mavenRepository")) testRepository(project(path: ":spring-boot-project:spring-boot-starters:spring-boot-starter-amqp", configuration: "mavenRepository")) testRepository(project(path: ":spring-boot-project:spring-boot-starters:spring-boot-starter-aop", configuration: "mavenRepository")) + testRepository(project(path: ":spring-boot-project:spring-boot-starters:spring-boot-starter-artemis", configuration: "mavenRepository")) testRepository(project(path: ":spring-boot-project:spring-boot-starters:spring-boot-starter-batch", configuration: "mavenRepository")) testRepository(project(path: ":spring-boot-project:spring-boot-starters:spring-boot-starter-data-jpa", configuration: "mavenRepository")) testRepository(project(path: ":spring-boot-project:spring-boot-starters:spring-boot-starter-jdbc", configuration: "mavenRepository")) diff --git a/spring-boot-project/spring-boot-cli/src/test/java/org/springframework/boot/cli/SampleIntegrationTests.java b/spring-boot-project/spring-boot-cli/src/test/java/org/springframework/boot/cli/SampleIntegrationTests.java index 6b7f8f7ead..3ce11e4ed0 100644 --- a/spring-boot-project/spring-boot-cli/src/test/java/org/springframework/boot/cli/SampleIntegrationTests.java +++ b/spring-boot-project/spring-boot-cli/src/test/java/org/springframework/boot/cli/SampleIntegrationTests.java @@ -118,7 +118,6 @@ class SampleIntegrationTests { } @Test - @Disabled("Requires Artemis to be run, so disable it") void jmsSample() throws Exception { System.setProperty("spring.artemis.embedded.queues", "spring-boot"); try { diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 366ed7dd00..3b26e86395 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -31,6 +31,32 @@ bom { ] } } + library("Artemis", "2.19.0") { + group("org.apache.activemq") { + modules = [ + "artemis-amqp-protocol", + "artemis-commons" { + exclude group: "commons-logging", module: "commons-logging" + }, + "artemis-core-client" { + exclude group: "org.apache.geronimo.specs", module: "geronimo-json_1.0_spec" + }, + "artemis-jms-client" { + exclude group: "org.apache.geronimo.specs", module: "geronimo-json_1.0_spec" + }, + "artemis-jms-server" { + exclude group: "org.apache.geronimo.specs", module: "geronimo-json_1.0_spec" + }, + "artemis-journal", + "artemis-selector", + "artemis-server" { + exclude group: "commons-logging", module: "commons-logging" + exclude group: "org.apache.geronimo.specs", module: "geronimo-json_1.0_spec" + }, + "artemis-service-extensions" + ] + } + } library("AspectJ", "1.9.7") { group("org.aspectj") { modules = [ diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/documentation/messaging.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/documentation/messaging.adoc index 40c435ca1d..51412fde0c 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/documentation/messaging.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/documentation/messaging.adoc @@ -2,7 +2,7 @@ == Messaging If your application uses any messaging protocol, see one or more of the following sections: -* *JMS:* <> +* *JMS:* <> * *AMQP:* <> * *Kafka:* <> * *RSocket:* <> diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/messaging/jms.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/messaging/jms.adoc index 3a46a505f6..ac97ad28f0 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/messaging/jms.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/messaging/jms.adoc @@ -7,6 +7,59 @@ Spring Boot also auto-configures the necessary infrastructure to send and receiv +[[messaging.jms.artemis]] +=== ActiveMQ Artemis Support +Spring Boot can auto-configure a `ConnectionFactory` when it detects that https://activemq.apache.org/components/artemis/[ActiveMQ Artemis] is available on the classpath. +If the broker is present, an embedded broker is automatically started and configured (unless the mode property has been explicitly set). +The supported modes are `embedded` (to make explicit that an embedded broker is required and that an error should occur if the broker is not available on the classpath) and `native` (to connect to a broker using the `netty` transport protocol). +When the latter is configured, Spring Boot configures a `ConnectionFactory` that connects to a broker running on the local machine with the default settings. + +NOTE: If you use `spring-boot-starter-artemis`, the necessary dependencies to connect to an existing ActiveMQ Artemis instance are provided, as well as the Spring infrastructure to integrate with JMS. +Adding `org.apache.activemq:artemis-jms-server` to your application lets you use embedded mode. + +ActiveMQ Artemis configuration is controlled by external configuration properties in `+spring.artemis.*+`. +For example, you might declare the following section in `application.properties`: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + artemis: + mode: native + broker-url: "tcp://192.168.1.210:9876" + user: "admin" + password: "secret" +---- + +When embedding the broker, you can choose if you want to enable persistence and list the destinations that should be made available. +These can be specified as a comma-separated list to create them with the default options, or you can define bean(s) of type `org.apache.activemq.artemis.jms.server.config.JMSQueueConfiguration` or `org.apache.activemq.artemis.jms.server.config.TopicConfiguration`, for advanced queue and topic configurations, respectively. + +By default, a `CachingConnectionFactory` wraps the native `ConnectionFactory` with sensible settings that you can control by external configuration properties in `+spring.jms.*+`: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + jms: + cache: + session-cache-size: 5 +---- + +If you'd rather use native pooling, you can do so by adding a dependency to `org.messaginghub:pooled-jms` and configuring the `JmsPoolConnectionFactory` accordingly, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + artemis: + pool: + enabled: true + max-connections: 50 +---- + +See {spring-boot-autoconfigure-module-code}/jms/artemis/ArtemisProperties.java[`ArtemisProperties`] for more supported options. + +No JNDI lookup is involved, and destinations are resolved against their names, using either the `name` attribute in the Artemis configuration or the names provided through configuration. + + + [[messaging.jms.jndi]] === Using a JNDI ConnectionFactory If you are running your application in an application server, Spring Boot tries to locate a JMS `ConnectionFactory` by using JNDI. diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-artemis/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-artemis/build.gradle new file mode 100644 index 0000000000..ae02168509 --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-artemis/build.gradle @@ -0,0 +1,13 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for JMS messaging using Apache Artemis" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter")) + api("org.springframework:spring-jms") + api("org.apache.activemq:artemis-jms-client") { + exclude group: "commons-logging", module: "commons-logging" + } +}