Create spring-boot-jms module

This commit is contained in:
Stéphane Nicoll
2025-03-18 11:37:00 +01:00
committed by Phillip Webb
parent b0f5788d9a
commit f78a6fa37a
49 changed files with 252 additions and 105 deletions

View File

@@ -0,0 +1,80 @@
/*
* Copyright 2012-2025 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.jms;
import jakarta.jms.ConnectionFactory;
import org.messaginghub.pooled.jms.JmsPoolConnectionFactory;
import org.springframework.jms.connection.CachingConnectionFactory;
/**
* Unwrap a {@link ConnectionFactory} that may have been wrapped to perform caching or
* pooling.
*
* @author Stephane Nicoll
* @since 3.4.0
*/
public final class ConnectionFactoryUnwrapper {
private ConnectionFactoryUnwrapper() {
}
/**
* Return the native {@link ConnectionFactory} by unwrapping from a
* {@link CachingConnectionFactory}. Return the given {@link ConnectionFactory} if no
* {@link CachingConnectionFactory} wrapper has been detected.
* @param connectionFactory a connection factory
* @return the native connection factory that a {@link CachingConnectionFactory}
* wraps, if any
* @since 3.4.1
*/
public static ConnectionFactory unwrapCaching(ConnectionFactory connectionFactory) {
if (connectionFactory instanceof CachingConnectionFactory cachingConnectionFactory) {
ConnectionFactory unwrapedConnectionFactory = cachingConnectionFactory.getTargetConnectionFactory();
return (unwrapedConnectionFactory != null) ? unwrapCaching(unwrapedConnectionFactory) : connectionFactory;
}
return connectionFactory;
}
/**
* Return the native {@link ConnectionFactory} by unwrapping it from a cache or pool
* connection factory. Return the given {@link ConnectionFactory} if no caching
* wrapper has been detected.
* @param connectionFactory a connection factory
* @return the native connection factory that it wraps, if any
*/
public static ConnectionFactory unwrap(ConnectionFactory connectionFactory) {
if (connectionFactory instanceof CachingConnectionFactory cachingConnectionFactory) {
return unwrap(cachingConnectionFactory.getTargetConnectionFactory());
}
ConnectionFactory unwrapedConnectionFactory = unwrapFromJmsPoolConnectionFactory(connectionFactory);
return (unwrapedConnectionFactory != null) ? unwrap(unwrapedConnectionFactory) : connectionFactory;
}
private static ConnectionFactory unwrapFromJmsPoolConnectionFactory(ConnectionFactory connectionFactory) {
try {
if (connectionFactory instanceof JmsPoolConnectionFactory jmsPoolConnectionFactory) {
return (ConnectionFactory) jmsPoolConnectionFactory.getConnectionFactory();
}
}
catch (Throwable ex) {
// ignore
}
return null;
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2012-2025 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.jms;
import jakarta.jms.ConnectionFactory;
import jakarta.jms.XAConnectionFactory;
import jakarta.transaction.TransactionManager;
/**
* Strategy interface used to wrap a JMS {@link XAConnectionFactory} enrolling it with a
* JTA {@link TransactionManager}.
*
* @author Phillip Webb
* @since 2.0.0
*/
@FunctionalInterface
public interface XAConnectionFactoryWrapper {
/**
* Wrap the specific {@link XAConnectionFactory} and enroll it with a JTA
* {@link TransactionManager}.
* @param connectionFactory the connection factory to wrap
* @return the wrapped connection factory
* @throws Exception if the connection factory cannot be wrapped
*/
ConnectionFactory wrapConnectionFactory(XAConnectionFactory connectionFactory) throws Exception;
}

View File

@@ -0,0 +1,105 @@
/*
* Copyright 2012-2025 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.jms.autoconfigure;
import java.util.HashMap;
import java.util.Map;
import jakarta.jms.Session;
import org.springframework.jms.support.JmsAccessor;
/**
* Acknowledge modes for a JMS Session. Supports the acknowledge modes defined by
* {@link jakarta.jms.Session} as well as other, non-standard modes.
*
* <p>
* Note that {@link jakarta.jms.Session#SESSION_TRANSACTED} is not defined. It should be
* handled through a call to {@link JmsAccessor#setSessionTransacted(boolean)}.
*
* @author Andy Wilkinson
* @since 3.2.0
*/
public final class AcknowledgeMode {
private static final Map<String, AcknowledgeMode> knownModes = new HashMap<>(3);
/**
* Messages sent or received from the session are automatically acknowledged. This is
* the simplest mode and enables once-only message delivery guarantee.
*/
public static final AcknowledgeMode AUTO = new AcknowledgeMode(Session.AUTO_ACKNOWLEDGE);
/**
* Messages are acknowledged once the message listener implementation has called
* {@link jakarta.jms.Message#acknowledge()}. This mode gives the application (rather
* than the JMS provider) complete control over message acknowledgement.
*/
public static final AcknowledgeMode CLIENT = new AcknowledgeMode(Session.CLIENT_ACKNOWLEDGE);
/**
* Similar to auto acknowledgment except that said acknowledgment is lazy. As a
* consequence, the messages might be delivered more than once. This mode enables
* at-least-once message delivery guarantee.
*/
public static final AcknowledgeMode DUPS_OK = new AcknowledgeMode(Session.DUPS_OK_ACKNOWLEDGE);
static {
knownModes.put("auto", AUTO);
knownModes.put("client", CLIENT);
knownModes.put("dupsok", DUPS_OK);
}
private final int mode;
private AcknowledgeMode(int mode) {
this.mode = mode;
}
public int getMode() {
return this.mode;
}
/**
* Creates an {@code AcknowledgeMode} of the given {@code mode}. The mode may be
* {@code auto}, {@code client}, {@code dupsok} or a non-standard acknowledge mode
* that can be {@link Integer#parseInt parsed as an integer}.
* @param mode the mode
* @return the acknowledge mode
*/
public static AcknowledgeMode of(String mode) {
String canonicalMode = canonicalize(mode);
AcknowledgeMode knownMode = knownModes.get(canonicalMode);
try {
return (knownMode != null) ? knownMode : new AcknowledgeMode(Integer.parseInt(canonicalMode));
}
catch (NumberFormatException ex) {
throw new IllegalArgumentException("'" + mode
+ "' is neither a known acknowledge mode (auto, client, or dups_ok) nor an integer value");
}
}
private static String canonicalize(String input) {
StringBuilder canonicalName = new StringBuilder(input.length());
input.chars()
.filter(Character::isLetterOrDigit)
.map(Character::toLowerCase)
.forEach((c) -> canonicalName.append((char) c));
return canonicalName.toString();
}
}

View File

@@ -0,0 +1,149 @@
/*
* Copyright 2012-2025 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.jms.autoconfigure;
import java.time.Duration;
import io.micrometer.observation.ObservationRegistry;
import jakarta.jms.ConnectionFactory;
import jakarta.jms.ExceptionListener;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.jms.autoconfigure.JmsProperties.Listener.Session;
import org.springframework.jms.config.DefaultJmsListenerContainerFactory;
import org.springframework.jms.support.converter.MessageConverter;
import org.springframework.jms.support.destination.DestinationResolver;
import org.springframework.transaction.jta.JtaTransactionManager;
import org.springframework.util.Assert;
/**
* Configure {@link DefaultJmsListenerContainerFactory} with sensible defaults tuned using
* configuration properties.
* <p>
* Can be injected into application code and used to define a custom
* {@code DefaultJmsListenerContainerFactory} whose configuration is based upon that
* produced by auto-configuration.
*
* @author Stephane Nicoll
* @author Eddú Meléndez
* @author Vedran Pavic
* @author Lasse Wulff
* @since 1.3.3
*/
public final class DefaultJmsListenerContainerFactoryConfigurer {
private DestinationResolver destinationResolver;
private MessageConverter messageConverter;
private ExceptionListener exceptionListener;
private JtaTransactionManager transactionManager;
private JmsProperties jmsProperties;
private ObservationRegistry observationRegistry;
/**
* Set the {@link DestinationResolver} to use or {@code null} if no destination
* resolver should be associated with the factory by default.
* @param destinationResolver the {@link DestinationResolver}
*/
void setDestinationResolver(DestinationResolver destinationResolver) {
this.destinationResolver = destinationResolver;
}
/**
* Set the {@link MessageConverter} to use or {@code null} if the out-of-the-box
* converter should be used.
* @param messageConverter the {@link MessageConverter}
*/
void setMessageConverter(MessageConverter messageConverter) {
this.messageConverter = messageConverter;
}
/**
* Set the {@link ExceptionListener} to use or {@code null} if no exception listener
* should be associated by default.
* @param exceptionListener the {@link ExceptionListener}
*/
void setExceptionListener(ExceptionListener exceptionListener) {
this.exceptionListener = exceptionListener;
}
/**
* Set the {@link JtaTransactionManager} to use or {@code null} if the JTA support
* should not be used.
* @param transactionManager the {@link JtaTransactionManager}
*/
void setTransactionManager(JtaTransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
/**
* Set the {@link JmsProperties} to use.
* @param jmsProperties the {@link JmsProperties}
*/
void setJmsProperties(JmsProperties jmsProperties) {
this.jmsProperties = jmsProperties;
}
/**
* Set the {@link ObservationRegistry} to use.
* @param observationRegistry the {@link ObservationRegistry}
* @since 3.2.1
* @deprecated since 3.3.10 for removal in 4.0.0 as this should have been package
* private
*/
@Deprecated(since = "3.3.10", forRemoval = true)
public void setObservationRegistry(ObservationRegistry observationRegistry) {
this.observationRegistry = observationRegistry;
}
/**
* Configure the specified jms listener container factory. The factory can be further
* tuned and default settings can be overridden.
* @param factory the {@link DefaultJmsListenerContainerFactory} instance to configure
* @param connectionFactory the {@link ConnectionFactory} to use
*/
public void configure(DefaultJmsListenerContainerFactory factory, ConnectionFactory connectionFactory) {
Assert.notNull(factory, "'factory' must not be null");
Assert.notNull(connectionFactory, "'connectionFactory' must not be null");
JmsProperties.Listener listenerProperties = this.jmsProperties.getListener();
Session sessionProperties = listenerProperties.getSession();
factory.setConnectionFactory(connectionFactory);
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(this.jmsProperties::isPubSubDomain).to(factory::setPubSubDomain);
map.from(this.jmsProperties::isSubscriptionDurable).to(factory::setSubscriptionDurable);
map.from(this.jmsProperties::getClientId).to(factory::setClientId);
map.from(this.transactionManager).to(factory::setTransactionManager);
map.from(this.destinationResolver).to(factory::setDestinationResolver);
map.from(this.messageConverter).to(factory::setMessageConverter);
map.from(this.exceptionListener).to(factory::setExceptionListener);
map.from(sessionProperties.getAcknowledgeMode()::getMode).to(factory::setSessionAcknowledgeMode);
if (this.transactionManager == null && sessionProperties.getTransacted() == null) {
factory.setSessionTransacted(true);
}
map.from(this.observationRegistry).to(factory::setObservationRegistry);
map.from(sessionProperties::getTransacted).to(factory::setSessionTransacted);
map.from(listenerProperties::isAutoStartup).to(factory::setAutoStartup);
map.from(listenerProperties::formatConcurrency).to(factory::setConcurrency);
map.from(listenerProperties::getReceiveTimeout).as(Duration::toMillis).to(factory::setReceiveTimeout);
map.from(listenerProperties::getMaxMessagesPerTask).to(factory::setMaxMessagesPerTask);
}
}

View File

@@ -0,0 +1,119 @@
/*
* Copyright 2012-2025 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.jms.autoconfigure;
import io.micrometer.observation.ObservationRegistry;
import jakarta.jms.ConnectionFactory;
import jakarta.jms.ExceptionListener;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnJndi;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
import org.springframework.boot.jms.ConnectionFactoryUnwrapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jms.annotation.EnableJms;
import org.springframework.jms.config.DefaultJmsListenerContainerFactory;
import org.springframework.jms.config.JmsListenerConfigUtils;
import org.springframework.jms.support.converter.MessageConverter;
import org.springframework.jms.support.destination.DestinationResolver;
import org.springframework.jms.support.destination.JndiDestinationResolver;
import org.springframework.transaction.jta.JtaTransactionManager;
/**
* Configuration for Spring 4.1 annotation driven JMS.
*
* @author Phillip Webb
* @author Stephane Nicoll
* @author Eddú Meléndez
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(EnableJms.class)
class JmsAnnotationDrivenConfiguration {
private final ObjectProvider<DestinationResolver> destinationResolver;
private final ObjectProvider<JtaTransactionManager> transactionManager;
private final ObjectProvider<MessageConverter> messageConverter;
private final ObjectProvider<ExceptionListener> exceptionListener;
private final ObjectProvider<ObservationRegistry> observationRegistry;
private final JmsProperties properties;
JmsAnnotationDrivenConfiguration(ObjectProvider<DestinationResolver> destinationResolver,
ObjectProvider<JtaTransactionManager> transactionManager, ObjectProvider<MessageConverter> messageConverter,
ObjectProvider<ExceptionListener> exceptionListener,
ObjectProvider<ObservationRegistry> observationRegistry, JmsProperties properties) {
this.destinationResolver = destinationResolver;
this.transactionManager = transactionManager;
this.messageConverter = messageConverter;
this.exceptionListener = exceptionListener;
this.observationRegistry = observationRegistry;
this.properties = properties;
}
@Bean
@ConditionalOnMissingBean
@SuppressWarnings("removal")
DefaultJmsListenerContainerFactoryConfigurer jmsListenerContainerFactoryConfigurer() {
DefaultJmsListenerContainerFactoryConfigurer configurer = new DefaultJmsListenerContainerFactoryConfigurer();
configurer.setDestinationResolver(this.destinationResolver.getIfUnique());
configurer.setTransactionManager(this.transactionManager.getIfUnique());
configurer.setMessageConverter(this.messageConverter.getIfUnique());
configurer.setExceptionListener(this.exceptionListener.getIfUnique());
configurer.setObservationRegistry(this.observationRegistry.getIfUnique());
configurer.setJmsProperties(this.properties);
return configurer;
}
@Bean
@ConditionalOnSingleCandidate(ConnectionFactory.class)
@ConditionalOnMissingBean(name = "jmsListenerContainerFactory")
DefaultJmsListenerContainerFactory jmsListenerContainerFactory(
DefaultJmsListenerContainerFactoryConfigurer configurer, ConnectionFactory connectionFactory) {
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
configurer.configure(factory, ConnectionFactoryUnwrapper.unwrapCaching(connectionFactory));
return factory;
}
@Configuration(proxyBeanMethods = false)
@EnableJms
@ConditionalOnMissingBean(name = JmsListenerConfigUtils.JMS_LISTENER_ANNOTATION_PROCESSOR_BEAN_NAME)
static class EnableJmsConfiguration {
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnJndi
static class JndiConfiguration {
@Bean
@ConditionalOnMissingBean(DestinationResolver.class)
JndiDestinationResolver destinationResolver() {
JndiDestinationResolver resolver = new JndiDestinationResolver();
resolver.setFallbackToDynamicDestination(true);
return resolver;
}
}
}

View File

@@ -0,0 +1,151 @@
/*
* Copyright 2012-2025 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.jms.autoconfigure;
import java.time.Duration;
import java.util.List;
import io.micrometer.observation.ObservationRegistry;
import jakarta.jms.ConnectionFactory;
import jakarta.jms.Message;
import org.springframework.aot.hint.ExecutableMode;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.aot.hint.TypeReference;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
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.condition.ConditionalOnSingleCandidate;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.jms.autoconfigure.JmsAutoConfiguration.JmsRuntimeHints;
import org.springframework.boot.jms.autoconfigure.JmsProperties.DeliveryMode;
import org.springframework.boot.jms.autoconfigure.JmsProperties.Template;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportRuntimeHints;
import org.springframework.jms.core.JmsMessageOperations;
import org.springframework.jms.core.JmsMessagingTemplate;
import org.springframework.jms.core.JmsOperations;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.support.converter.MessageConverter;
import org.springframework.jms.support.destination.DestinationResolver;
/**
* {@link EnableAutoConfiguration Auto-configuration} for Spring JMS.
*
* @author Greg Turnquist
* @author Stephane Nicoll
* @author Vedran Pavic
* @since 1.0.0
*/
@AutoConfiguration
@ConditionalOnClass({ Message.class, JmsTemplate.class })
@ConditionalOnBean(ConnectionFactory.class)
@EnableConfigurationProperties(JmsProperties.class)
@Import(JmsAnnotationDrivenConfiguration.class)
@ImportRuntimeHints(JmsRuntimeHints.class)
public class JmsAutoConfiguration {
@Configuration(proxyBeanMethods = false)
protected static class JmsTemplateConfiguration {
private final JmsProperties properties;
private final ObjectProvider<DestinationResolver> destinationResolver;
private final ObjectProvider<MessageConverter> messageConverter;
private final ObjectProvider<ObservationRegistry> observationRegistry;
public JmsTemplateConfiguration(JmsProperties properties,
ObjectProvider<DestinationResolver> destinationResolver,
ObjectProvider<MessageConverter> messageConverter,
ObjectProvider<ObservationRegistry> observationRegistry) {
this.properties = properties;
this.destinationResolver = destinationResolver;
this.messageConverter = messageConverter;
this.observationRegistry = observationRegistry;
}
@Bean
@ConditionalOnMissingBean(JmsOperations.class)
@ConditionalOnSingleCandidate(ConnectionFactory.class)
public JmsTemplate jmsTemplate(ConnectionFactory connectionFactory) {
PropertyMapper map = PropertyMapper.get();
JmsTemplate template = new JmsTemplate(connectionFactory);
template.setPubSubDomain(this.properties.isPubSubDomain());
map.from(this.destinationResolver::getIfUnique).whenNonNull().to(template::setDestinationResolver);
map.from(this.messageConverter::getIfUnique).whenNonNull().to(template::setMessageConverter);
map.from(this.observationRegistry::getIfUnique).whenNonNull().to(template::setObservationRegistry);
mapTemplateProperties(this.properties.getTemplate(), template);
return template;
}
private void mapTemplateProperties(Template properties, JmsTemplate template) {
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(properties.getSession().getAcknowledgeMode()::getMode).to(template::setSessionAcknowledgeMode);
map.from(properties.getSession()::isTransacted).to(template::setSessionTransacted);
map.from(properties::getDefaultDestination).whenNonNull().to(template::setDefaultDestinationName);
map.from(properties::getDeliveryDelay).whenNonNull().as(Duration::toMillis).to(template::setDeliveryDelay);
map.from(properties::determineQosEnabled).to(template::setExplicitQosEnabled);
map.from(properties::getDeliveryMode).as(DeliveryMode::getValue).to(template::setDeliveryMode);
map.from(properties::getPriority).whenNonNull().to(template::setPriority);
map.from(properties::getTimeToLive).whenNonNull().as(Duration::toMillis).to(template::setTimeToLive);
map.from(properties::getReceiveTimeout).as(Duration::toMillis).to(template::setReceiveTimeout);
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(JmsMessagingTemplate.class)
@Import(JmsTemplateConfiguration.class)
protected static class MessagingTemplateConfiguration {
@Bean
@ConditionalOnMissingBean(JmsMessageOperations.class)
@ConditionalOnSingleCandidate(JmsTemplate.class)
public JmsMessagingTemplate jmsMessagingTemplate(JmsProperties properties, JmsTemplate jmsTemplate) {
JmsMessagingTemplate messagingTemplate = new JmsMessagingTemplate(jmsTemplate);
mapTemplateProperties(properties.getTemplate(), messagingTemplate);
return messagingTemplate;
}
private void mapTemplateProperties(Template properties, JmsMessagingTemplate messagingTemplate) {
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(properties::getDefaultDestination).to(messagingTemplate::setDefaultDestinationName);
}
}
static class JmsRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
hints.reflection()
.registerType(TypeReference.of(AcknowledgeMode.class), (type) -> type.withMethod("of",
List.of(TypeReference.of(String.class)), ExecutableMode.INVOKE));
}
}
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright 2012-2025 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.jms.autoconfigure;
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;
}
}

View File

@@ -0,0 +1,137 @@
/*
* Copyright 2012-2025 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.jms.autoconfigure;
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;
}
}

View File

@@ -0,0 +1,475 @@
/*
* Copyright 2012-2025 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.jms.autoconfigure;
import java.time.Duration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.DeprecatedConfigurationProperty;
/**
* Configuration properties for JMS.
*
* @author Greg Turnquist
* @author Phillip Webb
* @author Stephane Nicoll
* @author Lasse Wulff
* @author Vedran Pavic
* @since 1.0.0
*/
@ConfigurationProperties("spring.jms")
public class JmsProperties {
/**
* Whether the default destination type is topic.
*/
private boolean pubSubDomain = false;
/**
* Connection factory JNDI name. When set, takes precedence to others connection
* factory auto-configurations.
*/
private String jndiName;
/**
* Whether the subscription is durable.
*/
private boolean subscriptionDurable = false;
/**
* Client id of the connection.
*/
private String clientId;
private final Cache cache = new Cache();
private final Listener listener = new Listener();
private final Template template = new Template();
public boolean isPubSubDomain() {
return this.pubSubDomain;
}
public void setPubSubDomain(boolean pubSubDomain) {
this.pubSubDomain = pubSubDomain;
}
public boolean isSubscriptionDurable() {
return this.subscriptionDurable;
}
public void setSubscriptionDurable(boolean subscriptionDurable) {
this.subscriptionDurable = subscriptionDurable;
}
public String getClientId() {
return this.clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public String getJndiName() {
return this.jndiName;
}
public void setJndiName(String jndiName) {
this.jndiName = jndiName;
}
public Cache getCache() {
return this.cache;
}
public Listener getListener() {
return this.listener;
}
public Template getTemplate() {
return this.template;
}
public static class Cache {
/**
* Whether to cache sessions.
*/
private boolean enabled = true;
/**
* Whether to cache message consumers.
*/
private boolean consumers = false;
/**
* Whether to cache message producers.
*/
private boolean producers = true;
/**
* Size of the session cache (per JMS Session type).
*/
private int sessionCacheSize = 1;
public boolean isEnabled() {
return this.enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public boolean isConsumers() {
return this.consumers;
}
public void setConsumers(boolean consumers) {
this.consumers = consumers;
}
public boolean isProducers() {
return this.producers;
}
public void setProducers(boolean producers) {
this.producers = producers;
}
public int getSessionCacheSize() {
return this.sessionCacheSize;
}
public void setSessionCacheSize(int sessionCacheSize) {
this.sessionCacheSize = sessionCacheSize;
}
}
public static class Listener {
/**
* Start the container automatically on startup.
*/
private boolean autoStartup = true;
/**
* Minimum number of concurrent consumers. When max-concurrency is not specified
* the minimum will also be used as the maximum.
*/
private Integer minConcurrency;
/**
* Maximum number of concurrent consumers.
*/
private Integer maxConcurrency;
/**
* Timeout to use for receive calls. Use -1 for a no-wait receive or 0 for no
* timeout at all. The latter is only feasible if not running within a transaction
* manager and is generally discouraged since it prevents clean shutdown.
*/
private Duration receiveTimeout = Duration.ofSeconds(1);
/**
* Maximum number of messages to process in one task. By default, unlimited unless
* a SchedulingTaskExecutor is configured on the listener (10 messages), as it
* indicates a preference for short-lived tasks.
*/
private Integer maxMessagesPerTask;
private final Session session = new Session();
public boolean isAutoStartup() {
return this.autoStartup;
}
public void setAutoStartup(boolean autoStartup) {
this.autoStartup = autoStartup;
}
@Deprecated(since = "3.2.0", forRemoval = true)
@DeprecatedConfigurationProperty(replacement = "spring.jms.listener.session.acknowledge-mode", since = "3.2.0")
public AcknowledgeMode getAcknowledgeMode() {
return this.session.getAcknowledgeMode();
}
@Deprecated(since = "3.2.0", forRemoval = true)
public void setAcknowledgeMode(AcknowledgeMode acknowledgeMode) {
this.session.setAcknowledgeMode(acknowledgeMode);
}
@DeprecatedConfigurationProperty(replacement = "spring.jms.listener.min-concurrency", since = "3.2.0")
@Deprecated(since = "3.2.0", forRemoval = true)
public Integer getConcurrency() {
return this.minConcurrency;
}
@Deprecated(since = "3.2.0", forRemoval = true)
public void setConcurrency(Integer concurrency) {
this.minConcurrency = concurrency;
}
public Integer getMinConcurrency() {
return this.minConcurrency;
}
public void setMinConcurrency(Integer minConcurrency) {
this.minConcurrency = minConcurrency;
}
public Integer getMaxConcurrency() {
return this.maxConcurrency;
}
public void setMaxConcurrency(Integer maxConcurrency) {
this.maxConcurrency = maxConcurrency;
}
public String formatConcurrency() {
if (this.minConcurrency == null) {
return (this.maxConcurrency != null) ? "1-" + this.maxConcurrency : null;
}
return this.minConcurrency + "-"
+ ((this.maxConcurrency != null) ? this.maxConcurrency : this.minConcurrency);
}
public Duration getReceiveTimeout() {
return this.receiveTimeout;
}
public void setReceiveTimeout(Duration receiveTimeout) {
this.receiveTimeout = receiveTimeout;
}
public Integer getMaxMessagesPerTask() {
return this.maxMessagesPerTask;
}
public void setMaxMessagesPerTask(Integer maxMessagesPerTask) {
this.maxMessagesPerTask = maxMessagesPerTask;
}
public Session getSession() {
return this.session;
}
public static class Session {
/**
* Acknowledge mode of the listener container.
*/
private AcknowledgeMode acknowledgeMode = AcknowledgeMode.AUTO;
/**
* Whether the listener container should use transacted JMS sessions. Defaults
* to false in the presence of a JtaTransactionManager and true otherwise.
*/
private Boolean transacted;
public AcknowledgeMode getAcknowledgeMode() {
return this.acknowledgeMode;
}
public void setAcknowledgeMode(AcknowledgeMode acknowledgeMode) {
this.acknowledgeMode = acknowledgeMode;
}
public Boolean getTransacted() {
return this.transacted;
}
public void setTransacted(Boolean transacted) {
this.transacted = transacted;
}
}
}
public static class Template {
/**
* Default destination to use on send and receive operations that do not have a
* destination parameter.
*/
private String defaultDestination;
/**
* Delivery delay to use for send calls.
*/
private Duration deliveryDelay;
/**
* Delivery mode. Enables QoS (Quality of Service) when set.
*/
private DeliveryMode deliveryMode;
/**
* Priority of a message when sending. Enables QoS (Quality of Service) when set.
*/
private Integer priority;
/**
* Time-to-live of a message when sending. Enables QoS (Quality of Service) when
* set.
*/
private Duration timeToLive;
/**
* Whether to enable explicit QoS (Quality of Service) when sending a message.
* When enabled, the delivery mode, priority and time-to-live properties will be
* used when sending a message. QoS is automatically enabled when at least one of
* those settings is customized.
*/
private Boolean qosEnabled;
/**
* Timeout to use for receive calls.
*/
private Duration receiveTimeout;
private final Session session = new Session();
public String getDefaultDestination() {
return this.defaultDestination;
}
public void setDefaultDestination(String defaultDestination) {
this.defaultDestination = defaultDestination;
}
public Duration getDeliveryDelay() {
return this.deliveryDelay;
}
public void setDeliveryDelay(Duration deliveryDelay) {
this.deliveryDelay = deliveryDelay;
}
public DeliveryMode getDeliveryMode() {
return this.deliveryMode;
}
public void setDeliveryMode(DeliveryMode deliveryMode) {
this.deliveryMode = deliveryMode;
}
public Integer getPriority() {
return this.priority;
}
public void setPriority(Integer priority) {
this.priority = priority;
}
public Duration getTimeToLive() {
return this.timeToLive;
}
public void setTimeToLive(Duration timeToLive) {
this.timeToLive = timeToLive;
}
public boolean determineQosEnabled() {
if (this.qosEnabled != null) {
return this.qosEnabled;
}
return (getDeliveryMode() != null || getPriority() != null || getTimeToLive() != null);
}
public Boolean getQosEnabled() {
return this.qosEnabled;
}
public void setQosEnabled(Boolean qosEnabled) {
this.qosEnabled = qosEnabled;
}
public Duration getReceiveTimeout() {
return this.receiveTimeout;
}
public void setReceiveTimeout(Duration receiveTimeout) {
this.receiveTimeout = receiveTimeout;
}
public Session getSession() {
return this.session;
}
public static class Session {
/**
* Acknowledge mode used when creating sessions.
*/
private AcknowledgeMode acknowledgeMode = AcknowledgeMode.AUTO;
/**
* Whether to use transacted sessions.
*/
private boolean transacted = false;
public AcknowledgeMode getAcknowledgeMode() {
return this.acknowledgeMode;
}
public void setAcknowledgeMode(AcknowledgeMode acknowledgeMode) {
this.acknowledgeMode = acknowledgeMode;
}
public boolean isTransacted() {
return this.transacted;
}
public void setTransacted(boolean transacted) {
this.transacted = transacted;
}
}
}
public enum DeliveryMode {
/**
* Does not require that the message be logged to stable storage. This is the
* lowest-overhead delivery mode but can lead to lost of message if the broker
* goes down.
*/
NON_PERSISTENT(1),
/*
* Instructs the JMS provider to log the message to stable storage as part of the
* client's send operation.
*/
PERSISTENT(2);
private final int value;
DeliveryMode(int value) {
this.value = value;
}
public int getValue() {
return this.value;
}
}
}

View File

@@ -0,0 +1,99 @@
/*
* Copyright 2012-2025 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.jms.autoconfigure;
import java.util.Arrays;
import javax.naming.NamingException;
import jakarta.jms.ConnectionFactory;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnJndi;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.jms.autoconfigure.JndiConnectionFactoryAutoConfiguration.JndiOrPropertyCondition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jndi.JndiLocatorDelegate;
import org.springframework.util.StringUtils;
/**
* {@link EnableAutoConfiguration Auto-configuration} for JMS provided from JNDI.
*
* @author Phillip Webb
* @since 1.2.0
*/
@AutoConfiguration(before = JmsAutoConfiguration.class)
@ConditionalOnClass(JmsTemplate.class)
@ConditionalOnMissingBean(ConnectionFactory.class)
@Conditional(JndiOrPropertyCondition.class)
@EnableConfigurationProperties(JmsProperties.class)
public class JndiConnectionFactoryAutoConfiguration {
// Keep these in sync with the condition below
private static final String[] JNDI_LOCATIONS = { "java:/JmsXA", "java:/XAConnectionFactory" };
@Bean
public ConnectionFactory jmsConnectionFactory(JmsProperties properties) throws NamingException {
JndiLocatorDelegate jndiLocatorDelegate = JndiLocatorDelegate.createDefaultResourceRefLocator();
if (StringUtils.hasLength(properties.getJndiName())) {
return jndiLocatorDelegate.lookup(properties.getJndiName(), ConnectionFactory.class);
}
return findJndiConnectionFactory(jndiLocatorDelegate);
}
private ConnectionFactory findJndiConnectionFactory(JndiLocatorDelegate jndiLocatorDelegate) {
for (String name : JNDI_LOCATIONS) {
try {
return jndiLocatorDelegate.lookup(name, ConnectionFactory.class);
}
catch (NamingException ex) {
// Swallow and continue
}
}
throw new IllegalStateException(
"Unable to find ConnectionFactory in JNDI locations " + Arrays.asList(JNDI_LOCATIONS));
}
/**
* Condition for JNDI name or a specific property.
*/
static class JndiOrPropertyCondition extends AnyNestedCondition {
JndiOrPropertyCondition() {
super(ConfigurationPhase.PARSE_CONFIGURATION);
}
@ConditionalOnJndi({ "java:/JmsXA", "java:/XAConnectionFactory" })
static class Jndi {
}
@ConditionalOnProperty("spring.jms.jndi-name")
static class Property {
}
}
}

View File

@@ -0,0 +1,20 @@
/*
* 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 JMS.
*/
package org.springframework.boot.jms.autoconfigure;

View File

@@ -0,0 +1,20 @@
/*
* 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.
*/
/**
* Support for Java Message Service (JMS).
*/
package org.springframework.boot.jms;

View File

@@ -0,0 +1,40 @@
{
"groups": [],
"properties": [],
"hints": [
{
"name": "spring.jms.listener.session.acknowledge-mode",
"values": [
{
"value": "auto",
"description": "Messages sent or received from the session are automatically acknowledged. This is the simplest mode and enables once-only message delivery guarantee."
},
{
"value": "client",
"description": "Messages are acknowledged once the message listener implementation has called \"jakarta.jms.Message#acknowledge()\". This mode gives the application (rather than the JMS provider) complete control over message acknowledgement."
},
{
"value": "dups_ok",
"description": "Similar to auto acknowledgment except that said acknowledgment is lazy. As a consequence, the messages might be delivered more than once. This mode enables at-least-once message delivery guarantee."
}
]
},
{
"name": "spring.jms.template.session.acknowledge-mode",
"values": [
{
"value": "auto",
"description": "Messages sent or received from the session are automatically acknowledged. This is the simplest mode and enables once-only message delivery guarantee."
},
{
"value": "client",
"description": "Messages are acknowledged once the message listener implementation has called \"jakarta.jms.Message#acknowledge()\". This mode gives the application (rather than the JMS provider) complete control over message acknowledgement."
},
{
"value": "dups_ok",
"description": "Similar to auto acknowledgment except that said acknowledgment is lazy. As a consequence, the messages might be delivered more than once. This mode enables at-least-once message delivery guarantee."
}
]
}
]
}

View File

@@ -0,0 +1,2 @@
org.springframework.boot.jms.autoconfigure.JmsAutoConfiguration
org.springframework.boot.jms.autoconfigure.JndiConnectionFactoryAutoConfiguration

View File

@@ -0,0 +1,145 @@
/*
* Copyright 2012-2025 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.jms;
import jakarta.jms.ConnectionFactory;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.messaginghub.pooled.jms.JmsPoolConnectionFactory;
import org.springframework.boot.testsupport.classpath.ClassPathExclusions;
import org.springframework.jms.connection.CachingConnectionFactory;
import org.springframework.jms.connection.SingleConnectionFactory;
import org.springframework.util.ClassUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link ConnectionFactoryUnwrapper}.
*
* @author Stephane Nicoll
*/
class ConnectionFactoryUnwrapperTests {
@Nested
class UnwrapCaching {
@Test
void unwrapWithSingleConnectionFactory() {
ConnectionFactory connectionFactory = new SingleConnectionFactory();
assertThat(unwrapCaching(connectionFactory)).isSameAs(connectionFactory);
}
@Test
void unwrapWithConnectionFactory() {
ConnectionFactory connectionFactory = mock(ConnectionFactory.class);
assertThat(unwrapCaching(connectionFactory)).isSameAs(connectionFactory);
}
@Test
void unwrapWithCachingConnectionFactory() {
ConnectionFactory connectionFactory = mock(ConnectionFactory.class);
assertThat(unwrapCaching(new CachingConnectionFactory(connectionFactory))).isSameAs(connectionFactory);
}
@Test
void unwrapWithNestedCachingConnectionFactories() {
ConnectionFactory connectionFactory = mock(ConnectionFactory.class);
CachingConnectionFactory firstCachingConnectionFactory = new CachingConnectionFactory(connectionFactory);
CachingConnectionFactory secondCachingConnectionFactory = new CachingConnectionFactory(
firstCachingConnectionFactory);
assertThat(unwrapCaching(secondCachingConnectionFactory)).isSameAs(connectionFactory);
}
@Test
void unwrapWithJmsPoolConnectionFactory() {
ConnectionFactory connectionFactory = mock(ConnectionFactory.class);
JmsPoolConnectionFactory poolConnectionFactory = new JmsPoolConnectionFactory();
poolConnectionFactory.setConnectionFactory(connectionFactory);
assertThat(unwrapCaching(poolConnectionFactory)).isSameAs(poolConnectionFactory);
}
private ConnectionFactory unwrapCaching(ConnectionFactory connectionFactory) {
return ConnectionFactoryUnwrapper.unwrapCaching(connectionFactory);
}
}
@Nested
class Unwrap {
@Test
void unwrapWithSingleConnectionFactory() {
ConnectionFactory connectionFactory = new SingleConnectionFactory();
assertThat(unwrap(connectionFactory)).isSameAs(connectionFactory);
}
@Test
void unwrapWithConnectionFactory() {
ConnectionFactory connectionFactory = mock(ConnectionFactory.class);
assertThat(unwrap(connectionFactory)).isSameAs(connectionFactory);
}
@Test
void unwrapWithCachingConnectionFactory() {
ConnectionFactory connectionFactory = mock(ConnectionFactory.class);
assertThat(unwrap(new CachingConnectionFactory(connectionFactory))).isSameAs(connectionFactory);
}
@Test
void unwrapWithNestedCachingConnectionFactories() {
ConnectionFactory connectionFactory = mock(ConnectionFactory.class);
CachingConnectionFactory firstCachingConnectionFactory = new CachingConnectionFactory(connectionFactory);
CachingConnectionFactory secondCachingConnectionFactory = new CachingConnectionFactory(
firstCachingConnectionFactory);
assertThat(unwrap(secondCachingConnectionFactory)).isSameAs(connectionFactory);
}
@Test
void unwrapWithJmsPoolConnectionFactory() {
ConnectionFactory connectionFactory = mock(ConnectionFactory.class);
JmsPoolConnectionFactory poolConnectionFactory = new JmsPoolConnectionFactory();
poolConnectionFactory.setConnectionFactory(connectionFactory);
assertThat(unwrap(poolConnectionFactory)).isSameAs(connectionFactory);
}
@Test
void unwrapWithNestedJmsPoolConnectionFactories() {
ConnectionFactory connectionFactory = mock(ConnectionFactory.class);
JmsPoolConnectionFactory firstPooledConnectionFactory = new JmsPoolConnectionFactory();
firstPooledConnectionFactory.setConnectionFactory(connectionFactory);
JmsPoolConnectionFactory secondPooledConnectionFactory = new JmsPoolConnectionFactory();
secondPooledConnectionFactory.setConnectionFactory(firstPooledConnectionFactory);
assertThat(unwrap(secondPooledConnectionFactory)).isSameAs(connectionFactory);
}
@Test
@ClassPathExclusions("pooled-jms-*")
void unwrapWithoutJmsPoolOnClasspath() {
assertThat(ClassUtils.isPresent("org.messaginghub.pooled.jms.JmsPoolConnectionFactory", null)).isFalse();
ConnectionFactory connectionFactory = mock(ConnectionFactory.class);
assertThat(unwrap(new CachingConnectionFactory(connectionFactory))).isSameAs(connectionFactory);
}
private ConnectionFactory unwrap(ConnectionFactory connectionFactory) {
return ConnectionFactoryUnwrapper.unwrap(connectionFactory);
}
}
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright 2012-2025 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.jms.autoconfigure;
import jakarta.jms.Session;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link AcknowledgeMode}.
*
* @author Andy Wilkinson
*/
class AcknowledgeModeTests {
@ParameterizedTest
@EnumSource
void stringIsMappedToInt(Mapping mapping) {
assertThat(AcknowledgeMode.of(mapping.actual)).extracting(AcknowledgeMode::getMode).isEqualTo(mapping.expected);
}
@Test
void mapShouldThrowWhenMapIsCalledWithUnknownNonIntegerString() {
assertThatIllegalArgumentException().isThrownBy(() -> AcknowledgeMode.of("some-string"))
.withMessage(
"'some-string' is neither a known acknowledge mode (auto, client, or dups_ok) nor an integer value");
}
private enum Mapping {
AUTO_LOWER_CASE("auto", Session.AUTO_ACKNOWLEDGE),
CLIENT_LOWER_CASE("client", Session.CLIENT_ACKNOWLEDGE),
DUPS_OK_LOWER_CASE("dups_ok", Session.DUPS_OK_ACKNOWLEDGE),
AUTO_UPPER_CASE("AUTO", Session.AUTO_ACKNOWLEDGE),
CLIENT_UPPER_CASE("CLIENT", Session.CLIENT_ACKNOWLEDGE),
DUPS_OK_UPPER_CASE("DUPS_OK", Session.DUPS_OK_ACKNOWLEDGE),
AUTO_MIXED_CASE("AuTo", Session.AUTO_ACKNOWLEDGE),
CLIENT_MIXED_CASE("CliEnT", Session.CLIENT_ACKNOWLEDGE),
DUPS_OK_MIXED_CASE("dUPs_Ok", Session.DUPS_OK_ACKNOWLEDGE),
DUPS_OK_KEBAB_CASE("DUPS-OK", Session.DUPS_OK_ACKNOWLEDGE),
DUPS_OK_NO_SEPARATOR_UPPER_CASE("DUPSOK", Session.DUPS_OK_ACKNOWLEDGE),
DUPS_OK_NO_SEPARATOR_LOWER_CASE("dupsok", Session.DUPS_OK_ACKNOWLEDGE),
DUPS_OK_NO_SEPARATOR_MIXED_CASE("duPSok", Session.DUPS_OK_ACKNOWLEDGE),
INTEGER("36", 36);
private final String actual;
private final int expected;
Mapping(String actual, int expected) {
this.actual = actual;
this.expected = expected;
}
}
}

View File

@@ -0,0 +1,586 @@
/*
* Copyright 2012-2025 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.jms.autoconfigure;
import io.micrometer.observation.ObservationRegistry;
import jakarta.jms.ConnectionFactory;
import jakarta.jms.ExceptionListener;
import jakarta.jms.Session;
import org.junit.jupiter.api.Test;
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
import org.springframework.aot.test.generate.TestGenerationContext;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.aot.ApplicationContextAotGenerator;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jms.annotation.EnableJms;
import org.springframework.jms.config.DefaultJmsListenerContainerFactory;
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.config.SimpleJmsListenerEndpoint;
import org.springframework.jms.core.JmsMessagingTemplate;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.listener.DefaultMessageListenerContainer;
import org.springframework.jms.support.converter.MessageConverter;
import org.springframework.jms.support.destination.DestinationResolver;
import org.springframework.transaction.jta.JtaTransactionManager;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link JmsAutoConfiguration}.
*
* @author Greg Turnquist
* @author Stephane Nicoll
* @author Aurélien Leboulanger
* @author Eddú Meléndez
* @author Vedran Pavic
* @author Lasse Wulff
*/
class JmsAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withBean(ConnectionFactory.class, () -> mock(ConnectionFactory.class))
.withConfiguration(AutoConfigurations.of(JmsAutoConfiguration.class));
@Test
void testNoConnectionFactoryJmsConfiguration() {
new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(JmsAutoConfiguration.class))
.run((context) -> assertThat(context).doesNotHaveBean(JmsTemplate.class)
.doesNotHaveBean(JmsMessagingTemplate.class)
.doesNotHaveBean(DefaultJmsListenerContainerFactoryConfigurer.class)
.doesNotHaveBean(DefaultJmsListenerContainerFactory.class));
}
@Test
void testDefaultJmsConfiguration() {
this.contextRunner.withUserConfiguration(TestConfiguration.class).run((context) -> {
ConnectionFactory connectionFactory = context.getBean(ConnectionFactory.class);
JmsTemplate jmsTemplate = context.getBean(JmsTemplate.class);
JmsMessagingTemplate messagingTemplate = context.getBean(JmsMessagingTemplate.class);
assertThat(jmsTemplate.getConnectionFactory()).isEqualTo(connectionFactory);
assertThat(messagingTemplate.getJmsTemplate()).isEqualTo(jmsTemplate);
assertThat(context.containsBean("jmsListenerContainerFactory")).isTrue();
});
}
@Test
void testJmsTemplateBackOff() {
this.contextRunner.withUserConfiguration(TestConfiguration3.class)
.run((context) -> assertThat(context.getBean(JmsTemplate.class).getPriority()).isEqualTo(999));
}
@Test
void testJmsMessagingTemplateBackOff() {
this.contextRunner.withUserConfiguration(TestConfiguration5.class)
.run((context) -> assertThat(context.getBean(JmsMessagingTemplate.class).getDefaultDestinationName())
.isEqualTo("fooBar"));
}
@Test
void testDefaultJmsListenerConfiguration() {
this.contextRunner.withUserConfiguration(TestConfiguration.class).run((loaded) -> {
ConnectionFactory connectionFactory = loaded.getBean(ConnectionFactory.class);
assertThat(loaded).hasSingleBean(DefaultJmsListenerContainerFactory.class);
DefaultJmsListenerContainerFactory containerFactory = loaded
.getBean(DefaultJmsListenerContainerFactory.class);
SimpleJmsListenerEndpoint jmsListenerEndpoint = new SimpleJmsListenerEndpoint();
jmsListenerEndpoint.setMessageListener((message) -> {
});
DefaultMessageListenerContainer container = containerFactory.createListenerContainer(jmsListenerEndpoint);
assertThat(container.getClientId()).isNull();
assertThat(container.getConcurrentConsumers()).isEqualTo(1);
assertThat(container.getConnectionFactory()).isSameAs(connectionFactory);
assertThat(container.getMaxConcurrentConsumers()).isEqualTo(1);
assertThat(container.getSessionAcknowledgeMode()).isEqualTo(Session.AUTO_ACKNOWLEDGE);
assertThat(container.isAutoStartup()).isTrue();
assertThat(container.isPubSubDomain()).isFalse();
assertThat(container.isSubscriptionDurable()).isFalse();
assertThat(container).hasFieldOrPropertyWithValue("receiveTimeout", 1000L);
});
}
@Test
void testEnableJmsCreateDefaultContainerFactory() {
this.contextRunner.withUserConfiguration(EnableJmsConfiguration.class)
.run((context) -> assertThat(context)
.getBean("jmsListenerContainerFactory", JmsListenerContainerFactory.class)
.isExactlyInstanceOf(DefaultJmsListenerContainerFactory.class));
}
@Test
void testJmsListenerContainerFactoryBackOff() {
this.contextRunner.withUserConfiguration(TestConfiguration6.class, EnableJmsConfiguration.class)
.run((context) -> assertThat(context)
.getBean("jmsListenerContainerFactory", JmsListenerContainerFactory.class)
.isExactlyInstanceOf(SimpleJmsListenerContainerFactory.class));
}
@Test
void jmsListenerContainerFactoryWhenMultipleConnectionFactoryBeansShouldBackOff() {
this.contextRunner.withUserConfiguration(TestConfiguration10.class)
.run((context) -> assertThat(context).doesNotHaveBean(JmsListenerContainerFactory.class));
}
@Test
void testJmsListenerContainerFactoryWithCustomSettings() {
this.contextRunner.withUserConfiguration(EnableJmsConfiguration.class)
.withPropertyValues("spring.jms.listener.autoStartup=false",
"spring.jms.listener.session.acknowledgeMode=client",
"spring.jms.listener.session.transacted=false", "spring.jms.listener.minConcurrency=2",
"spring.jms.listener.receiveTimeout=2s", "spring.jms.listener.maxConcurrency=10",
"spring.jms.subscription-durable=true", "spring.jms.client-id=exampleId",
"spring.jms.listener.max-messages-per-task=5")
.run(this::testJmsListenerContainerFactoryWithCustomSettings);
}
private void testJmsListenerContainerFactoryWithCustomSettings(AssertableApplicationContext loaded) {
DefaultMessageListenerContainer container = getContainer(loaded, "jmsListenerContainerFactory");
assertThat(container.isAutoStartup()).isFalse();
assertThat(container.getSessionAcknowledgeMode()).isEqualTo(Session.CLIENT_ACKNOWLEDGE);
assertThat(container.isSessionTransacted()).isFalse();
assertThat(container.getConcurrentConsumers()).isEqualTo(2);
assertThat(container.getMaxConcurrentConsumers()).isEqualTo(10);
assertThat(container).hasFieldOrPropertyWithValue("receiveTimeout", 2000L);
assertThat(container).hasFieldOrPropertyWithValue("maxMessagesPerTask", 5);
assertThat(container.isSubscriptionDurable()).isTrue();
assertThat(container.getClientId()).isEqualTo("exampleId");
}
@Test
void testJmsListenerContainerFactoryWithNonStandardAcknowledgeMode() {
this.contextRunner.withUserConfiguration(EnableJmsConfiguration.class)
.withPropertyValues("spring.jms.listener.session.acknowledge-mode=9")
.run((context) -> {
DefaultMessageListenerContainer container = getContainer(context, "jmsListenerContainerFactory");
assertThat(container.getSessionAcknowledgeMode()).isEqualTo(9);
});
}
@Test
void testJmsListenerContainerFactoryWithDefaultSettings() {
this.contextRunner.withUserConfiguration(EnableJmsConfiguration.class)
.run(this::testJmsListenerContainerFactoryWithDefaultSettings);
}
private void testJmsListenerContainerFactoryWithDefaultSettings(AssertableApplicationContext loaded) {
DefaultMessageListenerContainer container = getContainer(loaded, "jmsListenerContainerFactory");
assertThat(container).hasFieldOrPropertyWithValue("receiveTimeout", 1000L);
}
@Test
void testDefaultContainerFactoryWithJtaTransactionManager() {
this.contextRunner.withUserConfiguration(TestConfiguration7.class, EnableJmsConfiguration.class)
.run((context) -> {
DefaultMessageListenerContainer container = getContainer(context, "jmsListenerContainerFactory");
assertThat(container.isSessionTransacted()).isFalse();
assertThat(container).hasFieldOrPropertyWithValue("transactionManager",
context.getBean(JtaTransactionManager.class));
});
}
@Test
void testDefaultContainerFactoryWithJtaTransactionManagerAndSessionTransactedEnabled() {
this.contextRunner.withUserConfiguration(TestConfiguration7.class, EnableJmsConfiguration.class)
.withPropertyValues("spring.jms.listener.session.transacted=true")
.run((context) -> {
DefaultMessageListenerContainer container = getContainer(context, "jmsListenerContainerFactory");
assertThat(container.isSessionTransacted()).isTrue();
assertThat(container).hasFieldOrPropertyWithValue("transactionManager",
context.getBean(JtaTransactionManager.class));
});
}
@Test
void testDefaultContainerFactoryNonJtaTransactionManager() {
this.contextRunner.withUserConfiguration(TestConfiguration8.class, EnableJmsConfiguration.class)
.run((context) -> {
DefaultMessageListenerContainer container = getContainer(context, "jmsListenerContainerFactory");
assertThat(container.isSessionTransacted()).isTrue();
assertThat(container).hasFieldOrPropertyWithValue("transactionManager", null);
});
}
@Test
void testDefaultContainerFactoryNoTransactionManager() {
this.contextRunner.withUserConfiguration(EnableJmsConfiguration.class).run((context) -> {
DefaultMessageListenerContainer container = getContainer(context, "jmsListenerContainerFactory");
assertThat(container.isSessionTransacted()).isTrue();
assertThat(container).hasFieldOrPropertyWithValue("transactionManager", null);
});
}
@Test
void testDefaultContainerFactoryNoTransactionManagerAndSessionTransactedDisabled() {
this.contextRunner.withUserConfiguration(EnableJmsConfiguration.class)
.withPropertyValues("spring.jms.listener.session.transacted=false")
.run((context) -> {
DefaultMessageListenerContainer container = getContainer(context, "jmsListenerContainerFactory");
assertThat(container.isSessionTransacted()).isFalse();
assertThat(container).hasFieldOrPropertyWithValue("transactionManager", null);
});
}
@Test
void testDefaultContainerFactoryWithMessageConverters() {
this.contextRunner.withUserConfiguration(MessageConvertersConfiguration.class, EnableJmsConfiguration.class)
.run((context) -> {
DefaultMessageListenerContainer container = getContainer(context, "jmsListenerContainerFactory");
assertThat(container.getMessageConverter()).isSameAs(context.getBean("myMessageConverter"));
});
}
@Test
void testDefaultContainerFactoryWithExceptionListener() {
ExceptionListener exceptionListener = mock(ExceptionListener.class);
this.contextRunner.withUserConfiguration(EnableJmsConfiguration.class)
.withBean(ExceptionListener.class, () -> exceptionListener)
.run((context) -> {
DefaultMessageListenerContainer container = getContainer(context, "jmsListenerContainerFactory");
assertThat(container.getExceptionListener()).isSameAs(exceptionListener);
});
}
@Test
void testDefaultContainerFactoryWithObservationRegistry() {
ObservationRegistry observationRegistry = mock(ObservationRegistry.class);
this.contextRunner.withUserConfiguration(EnableJmsConfiguration.class)
.withBean(ObservationRegistry.class, () -> observationRegistry)
.run((context) -> {
DefaultMessageListenerContainer container = getContainer(context, "jmsListenerContainerFactory");
assertThat(container.getObservationRegistry()).isSameAs(observationRegistry);
});
}
@Test
void testCustomContainerFactoryWithConfigurer() {
this.contextRunner.withUserConfiguration(TestConfiguration9.class, EnableJmsConfiguration.class)
.withPropertyValues("spring.jms.listener.autoStartup=false")
.run((context) -> {
DefaultMessageListenerContainer container = getContainer(context, "customListenerContainerFactory");
assertThat(container.getCacheLevel()).isEqualTo(DefaultMessageListenerContainer.CACHE_CONSUMER);
assertThat(container.isAutoStartup()).isFalse();
});
}
private DefaultMessageListenerContainer getContainer(AssertableApplicationContext loaded, String name) {
JmsListenerContainerFactory<?> factory = loaded.getBean(name, JmsListenerContainerFactory.class);
assertThat(factory).isInstanceOf(DefaultJmsListenerContainerFactory.class);
return ((DefaultJmsListenerContainerFactory) factory).createListenerContainer(mock(JmsListenerEndpoint.class));
}
@Test
void testJmsTemplateWithMessageConverter() {
this.contextRunner.withUserConfiguration(MessageConvertersConfiguration.class).run((context) -> {
JmsTemplate jmsTemplate = context.getBean(JmsTemplate.class);
assertThat(jmsTemplate.getMessageConverter()).isSameAs(context.getBean("myMessageConverter"));
});
}
@Test
void testJmsTemplateWithDestinationResolver() {
this.contextRunner.withUserConfiguration(DestinationResolversConfiguration.class)
.run((context) -> assertThat(context.getBean(JmsTemplate.class).getDestinationResolver())
.isSameAs(context.getBean("myDestinationResolver")));
}
@Test
void testJmsTemplateWithObservationRegistry() {
ObservationRegistry observationRegistry = mock(ObservationRegistry.class);
this.contextRunner.withUserConfiguration(EnableJmsConfiguration.class)
.withBean(ObservationRegistry.class, () -> observationRegistry)
.run((context) -> {
JmsTemplate jmsTemplate = context.getBean(JmsTemplate.class);
assertThat(jmsTemplate).extracting("observationRegistry").isSameAs(observationRegistry);
});
}
@Test
void testJmsTemplateFullCustomization() {
this.contextRunner.withUserConfiguration(MessageConvertersConfiguration.class)
.withPropertyValues("spring.jms.template.session.acknowledge-mode=client",
"spring.jms.template.session.transacted=true", "spring.jms.template.default-destination=testQueue",
"spring.jms.template.delivery-delay=500", "spring.jms.template.delivery-mode=non-persistent",
"spring.jms.template.priority=6", "spring.jms.template.time-to-live=6000",
"spring.jms.template.receive-timeout=2000")
.run((context) -> {
JmsTemplate jmsTemplate = context.getBean(JmsTemplate.class);
assertThat(jmsTemplate.getMessageConverter()).isSameAs(context.getBean("myMessageConverter"));
assertThat(jmsTemplate.isPubSubDomain()).isFalse();
assertThat(jmsTemplate.getSessionAcknowledgeMode()).isEqualTo(Session.CLIENT_ACKNOWLEDGE);
assertThat(jmsTemplate.isSessionTransacted()).isTrue();
assertThat(jmsTemplate.getDefaultDestinationName()).isEqualTo("testQueue");
assertThat(jmsTemplate.getDeliveryDelay()).isEqualTo(500);
assertThat(jmsTemplate.getDeliveryMode()).isOne();
assertThat(jmsTemplate.getPriority()).isEqualTo(6);
assertThat(jmsTemplate.getTimeToLive()).isEqualTo(6000);
assertThat(jmsTemplate.isExplicitQosEnabled()).isTrue();
assertThat(jmsTemplate.getReceiveTimeout()).isEqualTo(2000);
});
}
@Test
void testJmsTemplateWithNonStandardAcknowledgeMode() {
this.contextRunner.withUserConfiguration(EnableJmsConfiguration.class)
.withPropertyValues("spring.jms.template.session.acknowledge-mode=7")
.run((context) -> {
JmsTemplate jmsTemplate = context.getBean(JmsTemplate.class);
assertThat(jmsTemplate.getSessionAcknowledgeMode()).isEqualTo(7);
});
}
@Test
void testJmsMessagingTemplateUseConfiguredDefaultDestination() {
this.contextRunner.withPropertyValues("spring.jms.template.default-destination=testQueue").run((context) -> {
JmsMessagingTemplate messagingTemplate = context.getBean(JmsMessagingTemplate.class);
assertThat(messagingTemplate.getDefaultDestinationName()).isEqualTo("testQueue");
});
}
@Test
void testPubSubDisabledByDefault() {
this.contextRunner.withUserConfiguration(TestConfiguration.class)
.run((context) -> assertThat(context.getBean(JmsTemplate.class).isPubSubDomain()).isFalse());
}
@Test
void testJmsTemplatePostProcessedSoThatPubSubIsTrue() {
this.contextRunner.withUserConfiguration(TestConfiguration4.class)
.run((context) -> assertThat(context.getBean(JmsTemplate.class).isPubSubDomain()).isTrue());
}
@Test
void testPubSubDomainActive() {
this.contextRunner.withUserConfiguration(TestConfiguration.class)
.withPropertyValues("spring.jms.pubSubDomain:true")
.run((context) -> {
JmsTemplate jmsTemplate = context.getBean(JmsTemplate.class);
DefaultMessageListenerContainer defaultMessageListenerContainer = context
.getBean(DefaultJmsListenerContainerFactory.class)
.createListenerContainer(mock(JmsListenerEndpoint.class));
assertThat(jmsTemplate.isPubSubDomain()).isTrue();
assertThat(defaultMessageListenerContainer.isPubSubDomain()).isTrue();
});
}
@Test
void testPubSubDomainOverride() {
this.contextRunner.withUserConfiguration(TestConfiguration.class)
.withPropertyValues("spring.jms.pubSubDomain:false")
.run((context) -> {
assertThat(context).hasSingleBean(JmsTemplate.class);
assertThat(context).hasSingleBean(ConnectionFactory.class);
JmsTemplate jmsTemplate = context.getBean(JmsTemplate.class);
ConnectionFactory factory = context.getBean(ConnectionFactory.class);
assertThat(jmsTemplate).isNotNull();
assertThat(jmsTemplate.isPubSubDomain()).isFalse();
assertThat(factory).isNotNull().isEqualTo(jmsTemplate.getConnectionFactory());
});
}
@Test
void enableJmsAutomatically() {
this.contextRunner.withUserConfiguration(NoEnableJmsConfiguration.class)
.run((context) -> assertThat(context)
.hasBean(JmsListenerConfigUtils.JMS_LISTENER_ANNOTATION_PROCESSOR_BEAN_NAME)
.hasBean(JmsListenerConfigUtils.JMS_LISTENER_ENDPOINT_REGISTRY_BEAN_NAME));
}
@Test
void runtimeHintsAreRegisteredForBindingOfAcknowledgeMode() {
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
context.register(TestConfiguration2.class, JmsAutoConfiguration.class);
TestGenerationContext generationContext = new TestGenerationContext();
new ApplicationContextAotGenerator().processAheadOfTime(context, generationContext);
assertThat(RuntimeHintsPredicates.reflection().onMethodInvocation(AcknowledgeMode.class, "of"))
.accepts(generationContext.getRuntimeHints());
}
}
@Configuration(proxyBeanMethods = false)
static class TestConfiguration {
}
@Configuration(proxyBeanMethods = false)
static class TestConfiguration2 {
@Bean
ConnectionFactory customConnectionFactory() {
return mock(ConnectionFactory.class);
}
}
@Configuration(proxyBeanMethods = false)
static class TestConfiguration3 {
@Bean
JmsTemplate jmsTemplate(ConnectionFactory connectionFactory) {
JmsTemplate jmsTemplate = new JmsTemplate(connectionFactory);
jmsTemplate.setPriority(999);
return jmsTemplate;
}
}
@Configuration(proxyBeanMethods = false)
static class TestConfiguration4 implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
if (bean.getClass().isAssignableFrom(JmsTemplate.class)) {
JmsTemplate jmsTemplate = (JmsTemplate) bean;
jmsTemplate.setPubSubDomain(true);
}
return bean;
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
return bean;
}
}
@Configuration(proxyBeanMethods = false)
static class TestConfiguration5 {
@Bean
JmsMessagingTemplate jmsMessagingTemplate(JmsTemplate jmsTemplate) {
JmsMessagingTemplate messagingTemplate = new JmsMessagingTemplate(jmsTemplate);
messagingTemplate.setDefaultDestinationName("fooBar");
return messagingTemplate;
}
}
@Configuration(proxyBeanMethods = false)
static class TestConfiguration6 {
@Bean
JmsListenerContainerFactory<?> jmsListenerContainerFactory(ConnectionFactory connectionFactory) {
SimpleJmsListenerContainerFactory factory = new SimpleJmsListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
return factory;
}
}
@Configuration(proxyBeanMethods = false)
static class TestConfiguration7 {
@Bean
JtaTransactionManager transactionManager() {
return mock(JtaTransactionManager.class);
}
}
@Configuration(proxyBeanMethods = false)
static class TestConfiguration8 {
@Bean
DataSourceTransactionManager transactionManager() {
return mock(DataSourceTransactionManager.class);
}
}
@Configuration(proxyBeanMethods = false)
static class MessageConvertersConfiguration {
@Bean
@Primary
MessageConverter myMessageConverter() {
return mock(MessageConverter.class);
}
@Bean
MessageConverter anotherMessageConverter() {
return mock(MessageConverter.class);
}
}
@Configuration(proxyBeanMethods = false)
static class DestinationResolversConfiguration {
@Bean
@Primary
DestinationResolver myDestinationResolver() {
return mock(DestinationResolver.class);
}
@Bean
DestinationResolver anotherDestinationResolver() {
return mock(DestinationResolver.class);
}
}
@Configuration(proxyBeanMethods = false)
static class TestConfiguration9 {
@Bean
JmsListenerContainerFactory<?> customListenerContainerFactory(
DefaultJmsListenerContainerFactoryConfigurer configurer, ConnectionFactory connectionFactory) {
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
configurer.configure(factory, connectionFactory);
factory.setCacheLevel(DefaultMessageListenerContainer.CACHE_CONSUMER);
return factory;
}
}
@Configuration(proxyBeanMethods = false)
static class TestConfiguration10 {
@Bean
ConnectionFactory connectionFactory1() {
return mock(ConnectionFactory.class);
}
@Bean
ConnectionFactory connectionFactory2() {
return mock(ConnectionFactory.class);
}
}
@Configuration(proxyBeanMethods = false)
@EnableJms
static class EnableJmsConfiguration {
}
@Configuration(proxyBeanMethods = false)
static class NoEnableJmsConfiguration {
}
}

View File

@@ -0,0 +1,89 @@
/*
* Copyright 2012-2025 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.jms.autoconfigure;
import java.time.Duration;
import org.junit.jupiter.api.Test;
import org.springframework.jms.listener.AbstractPollingMessageListenerContainer;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link JmsProperties}.
*
* @author Stephane Nicoll
*/
class JmsPropertiesTests {
@Test
void formatConcurrencyNull() {
JmsProperties properties = new JmsProperties();
assertThat(properties.getListener().formatConcurrency()).isNull();
}
@Test
void formatConcurrencyOnlyLowerBound() {
JmsProperties properties = new JmsProperties();
properties.getListener().setMinConcurrency(2);
assertThat(properties.getListener().formatConcurrency()).isEqualTo("2-2");
}
@Test
void formatConcurrencyOnlyHigherBound() {
JmsProperties properties = new JmsProperties();
properties.getListener().setMaxConcurrency(5);
assertThat(properties.getListener().formatConcurrency()).isEqualTo("1-5");
}
@Test
void formatConcurrencyBothBounds() {
JmsProperties properties = new JmsProperties();
properties.getListener().setMinConcurrency(2);
properties.getListener().setMaxConcurrency(10);
assertThat(properties.getListener().formatConcurrency()).isEqualTo("2-10");
}
@Test
void setDeliveryModeEnablesQoS() {
JmsProperties properties = new JmsProperties();
properties.getTemplate().setDeliveryMode(JmsProperties.DeliveryMode.PERSISTENT);
assertThat(properties.getTemplate().determineQosEnabled()).isTrue();
}
@Test
void setPriorityEnablesQoS() {
JmsProperties properties = new JmsProperties();
properties.getTemplate().setPriority(6);
assertThat(properties.getTemplate().determineQosEnabled()).isTrue();
}
@Test
void setTimeToLiveEnablesQoS() {
JmsProperties properties = new JmsProperties();
properties.getTemplate().setTimeToLive(Duration.ofSeconds(5));
assertThat(properties.getTemplate().determineQosEnabled()).isTrue();
}
@Test
void defaultReceiveTimeoutMatchesListenerContainersDefault() {
assertThat(new JmsProperties().getListener().getReceiveTimeout())
.hasMillis(AbstractPollingMessageListenerContainer.DEFAULT_RECEIVE_TIMEOUT);
}
}

View File

@@ -0,0 +1,127 @@
/*
* Copyright 2012-2025 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.jms.autoconfigure;
import javax.naming.Context;
import jakarta.jms.ConnectionFactory;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.jndi.JndiPropertiesHidingClassLoader;
import org.springframework.boot.autoconfigure.jndi.TestableInitialContextFactory;
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.context.runner.ContextConsumer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link JndiConnectionFactoryAutoConfiguration}.
* PersistenceExceptionTranslationAutoConfigurationTests
*
* @author Stephane Nicoll
*/
class JndiConnectionFactoryAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(JndiConnectionFactoryAutoConfiguration.class));
private ClassLoader threadContextClassLoader;
private String initialContextFactory;
@BeforeEach
void setupJndi() {
this.initialContextFactory = System.getProperty(Context.INITIAL_CONTEXT_FACTORY);
System.setProperty(Context.INITIAL_CONTEXT_FACTORY, TestableInitialContextFactory.class.getName());
this.threadContextClassLoader = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(new JndiPropertiesHidingClassLoader(getClass().getClassLoader()));
}
@AfterEach
void cleanUp() {
TestableInitialContextFactory.clearAll();
if (this.initialContextFactory != null) {
System.setProperty(Context.INITIAL_CONTEXT_FACTORY, this.initialContextFactory);
}
else {
System.clearProperty(Context.INITIAL_CONTEXT_FACTORY);
}
Thread.currentThread().setContextClassLoader(this.threadContextClassLoader);
}
@Test
void detectNoAvailableCandidates() {
this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(ConnectionFactory.class));
}
@Test
void detectWithJmsXAConnectionFactory() {
ConnectionFactory connectionFactory = configureConnectionFactory("java:/JmsXA");
this.contextRunner.run(assertConnectionFactory(connectionFactory));
}
@Test
void detectWithXAConnectionFactory() {
ConnectionFactory connectionFactory = configureConnectionFactory("java:/XAConnectionFactory");
this.contextRunner.run(assertConnectionFactory(connectionFactory));
}
@Test
void jndiNamePropertySet() {
ConnectionFactory connectionFactory = configureConnectionFactory("java:comp/env/myCF");
this.contextRunner.withPropertyValues("spring.jms.jndi-name=java:comp/env/myCF")
.run(assertConnectionFactory(connectionFactory));
}
@Test
void jndiNamePropertySetWithResourceRef() {
ConnectionFactory connectionFactory = configureConnectionFactory("java:comp/env/myCF");
this.contextRunner.withPropertyValues("spring.jms.jndi-name=myCF")
.run(assertConnectionFactory(connectionFactory));
}
@Test
void jndiNamePropertySetWithWrongValue() {
this.contextRunner.withPropertyValues("spring.jms.jndi-name=doesNotExistCF").run((context) -> {
assertThat(context).hasFailed();
assertThat(context).getFailure()
.isInstanceOf(BeanCreationException.class)
.hasMessageContaining("doesNotExistCF");
});
}
private ContextConsumer<AssertableApplicationContext> assertConnectionFactory(ConnectionFactory connectionFactory) {
return (context) -> {
assertThat(context).hasSingleBean(ConnectionFactory.class).hasBean("jmsConnectionFactory");
assertThat(context.getBean(ConnectionFactory.class)).isSameAs(connectionFactory)
.isSameAs(context.getBean("jmsConnectionFactory"));
};
}
private ConnectionFactory configureConnectionFactory(String name) {
ConnectionFactory connectionFactory = mock(ConnectionFactory.class);
TestableInitialContextFactory.bind(name, connectionFactory);
return connectionFactory;
}
}