From 6fbc328b4cd00e4ad9d4ded852dd6064c24d4d25 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 27 Sep 2023 18:11:50 +0100 Subject: [PATCH] Polish "Add support for configuring non-standard JMS acknowledge modes" See gh-37576 --- .../autoconfigure/jms/AcknowledgeMode.java | 105 ++++++++++++++++++ ...JmsListenerContainerFactoryConfigurer.java | 4 +- .../jms/JmsAcknowledgeModeMapper.java | 46 -------- .../jms/JmsAutoConfiguration.java | 23 +++- .../boot/autoconfigure/jms/JmsProperties.java | 16 +-- .../jms/AcknowledgeModeTests.java | 88 +++++++++++++++ .../jms/JmsAutoConfigurationTests.java | 35 ++++++ 7 files changed, 257 insertions(+), 60 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/AcknowledgeMode.java delete mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAcknowledgeModeMapper.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/AcknowledgeModeTests.java 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 {