Skip to content

Commit 6fbc328

Browse files
committed
Polish "Add support for configuring non-standard JMS acknowledge modes"
See gh-37576
1 parent d72fb8e commit 6fbc328

File tree

7 files changed

+257
-60
lines changed

7 files changed

+257
-60
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
* Copyright 2012-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.autoconfigure.jms;
18+
19+
import java.util.HashMap;
20+
import java.util.Map;
21+
22+
import jakarta.jms.Session;
23+
24+
import org.springframework.jms.support.JmsAccessor;
25+
26+
/**
27+
* Acknowledge modes for a JMS Session. Supports the acknowledge modes defined by
28+
* {@link jakarta.jms.Session} as well as other, non-standard modes.
29+
*
30+
* <p>
31+
* Note that {@link jakarta.jms.Session#SESSION_TRANSACTED} is not defined. It should be
32+
* handled through a call to {@link JmsAccessor#setSessionTransacted(boolean)}.
33+
*
34+
* @author Andy Wilkinson
35+
* @since 3.2.0
36+
*/
37+
public final class AcknowledgeMode {
38+
39+
private static final Map<String, AcknowledgeMode> knownModes = new HashMap<>(3);
40+
41+
/**
42+
* Messages sent or received from the session are automatically acknowledged. This is
43+
* the simplest mode and enables once-only message delivery guarantee.
44+
*/
45+
public static final AcknowledgeMode AUTO = new AcknowledgeMode(Session.AUTO_ACKNOWLEDGE);
46+
47+
/**
48+
* Messages are acknowledged once the message listener implementation has called
49+
* {@link jakarta.jms.Message#acknowledge()}. This mode gives the application (rather
50+
* than the JMS provider) complete control over message acknowledgement.
51+
*/
52+
public static final AcknowledgeMode CLIENT = new AcknowledgeMode(Session.CLIENT_ACKNOWLEDGE);
53+
54+
/**
55+
* Similar to auto acknowledgment except that said acknowledgment is lazy. As a
56+
* consequence, the messages might be delivered more than once. This mode enables
57+
* at-least-once message delivery guarantee.
58+
*/
59+
public static final AcknowledgeMode DUPS_OK = new AcknowledgeMode(Session.DUPS_OK_ACKNOWLEDGE);
60+
61+
static {
62+
knownModes.put("auto", AUTO);
63+
knownModes.put("client", CLIENT);
64+
knownModes.put("dupsok", DUPS_OK);
65+
}
66+
67+
private final int mode;
68+
69+
private AcknowledgeMode(int mode) {
70+
this.mode = mode;
71+
}
72+
73+
public int getMode() {
74+
return this.mode;
75+
}
76+
77+
/**
78+
* Creates an {@code AcknowledgeMode} of the given {@code mode}. The mode may be
79+
* {@code auto}, {@code client}, {@code dupsok} or a non-standard acknowledge mode
80+
* that can be {@link Integer#parseInt parsed as an integer}.
81+
* @param mode the mode
82+
* @return the acknowledge mode
83+
*/
84+
public static AcknowledgeMode of(String mode) {
85+
String canonicalMode = canonicalize(mode);
86+
AcknowledgeMode knownMode = knownModes.get(canonicalMode);
87+
try {
88+
return (knownMode != null) ? knownMode : new AcknowledgeMode(Integer.parseInt(canonicalMode));
89+
}
90+
catch (NumberFormatException ex) {
91+
throw new IllegalArgumentException("'" + mode
92+
+ "' is neither a known acknowledge mode (auto, client, or dups_ok) nor an integer value");
93+
}
94+
}
95+
96+
private static String canonicalize(String input) {
97+
StringBuilder canonicalName = new StringBuilder(input.length());
98+
input.chars()
99+
.filter(Character::isLetterOrDigit)
100+
.map(Character::toLowerCase)
101+
.forEach((c) -> canonicalName.append((char) c));
102+
return canonicalName.toString();
103+
}
104+
105+
}

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/DefaultJmsListenerContainerFactoryConfigurer.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,7 @@ public void configure(DefaultJmsListenerContainerFactory factory, ConnectionFact
111111
map.from(this.destinationResolver).to(factory::setDestinationResolver);
112112
map.from(this.messageConverter).to(factory::setMessageConverter);
113113
map.from(this.exceptionListener).to(factory::setExceptionListener);
114-
map.from(sessionProperties.getAcknowledgeMode())
115-
.as(JmsAcknowledgeModeMapper::map)
116-
.to(factory::setSessionAcknowledgeMode);
114+
map.from(sessionProperties.getAcknowledgeMode()::getMode).to(factory::setSessionAcknowledgeMode);
117115
if (this.transactionManager == null && sessionProperties.getTransacted() == null) {
118116
factory.setSessionTransacted(true);
119117
}

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAcknowledgeModeMapper.java

Lines changed: 0 additions & 46 deletions
This file was deleted.

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfiguration.java

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,31 @@
1717
package org.springframework.boot.autoconfigure.jms;
1818

1919
import java.time.Duration;
20+
import java.util.List;
2021

2122
import jakarta.jms.ConnectionFactory;
2223
import jakarta.jms.Message;
2324

25+
import org.springframework.aot.hint.ExecutableMode;
26+
import org.springframework.aot.hint.RuntimeHints;
27+
import org.springframework.aot.hint.RuntimeHintsRegistrar;
28+
import org.springframework.aot.hint.TypeReference;
2429
import org.springframework.beans.factory.ObjectProvider;
2530
import org.springframework.boot.autoconfigure.AutoConfiguration;
2631
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
2732
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
2833
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
2934
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
3035
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
36+
import org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration.JmsRuntimeHints;
3137
import org.springframework.boot.autoconfigure.jms.JmsProperties.DeliveryMode;
3238
import org.springframework.boot.autoconfigure.jms.JmsProperties.Template;
3339
import org.springframework.boot.context.properties.EnableConfigurationProperties;
3440
import org.springframework.boot.context.properties.PropertyMapper;
3541
import org.springframework.context.annotation.Bean;
3642
import org.springframework.context.annotation.Configuration;
3743
import org.springframework.context.annotation.Import;
44+
import org.springframework.context.annotation.ImportRuntimeHints;
3845
import org.springframework.jms.core.JmsMessageOperations;
3946
import org.springframework.jms.core.JmsMessagingTemplate;
4047
import org.springframework.jms.core.JmsOperations;
@@ -55,6 +62,7 @@
5562
@ConditionalOnBean(ConnectionFactory.class)
5663
@EnableConfigurationProperties(JmsProperties.class)
5764
@Import(JmsAnnotationDrivenConfiguration.class)
65+
@ImportRuntimeHints(JmsRuntimeHints.class)
5866
public class JmsAutoConfiguration {
5967

6068
@Configuration(proxyBeanMethods = false)
@@ -89,9 +97,7 @@ public JmsTemplate jmsTemplate(ConnectionFactory connectionFactory) {
8997

9098
private void mapTemplateProperties(Template properties, JmsTemplate template) {
9199
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
92-
map.from(properties.getSession()::getAcknowledgeMode)
93-
.to((acknowledgeMode) -> template
94-
.setSessionAcknowledgeMode(JmsAcknowledgeModeMapper.map(acknowledgeMode)));
100+
map.from(properties.getSession().getAcknowledgeMode()::getMode).to(template::setSessionAcknowledgeMode);
95101
map.from(properties.getSession()::isTransacted).to(template::setSessionTransacted);
96102
map.from(properties::getDefaultDestination).whenNonNull().to(template::setDefaultDestinationName);
97103
map.from(properties::getDeliveryDelay).whenNonNull().as(Duration::toMillis).to(template::setDeliveryDelay);
@@ -125,4 +131,15 @@ private void mapTemplateProperties(Template properties, JmsMessagingTemplate mes
125131

126132
}
127133

134+
static class JmsRuntimeHints implements RuntimeHintsRegistrar {
135+
136+
@Override
137+
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
138+
hints.reflection()
139+
.registerType(TypeReference.of(AcknowledgeMode.class), (type) -> type.withMethod("of",
140+
List.of(TypeReference.of(String.class)), ExecutableMode.INVOKE));
141+
}
142+
143+
}
144+
128145
}

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsProperties.java

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -171,12 +171,12 @@ public void setAutoStartup(boolean autoStartup) {
171171

172172
@Deprecated(since = "3.2.0", forRemoval = true)
173173
@DeprecatedConfigurationProperty(replacement = "spring.jms.listener.session.acknowledge-mode", since = "3.2.0")
174-
public String getAcknowledgeMode() {
174+
public AcknowledgeMode getAcknowledgeMode() {
175175
return this.session.getAcknowledgeMode();
176176
}
177177

178178
@Deprecated(since = "3.2.0", forRemoval = true)
179-
public void setAcknowledgeMode(String acknowledgeMode) {
179+
public void setAcknowledgeMode(AcknowledgeMode acknowledgeMode) {
180180
this.session.setAcknowledgeMode(acknowledgeMode);
181181
}
182182

@@ -232,19 +232,19 @@ public static class Session {
232232
/**
233233
* Acknowledge mode of the listener container.
234234
*/
235-
private String acknowledgeMode = "auto";
235+
private AcknowledgeMode acknowledgeMode = AcknowledgeMode.AUTO;
236236

237237
/**
238238
* Whether the listener container should use transacted JMS sessions. Defaults
239239
* to false in the presence of a JtaTransactionManager and true otherwise.
240240
*/
241241
private Boolean transacted;
242242

243-
public String getAcknowledgeMode() {
243+
public AcknowledgeMode getAcknowledgeMode() {
244244
return this.acknowledgeMode;
245245
}
246246

247-
public void setAcknowledgeMode(String acknowledgeMode) {
247+
public void setAcknowledgeMode(AcknowledgeMode acknowledgeMode) {
248248
this.acknowledgeMode = acknowledgeMode;
249249
}
250250

@@ -376,18 +376,18 @@ public static class Session {
376376
/**
377377
* Acknowledge mode used when creating sessions.
378378
*/
379-
private String acknowledgeMode = "auto";
379+
private AcknowledgeMode acknowledgeMode = AcknowledgeMode.AUTO;
380380

381381
/**
382382
* Whether to use transacted sessions.
383383
*/
384384
private boolean transacted = false;
385385

386-
public String getAcknowledgeMode() {
386+
public AcknowledgeMode getAcknowledgeMode() {
387387
return this.acknowledgeMode;
388388
}
389389

390-
public void setAcknowledgeMode(String acknowledgeMode) {
390+
public void setAcknowledgeMode(AcknowledgeMode acknowledgeMode) {
391391
this.acknowledgeMode = acknowledgeMode;
392392
}
393393

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright 2012-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.autoconfigure.jms;
18+
19+
import jakarta.jms.Session;
20+
import org.junit.jupiter.api.Test;
21+
import org.junit.jupiter.params.ParameterizedTest;
22+
import org.junit.jupiter.params.provider.EnumSource;
23+
24+
import static org.assertj.core.api.Assertions.assertThat;
25+
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
26+
27+
/**
28+
* Tests for {@link AcknowledgeMode}.
29+
*
30+
* @author Andy Wilkinson
31+
*/
32+
class AcknowledgeModeTests {
33+
34+
@ParameterizedTest
35+
@EnumSource(Mapping.class)
36+
void stringIsMappedToInt(Mapping mapping) {
37+
assertThat(AcknowledgeMode.of(mapping.actual)).extracting(AcknowledgeMode::getMode).isEqualTo(mapping.expected);
38+
}
39+
40+
@Test
41+
void mapShouldThrowWhenMapIsCalledWithUnknownNonIntegerString() {
42+
assertThatIllegalArgumentException().isThrownBy(() -> AcknowledgeMode.of("some-string"))
43+
.withMessage(
44+
"'some-string' is neither a known acknowledge mode (auto, client, or dups_ok) nor an integer value");
45+
}
46+
47+
private enum Mapping {
48+
49+
AUTO_LOWER_CASE("auto", Session.AUTO_ACKNOWLEDGE),
50+
51+
CLIENT_LOWER_CASE("client", Session.CLIENT_ACKNOWLEDGE),
52+
53+
DUPS_OK_LOWER_CASE("dups_ok", Session.DUPS_OK_ACKNOWLEDGE),
54+
55+
AUTO_UPPER_CASE("AUTO", Session.AUTO_ACKNOWLEDGE),
56+
57+
CLIENT_UPPER_CASE("CLIENT", Session.CLIENT_ACKNOWLEDGE),
58+
59+
DUPS_OK_UPPER_CASE("DUPS_OK", Session.DUPS_OK_ACKNOWLEDGE),
60+
61+
AUTO_MIXED_CASE("AuTo", Session.AUTO_ACKNOWLEDGE),
62+
63+
CLIENT_MIXED_CASE("CliEnT", Session.CLIENT_ACKNOWLEDGE),
64+
65+
DUPS_OK_MIXED_CASE("dUPs_Ok", Session.DUPS_OK_ACKNOWLEDGE),
66+
67+
DUPS_OK_KEBAB_CASE("DUPS-OK", Session.DUPS_OK_ACKNOWLEDGE),
68+
69+
DUPS_OK_NO_SEPARATOR_UPPER_CASE("DUPSOK", Session.DUPS_OK_ACKNOWLEDGE),
70+
71+
DUPS_OK_NO_SEPARATOR_LOWER_CASE("dupsok", Session.DUPS_OK_ACKNOWLEDGE),
72+
73+
DUPS_OK_NO_SEPARATOR_MIXED_CASE("duPSok", Session.DUPS_OK_ACKNOWLEDGE),
74+
75+
INTEGER("36", 36);
76+
77+
private final String actual;
78+
79+
private final int expected;
80+
81+
Mapping(String actual, int expected) {
82+
this.actual = actual;
83+
this.expected = expected;
84+
}
85+
86+
}
87+
88+
}

0 commit comments

Comments
 (0)