From 1ee079c26512238b73fa3ebc4c4ae5d02f2df9d0 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 4 May 2022 17:31:14 +0100 Subject: [PATCH] Reinstate support for pooled-jms as it is now JMS 3.0 compatible Closes gh-30865 --- .../spring-boot-autoconfigure/build.gradle | 3 + .../jms/JmsPoolConnectionFactoryFactory.java | 65 +++++++++ .../JmsPoolConnectionFactoryProperties.java | 137 ++++++++++++++++++ ...ArtemisConnectionFactoryConfiguration.java | 18 +++ .../jms/artemis/ArtemisProperties.java | 9 ++ .../ArtemisAutoConfigurationTests.java | 53 +++++++ .../spring-boot-dependencies/build.gradle | 7 + .../src/docs/asciidoc/messaging/jms.adoc | 13 ++ 8 files changed, 305 insertions(+) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsPoolConnectionFactoryFactory.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsPoolConnectionFactoryProperties.java diff --git a/spring-boot-project/spring-boot-autoconfigure/build.gradle b/spring-boot-project/spring-boot-autoconfigure/build.gradle index 2cc10c591b..11ee26afb8 100644 --- a/spring-boot-project/spring-boot-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-autoconfigure/build.gradle @@ -118,6 +118,9 @@ dependencies { optional("org.liquibase:liquibase-core") { exclude group: "javax.xml.bind", module: "jaxb-api" } + optional("org.messaginghub:pooled-jms") { + exclude group: "org.apache.geronimo.specs", module: "geronimo-jms_2.0_spec" + } optional("org.mongodb:mongodb-driver-reactivestreams") optional("org.mongodb:mongodb-driver-sync") optional("org.quartz-scheduler:quartz") diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsPoolConnectionFactoryFactory.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsPoolConnectionFactoryFactory.java new file mode 100644 index 0000000000..2939a4ebe1 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsPoolConnectionFactoryFactory.java @@ -0,0 +1,65 @@ +/* + * Copyright 2012-2022 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; + +import jakarta.jms.ConnectionFactory; +import org.messaginghub.pooled.jms.JmsPoolConnectionFactory; + +/** + * Factory to create a {@link JmsPoolConnectionFactory} from properties defined in + * {@link JmsPoolConnectionFactoryProperties}. + * + * @author Stephane Nicoll + * @since 2.1.0 + */ +public class JmsPoolConnectionFactoryFactory { + + private final JmsPoolConnectionFactoryProperties properties; + + public JmsPoolConnectionFactoryFactory(JmsPoolConnectionFactoryProperties properties) { + this.properties = properties; + } + + /** + * Create a {@link JmsPoolConnectionFactory} based on the specified + * {@link ConnectionFactory}. + * @param connectionFactory the connection factory to wrap + * @return a pooled connection factory + */ + public JmsPoolConnectionFactory createPooledConnectionFactory(ConnectionFactory connectionFactory) { + JmsPoolConnectionFactory pooledConnectionFactory = new JmsPoolConnectionFactory(); + pooledConnectionFactory.setConnectionFactory(connectionFactory); + + pooledConnectionFactory.setBlockIfSessionPoolIsFull(this.properties.isBlockIfFull()); + if (this.properties.getBlockIfFullTimeout() != null) { + pooledConnectionFactory + .setBlockIfSessionPoolIsFullTimeout(this.properties.getBlockIfFullTimeout().toMillis()); + } + if (this.properties.getIdleTimeout() != null) { + pooledConnectionFactory.setConnectionIdleTimeout((int) this.properties.getIdleTimeout().toMillis()); + } + pooledConnectionFactory.setMaxConnections(this.properties.getMaxConnections()); + pooledConnectionFactory.setMaxSessionsPerConnection(this.properties.getMaxSessionsPerConnection()); + if (this.properties.getTimeBetweenExpirationCheck() != null) { + pooledConnectionFactory + .setConnectionCheckInterval(this.properties.getTimeBetweenExpirationCheck().toMillis()); + } + pooledConnectionFactory.setUseAnonymousProducers(this.properties.isUseAnonymousProducers()); + return pooledConnectionFactory; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsPoolConnectionFactoryProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsPoolConnectionFactoryProperties.java new file mode 100644 index 0000000000..6eed0f5268 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsPoolConnectionFactoryProperties.java @@ -0,0 +1,137 @@ +/* + * Copyright 2012-2022 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; + +import java.time.Duration; + +/** + * Configuration properties for connection factory pooling. + * + * @author Stephane Nicoll + * @since 2.1.0 + */ +public class JmsPoolConnectionFactoryProperties { + + /** + * Whether a JmsPoolConnectionFactory should be created, instead of a regular + * ConnectionFactory. + */ + private boolean enabled; + + /** + * Whether to block when a connection is requested and the pool is full. Set it to + * false to throw a "JMSException" instead. + */ + private boolean blockIfFull = true; + + /** + * Blocking period before throwing an exception if the pool is still full. + */ + private Duration blockIfFullTimeout = Duration.ofMillis(-1); + + /** + * Connection idle timeout. + */ + private Duration idleTimeout = Duration.ofSeconds(30); + + /** + * Maximum number of pooled connections. + */ + private int maxConnections = 1; + + /** + * Maximum number of pooled sessions per connection in the pool. + */ + private int maxSessionsPerConnection = 500; + + /** + * Time to sleep between runs of the idle connection eviction thread. When negative, + * no idle connection eviction thread runs. + */ + private Duration timeBetweenExpirationCheck = Duration.ofMillis(-1); + + /** + * Whether to use only one anonymous "MessageProducer" instance. Set it to false to + * create one "MessageProducer" every time one is required. + */ + private boolean useAnonymousProducers = true; + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public boolean isBlockIfFull() { + return this.blockIfFull; + } + + public void setBlockIfFull(boolean blockIfFull) { + this.blockIfFull = blockIfFull; + } + + public Duration getBlockIfFullTimeout() { + return this.blockIfFullTimeout; + } + + public void setBlockIfFullTimeout(Duration blockIfFullTimeout) { + this.blockIfFullTimeout = blockIfFullTimeout; + } + + public Duration getIdleTimeout() { + return this.idleTimeout; + } + + public void setIdleTimeout(Duration idleTimeout) { + this.idleTimeout = idleTimeout; + } + + public int getMaxConnections() { + return this.maxConnections; + } + + public void setMaxConnections(int maxConnections) { + this.maxConnections = maxConnections; + } + + public int getMaxSessionsPerConnection() { + return this.maxSessionsPerConnection; + } + + public void setMaxSessionsPerConnection(int maxSessionsPerConnection) { + this.maxSessionsPerConnection = maxSessionsPerConnection; + } + + public Duration getTimeBetweenExpirationCheck() { + return this.timeBetweenExpirationCheck; + } + + public void setTimeBetweenExpirationCheck(Duration timeBetweenExpirationCheck) { + this.timeBetweenExpirationCheck = timeBetweenExpirationCheck; + } + + public boolean isUseAnonymousProducers() { + return this.useAnonymousProducers; + } + + public void setUseAnonymousProducers(boolean useAnonymousProducers) { + this.useAnonymousProducers = useAnonymousProducers; + } + +} 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 index 1ae8db52ea..7215984843 100644 --- 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 @@ -18,11 +18,14 @@ package org.springframework.boot.autoconfigure.jms.artemis; import jakarta.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; @@ -79,4 +82,19 @@ class ArtemisConnectionFactoryConfiguration { } + @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/ArtemisProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisProperties.java index cd7edf8ecd..fd2aa33211 100644 --- 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 @@ -23,7 +23,9 @@ 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.NestedConfigurationProperty; /** * Configuration properties for Artemis. @@ -58,6 +60,9 @@ public class ArtemisProperties { private final Embedded embedded = new Embedded(); + @NestedConfigurationProperty + private final JmsPoolConnectionFactoryProperties pool = new JmsPoolConnectionFactoryProperties(); + public ArtemisMode getMode() { return this.mode; } @@ -94,6 +99,10 @@ public class ArtemisProperties { return this.embedded; } + public JmsPoolConnectionFactoryProperties getPool() { + return this.pool; + } + /** * Configuration for an embedded Artemis server. */ 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 index 1df614aba0..87b0641cdf 100644 --- 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 @@ -42,6 +42,7 @@ import org.apache.activemq.artemis.jms.server.config.impl.JMSQueueConfigurationI 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; @@ -299,6 +300,58 @@ class ArtemisAutoConfigurationTests { }); } + @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); diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 23588e02fc..10ac23a47d 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1093,6 +1093,13 @@ bom { ] } } + library("Pooled JMS", "3.0.0") { + group("org.messaginghub") { + modules = [ + "pooled-jms" + ] + } + } library("Postgresql", "42.3.3") { group("org.postgresql") { modules = [ 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 afb9690f2a..9ab3c7af01 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 @@ -43,6 +43,19 @@ By default, a `CachingConnectionFactory` wraps the native `ConnectionFactory` wi session-cache-size: 5 ---- +If you'd rather use native pooling, you can do so by adding a dependency on `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.