diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/AcknowledgeMode.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/AcknowledgeMode.java
new file mode 100644
index 00000000000..f3c3240d7e2
--- /dev/null
+++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/AcknowledgeMode.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2012-2023 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.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.
+ *
+ *
+ * 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 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();
+ }
+
+}
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/DefaultJmsListenerContainerFactoryConfigurer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/DefaultJmsListenerContainerFactoryConfigurer.java
index fb9ef42f830..2e5f1cc58a0 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/DefaultJmsListenerContainerFactoryConfigurer.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/DefaultJmsListenerContainerFactoryConfigurer.java
@@ -111,9 +111,7 @@ public final class DefaultJmsListenerContainerFactoryConfigurer {
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())
- .as(JmsAcknowledgeModeMapper::map)
- .to(factory::setSessionAcknowledgeMode);
+ map.from(sessionProperties.getAcknowledgeMode()::getMode).to(factory::setSessionAcknowledgeMode);
if (this.transactionManager == null && sessionProperties.getTransacted() == null) {
factory.setSessionTransacted(true);
}
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAcknowledgeModeMapper.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAcknowledgeModeMapper.java
deleted file mode 100644
index 66991fe5ab8..00000000000
--- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAcknowledgeModeMapper.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright 2012-2023 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.util.HashMap;
-import java.util.Map;
-
-import jakarta.jms.Session;
-
-/**
- * Helper class used to map JMS acknowledge modes.
- *
- * @author Vedran Pavic
- */
-final class JmsAcknowledgeModeMapper {
-
- private static final Map acknowledgeModes = new HashMap<>(3);
-
- static {
- acknowledgeModes.put("auto", Session.AUTO_ACKNOWLEDGE);
- acknowledgeModes.put("client", Session.CLIENT_ACKNOWLEDGE);
- acknowledgeModes.put("dups_ok", Session.DUPS_OK_ACKNOWLEDGE);
- }
-
- private JmsAcknowledgeModeMapper() {
- }
-
- static int map(String acknowledgeMode) {
- return acknowledgeModes.computeIfAbsent(acknowledgeMode.toLowerCase(), Integer::parseInt);
- }
-
-}
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfiguration.java
index 9eaa347e81d..2c2ba4b5a0b 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfiguration.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfiguration.java
@@ -17,10 +17,15 @@
package org.springframework.boot.autoconfigure.jms;
import java.time.Duration;
+import java.util.List;
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;
@@ -28,6 +33,7 @@ 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.autoconfigure.jms.JmsAutoConfiguration.JmsRuntimeHints;
import org.springframework.boot.autoconfigure.jms.JmsProperties.DeliveryMode;
import org.springframework.boot.autoconfigure.jms.JmsProperties.Template;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@@ -35,6 +41,7 @@ import org.springframework.boot.context.properties.PropertyMapper;
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;
@@ -55,6 +62,7 @@ import org.springframework.jms.support.destination.DestinationResolver;
@ConditionalOnBean(ConnectionFactory.class)
@EnableConfigurationProperties(JmsProperties.class)
@Import(JmsAnnotationDrivenConfiguration.class)
+@ImportRuntimeHints(JmsRuntimeHints.class)
public class JmsAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@@ -89,9 +97,7 @@ public class JmsAutoConfiguration {
private void mapTemplateProperties(Template properties, JmsTemplate template) {
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
- map.from(properties.getSession()::getAcknowledgeMode)
- .to((acknowledgeMode) -> template
- .setSessionAcknowledgeMode(JmsAcknowledgeModeMapper.map(acknowledgeMode)));
+ 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);
@@ -125,4 +131,15 @@ public class JmsAutoConfiguration {
}
+ 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));
+ }
+
+ }
+
}
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsProperties.java
index d0eee224307..08b7a6d5ee5 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsProperties.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsProperties.java
@@ -171,12 +171,12 @@ public class JmsProperties {
@Deprecated(since = "3.2.0", forRemoval = true)
@DeprecatedConfigurationProperty(replacement = "spring.jms.listener.session.acknowledge-mode", since = "3.2.0")
- public String getAcknowledgeMode() {
+ public AcknowledgeMode getAcknowledgeMode() {
return this.session.getAcknowledgeMode();
}
@Deprecated(since = "3.2.0", forRemoval = true)
- public void setAcknowledgeMode(String acknowledgeMode) {
+ public void setAcknowledgeMode(AcknowledgeMode acknowledgeMode) {
this.session.setAcknowledgeMode(acknowledgeMode);
}
@@ -232,7 +232,7 @@ public class JmsProperties {
/**
* Acknowledge mode of the listener container.
*/
- private String acknowledgeMode = "auto";
+ private AcknowledgeMode acknowledgeMode = AcknowledgeMode.AUTO;
/**
* Whether the listener container should use transacted JMS sessions. Defaults
@@ -240,11 +240,11 @@ public class JmsProperties {
*/
private Boolean transacted;
- public String getAcknowledgeMode() {
+ public AcknowledgeMode getAcknowledgeMode() {
return this.acknowledgeMode;
}
- public void setAcknowledgeMode(String acknowledgeMode) {
+ public void setAcknowledgeMode(AcknowledgeMode acknowledgeMode) {
this.acknowledgeMode = acknowledgeMode;
}
@@ -376,18 +376,18 @@ public class JmsProperties {
/**
* Acknowledge mode used when creating sessions.
*/
- private String acknowledgeMode = "auto";
+ private AcknowledgeMode acknowledgeMode = AcknowledgeMode.AUTO;
/**
* Whether to use transacted sessions.
*/
private boolean transacted = false;
- public String getAcknowledgeMode() {
+ public AcknowledgeMode getAcknowledgeMode() {
return this.acknowledgeMode;
}
- public void setAcknowledgeMode(String acknowledgeMode) {
+ public void setAcknowledgeMode(AcknowledgeMode acknowledgeMode) {
this.acknowledgeMode = acknowledgeMode;
}
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/AcknowledgeModeTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/AcknowledgeModeTests.java
new file mode 100644
index 00000000000..77957f5a967
--- /dev/null
+++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/AcknowledgeModeTests.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2012-2023 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.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(Mapping.class)
+ 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;
+ }
+
+ }
+
+}
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java
index 49f5ddb3b27..b9b6025bae4 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java
@@ -24,14 +24,18 @@ import jakarta.jms.Session;
import org.apache.activemq.artemis.jms.client.ActiveMQConnectionFactory;
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.autoconfigure.jms.artemis.ArtemisAutoConfiguration;
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.annotation.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;
@@ -160,6 +164,16 @@ class JmsAutoConfigurationTests {
assertThat(container).hasFieldOrPropertyWithValue("receiveTimeout", 2000L);
}
+ @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)
@@ -300,6 +314,16 @@ class JmsAutoConfigurationTests {
});
}
+ @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) -> {
@@ -367,6 +391,17 @@ class JmsAutoConfigurationTests {
.hasBean(JmsListenerConfigUtils.JMS_LISTENER_ENDPOINT_REGISTRY_BEAN_NAME));
}
+ @Test
+ void runtimeHintsAreRegisteredForBindingOfAcknowledgeMode() {
+ try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
+ context.register(ArtemisAutoConfiguration.class, JmsAutoConfiguration.class);
+ TestGenerationContext generationContext = new TestGenerationContext();
+ new ApplicationContextAotGenerator().processAheadOfTime(context, generationContext);
+ assertThat(RuntimeHintsPredicates.reflection().onMethod(AcknowledgeMode.class, "of").invoke())
+ .accepts(generationContext.getRuntimeHints());
+ }
+ }
+
@Configuration(proxyBeanMethods = false)
static class TestConfiguration {