Skip to content

Commit fc2890d

Browse files
committed
Use native connection factory with message listener containers
This commit updates the auto-configuration to use the native connection factory for configuring message listener containers. Previously, the connection factory that could have been wrapped in a caching connection factory was used. While using a caching connection factory is suitable for sending messages (i.e. JmsTemplate usage), it isn't for message listeners as they need to own the connection for local recovery purposes. Closes gh-39816
1 parent 369cfc4 commit fc2890d

File tree

10 files changed

+172
-23
lines changed

10 files changed

+172
-23
lines changed

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2021 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -25,6 +25,7 @@
2525
import org.springframework.boot.autoconfigure.condition.ConditionalOnJndi;
2626
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
2727
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
28+
import org.springframework.boot.jms.ConnectionFactoryUnwrapper;
2829
import org.springframework.context.annotation.Bean;
2930
import org.springframework.context.annotation.Configuration;
3031
import org.springframework.jms.annotation.EnableJms;
@@ -89,7 +90,7 @@ DefaultJmsListenerContainerFactoryConfigurer jmsListenerContainerFactoryConfigur
8990
DefaultJmsListenerContainerFactory jmsListenerContainerFactory(
9091
DefaultJmsListenerContainerFactoryConfigurer configurer, ConnectionFactory connectionFactory) {
9192
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
92-
configurer.configure(factory, connectionFactory);
93+
configurer.configure(factory, ConnectionFactoryUnwrapper.unwrap(connectionFactory));
9394
return factory;
9495
}
9596

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ void testDefaultJmsListenerConfiguration() {
137137
DefaultMessageListenerContainer container = containerFactory.createListenerContainer(jmsListenerEndpoint);
138138
assertThat(container.getClientId()).isNull();
139139
assertThat(container.getConcurrentConsumers()).isEqualTo(1);
140-
assertThat(container.getConnectionFactory()).isSameAs(connectionFactory);
140+
assertThat(container.getConnectionFactory()).isSameAs(connectionFactory.getTargetConnectionFactory());
141141
assertThat(container.getMaxConcurrentConsumers()).isEqualTo(1);
142142
assertThat(container.getSessionAcknowledgeMode()).isEqualTo(Session.AUTO_ACKNOWLEDGE);
143143
assertThat(container.isAutoStartup()).isTrue();

spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/jms.adoc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,10 @@ When the JMS infrastructure is present, any bean can be annotated with `@JmsList
144144
If no `JmsListenerContainerFactory` has been defined, a default one is configured automatically.
145145
If a `DestinationResolver`, a `MessageConverter`, or a `jakarta.jms.ExceptionListener` beans are defined, they are associated automatically with the default factory.
146146

147+
In most scenarios, message listener containers should be configured against the native `ConnectionFactory`.
148+
This way each listener container has its own connection and this gives full responsibility to it in terms of local recovery.
149+
The auto-configuration uses `ConnectionFactoryUnwrapper` to unwrap the native connection factory from the auto-configured one.
150+
147151
By default, the default factory is transactional.
148152
If you run in an infrastructure where a `JtaTransactionManager` is present, it is associated to the listener container by default.
149153
If not, the `sessionTransacted` flag is enabled.
@@ -163,6 +167,8 @@ For instance, the following example exposes another factory that uses a specific
163167

164168
include-code::custom/MyJmsConfiguration[]
165169

170+
NOTE: In the example above, the customization uses `ConnectionFactoryUnwrapper` to associate the native connection factory to the message listener container the same way the auto-configured factory does.
171+
166172
Then you can use the factory in any `@JmsListener`-annotated method as follows:
167173

168174
include-code::custom/MyBean[]

spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/messaging/disabletransactedjmssession/MyJmsConfiguration.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2021 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@
1919
import jakarta.jms.ConnectionFactory;
2020

2121
import org.springframework.boot.autoconfigure.jms.DefaultJmsListenerContainerFactoryConfigurer;
22+
import org.springframework.boot.jms.ConnectionFactoryUnwrapper;
2223
import org.springframework.context.annotation.Bean;
2324
import org.springframework.context.annotation.Configuration;
2425
import org.springframework.jms.config.DefaultJmsListenerContainerFactory;
@@ -30,7 +31,7 @@ public class MyJmsConfiguration {
3031
public DefaultJmsListenerContainerFactory jmsListenerContainerFactory(ConnectionFactory connectionFactory,
3132
DefaultJmsListenerContainerFactoryConfigurer configurer) {
3233
DefaultJmsListenerContainerFactory listenerFactory = new DefaultJmsListenerContainerFactory();
33-
configurer.configure(listenerFactory, connectionFactory);
34+
configurer.configure(listenerFactory, ConnectionFactoryUnwrapper.unwrap(connectionFactory));
3435
listenerFactory.setTransactionManager(null);
3536
listenerFactory.setSessionTransacted(false);
3637
return listenerFactory;

spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/jms/receiving/custom/MyJmsConfiguration.java

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2021 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@
1919
import jakarta.jms.ConnectionFactory;
2020

2121
import org.springframework.boot.autoconfigure.jms.DefaultJmsListenerContainerFactoryConfigurer;
22+
import org.springframework.boot.jms.ConnectionFactoryUnwrapper;
2223
import org.springframework.context.annotation.Bean;
2324
import org.springframework.context.annotation.Configuration;
2425
import org.springframework.jms.config.DefaultJmsListenerContainerFactory;
@@ -27,16 +28,12 @@
2728
public class MyJmsConfiguration {
2829

2930
@Bean
30-
public DefaultJmsListenerContainerFactory myFactory(DefaultJmsListenerContainerFactoryConfigurer configurer) {
31+
public DefaultJmsListenerContainerFactory myFactory(DefaultJmsListenerContainerFactoryConfigurer configurer,
32+
ConnectionFactory connectionFactory) {
3133
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
32-
ConnectionFactory connectionFactory = getCustomConnectionFactory();
33-
configurer.configure(factory, connectionFactory);
34+
configurer.configure(factory, ConnectionFactoryUnwrapper.unwrap(connectionFactory));
3435
factory.setMessageConverter(new MyMessageConverter());
3536
return factory;
3637
}
3738

38-
private ConnectionFactory getCustomConnectionFactory() {
39-
return /**/ null;
40-
}
41-
4239
}

spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/howto/messaging/disabletransactedjmssession/MyJmsConfiguration.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2022 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
1717
package org.springframework.boot.docs.howto.messaging.disabletransactedjmssession
1818

1919
import jakarta.jms.ConnectionFactory
20+
import org.springframework.boot.jms.ConnectionFactoryUnwrapper
2021
import org.springframework.boot.autoconfigure.jms.DefaultJmsListenerContainerFactoryConfigurer
2122
import org.springframework.context.annotation.Bean
2223
import org.springframework.context.annotation.Configuration
@@ -30,7 +31,7 @@ class MyJmsConfiguration {
3031
fun jmsListenerContainerFactory(connectionFactory: ConnectionFactory?,
3132
configurer: DefaultJmsListenerContainerFactoryConfigurer): DefaultJmsListenerContainerFactory {
3233
val listenerFactory = DefaultJmsListenerContainerFactory()
33-
configurer.configure(listenerFactory, connectionFactory)
34+
configurer.configure(listenerFactory, ConnectionFactoryUnwrapper.unwrap(connectionFactory))
3435
listenerFactory.setTransactionManager(null)
3536
listenerFactory.setSessionTransacted(false)
3637
return listenerFactory

spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/jms/receiving/custom/MyJmsConfiguration.kt

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2022 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@ package org.springframework.boot.docs.messaging.jms.receiving.custom
1818

1919
import jakarta.jms.ConnectionFactory
2020
import org.springframework.boot.autoconfigure.jms.DefaultJmsListenerContainerFactoryConfigurer
21+
import org.springframework.boot.jms.ConnectionFactoryUnwrapper
2122
import org.springframework.context.annotation.Bean
2223
import org.springframework.context.annotation.Configuration
2324
import org.springframework.jms.config.DefaultJmsListenerContainerFactory
@@ -26,16 +27,12 @@ import org.springframework.jms.config.DefaultJmsListenerContainerFactory
2627
class MyJmsConfiguration {
2728

2829
@Bean
29-
fun myFactory(configurer: DefaultJmsListenerContainerFactoryConfigurer): DefaultJmsListenerContainerFactory {
30+
fun myFactory(configurer: DefaultJmsListenerContainerFactoryConfigurer,
31+
connectionFactory: ConnectionFactory): DefaultJmsListenerContainerFactory {
3032
val factory = DefaultJmsListenerContainerFactory()
31-
val connectionFactory = getCustomConnectionFactory()
32-
configurer.configure(factory, connectionFactory)
33+
configurer.configure(factory, ConnectionFactoryUnwrapper.unwrap(connectionFactory))
3334
factory.setMessageConverter(MyMessageConverter())
3435
return factory
3536
}
3637

37-
fun getCustomConnectionFactory() : ConnectionFactory? {
38-
return /**/ null
39-
}
40-
4138
}

spring-boot-project/spring-boot/build.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,15 @@ dependencies {
7373
optional("org.liquibase:liquibase-core") {
7474
exclude(group: "javax.xml.bind", module: "jaxb-api")
7575
}
76+
optional("org.messaginghub:pooled-jms") {
77+
exclude group: "org.apache.geronimo.specs", module: "geronimo-jms_2.0_spec"
78+
}
7679
optional("org.postgresql:postgresql")
7780
optional("org.slf4j:jul-to-slf4j")
7881
optional("org.slf4j:slf4j-api")
7982
optional("org.springframework:spring-messaging")
8083
optional("org.springframework:spring-orm")
84+
optional("org.springframework:spring-jms")
8185
optional("org.springframework:spring-oxm")
8286
optional("org.springframework:spring-r2dbc")
8387
optional("org.springframework:spring-test")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright 2012-2024 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.jms;
18+
19+
import jakarta.jms.ConnectionFactory;
20+
import org.messaginghub.pooled.jms.JmsPoolConnectionFactory;
21+
22+
import org.springframework.jms.connection.CachingConnectionFactory;
23+
24+
/**
25+
* Unwrap a {@link ConnectionFactory} that may have been wrapped to perform caching or
26+
* pooling.
27+
*
28+
* @author Stephane Nicoll
29+
* @since 6.4.0
30+
*/
31+
public abstract class ConnectionFactoryUnwrapper {
32+
33+
/**
34+
* Return the native {@link ConnectionFactory} by unwrapping it from a cache or pool
35+
* connection factory. Return the given {@link ConnectionFactory} if no caching
36+
* wrapper has been detected.
37+
* @param connectionFactory a connection factory
38+
* @return the native connection factory that it wraps, if any
39+
*/
40+
public static ConnectionFactory unwrap(ConnectionFactory connectionFactory) {
41+
if (connectionFactory instanceof CachingConnectionFactory ccf) {
42+
return unwrap(ccf.getTargetConnectionFactory());
43+
}
44+
ConnectionFactory unwrapedConnectionFactory = unwrapFromJmsPoolConnectionFactory(connectionFactory);
45+
return (unwrapedConnectionFactory != null) ? unwrap(unwrapedConnectionFactory) : connectionFactory;
46+
}
47+
48+
private static ConnectionFactory unwrapFromJmsPoolConnectionFactory(ConnectionFactory connectionFactory) {
49+
try {
50+
if (connectionFactory instanceof JmsPoolConnectionFactory poolConnectionFactory) {
51+
return (ConnectionFactory) poolConnectionFactory.getConnectionFactory();
52+
}
53+
}
54+
catch (Throwable ex) {
55+
// ignore
56+
}
57+
return null;
58+
}
59+
60+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright 2012-2024 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.jms;
18+
19+
import jakarta.jms.ConnectionFactory;
20+
import org.junit.jupiter.api.Test;
21+
import org.messaginghub.pooled.jms.JmsPoolConnectionFactory;
22+
23+
import org.springframework.jms.connection.CachingConnectionFactory;
24+
import org.springframework.jms.connection.SingleConnectionFactory;
25+
26+
import static org.assertj.core.api.Assertions.assertThat;
27+
import static org.mockito.Mockito.mock;
28+
29+
/**
30+
* Tests for {@link ConnectionFactoryUnwrapper}.
31+
*
32+
* @author Stephane Nicoll
33+
*/
34+
class ConnectionFactoryUnwrapperTests {
35+
36+
@Test
37+
void unwrapWithSingleConnectionFactory() {
38+
ConnectionFactory connectionFactory = new SingleConnectionFactory();
39+
assertThat(ConnectionFactoryUnwrapper.unwrap(connectionFactory)).isSameAs(connectionFactory);
40+
}
41+
42+
@Test
43+
void unwrapWithConnectionFactory() {
44+
ConnectionFactory connectionFactory = mock(ConnectionFactory.class);
45+
assertThat(ConnectionFactoryUnwrapper.unwrap(connectionFactory)).isSameAs(connectionFactory);
46+
}
47+
48+
@Test
49+
void unwrapWithCachingConnectionFactory() {
50+
ConnectionFactory connectionFactory = mock(ConnectionFactory.class);
51+
assertThat(ConnectionFactoryUnwrapper.unwrap(new CachingConnectionFactory(connectionFactory)))
52+
.isSameAs(connectionFactory);
53+
}
54+
55+
@Test
56+
void unwrapWithNestedCachingConnectionFactories() {
57+
ConnectionFactory connectionFactory = mock(ConnectionFactory.class);
58+
CachingConnectionFactory firstCachingConnectionFactory = new CachingConnectionFactory(connectionFactory);
59+
CachingConnectionFactory secondCachingConnectionFactory = new CachingConnectionFactory(
60+
firstCachingConnectionFactory);
61+
assertThat(ConnectionFactoryUnwrapper.unwrap(secondCachingConnectionFactory)).isSameAs(connectionFactory);
62+
}
63+
64+
@Test
65+
void unwrapWithJmsPoolConnectionFactory() {
66+
ConnectionFactory connectionFactory = mock(ConnectionFactory.class);
67+
JmsPoolConnectionFactory poolConnectionFactory = new JmsPoolConnectionFactory();
68+
poolConnectionFactory.setConnectionFactory(connectionFactory);
69+
assertThat(ConnectionFactoryUnwrapper.unwrap(poolConnectionFactory)).isSameAs(connectionFactory);
70+
}
71+
72+
@Test
73+
void unwrapWithNestedJmsPoolConnectionFactories() {
74+
ConnectionFactory connectionFactory = mock(ConnectionFactory.class);
75+
JmsPoolConnectionFactory firstPooledConnectionFactory = new JmsPoolConnectionFactory();
76+
firstPooledConnectionFactory.setConnectionFactory(connectionFactory);
77+
JmsPoolConnectionFactory secondPooledConnectionFactory = new JmsPoolConnectionFactory();
78+
secondPooledConnectionFactory.setConnectionFactory(firstPooledConnectionFactory);
79+
assertThat(ConnectionFactoryUnwrapper.unwrap(secondPooledConnectionFactory)).isSameAs(connectionFactory);
80+
}
81+
82+
}

0 commit comments

Comments
 (0)