Polish "Add support for configuring non-standard JMS acknowledge modes"

See gh-37576
This commit is contained in:
Andy Wilkinson 2023-09-27 18:11:50 +01:00
parent d72fb8e127
commit 6fbc328b4c
7 changed files with 257 additions and 60 deletions

View File

@ -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.
*
* <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

@ -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);
}

View File

@ -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<String, Integer> 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);
}
}

View File

@ -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));
}
}
}

View File

@ -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;
}

View File

@ -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;
}
}
}

View File

@ -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 {