From 4920499bd25226bc1abcea03e15a15be572c6d9f Mon Sep 17 00:00:00 2001 From: Marcus Hert Da Coregio Date: Fri, 23 Feb 2024 14:12:51 -0300 Subject: [PATCH] Fix issue where application fails to start with multiple TransactionManager beans Prior to this commit, if an application had multiple PlatformTransactionManager beans it would fail to start even when configuring a proper TransactionOperations qualified bean. This commit introduces the @SpringSessionTransactionManager bean qualifier to specify which bean should be injected into Spring Session JDBC configuration, if no bean specified, it tries to resolve a unique PlatformTransactionManager bean from the application context to maintain backward compatibility. Closes gh-2801 --- .../SpringSessionTransactionManager.java | 42 +++++++++ .../http/JdbcHttpSessionConfiguration.java | 39 +++++++- .../JdbcHttpSessionConfigurationTests.java | 91 ++++++++++++++++++- 3 files changed, 167 insertions(+), 5 deletions(-) create mode 100644 spring-session-jdbc/src/main/java/org/springframework/session/jdbc/config/annotation/SpringSessionTransactionManager.java diff --git a/spring-session-jdbc/src/main/java/org/springframework/session/jdbc/config/annotation/SpringSessionTransactionManager.java b/spring-session-jdbc/src/main/java/org/springframework/session/jdbc/config/annotation/SpringSessionTransactionManager.java new file mode 100644 index 00000000..ba592563 --- /dev/null +++ b/spring-session-jdbc/src/main/java/org/springframework/session/jdbc/config/annotation/SpringSessionTransactionManager.java @@ -0,0 +1,42 @@ +/* + * Copyright 2014-2024 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.session.jdbc.config.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.session.jdbc.JdbcIndexedSessionRepository; + +/** + * Qualifier annotation for a + * {@link org.springframework.transaction.PlatformTransactionManager} to be injected in + * {@link JdbcIndexedSessionRepository}. + * + * @author Marcus da Coregio + * @since 3.1.5 + */ +@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Qualifier +public @interface SpringSessionTransactionManager { + +} diff --git a/spring-session-jdbc/src/main/java/org/springframework/session/jdbc/config/annotation/web/http/JdbcHttpSessionConfiguration.java b/spring-session-jdbc/src/main/java/org/springframework/session/jdbc/config/annotation/web/http/JdbcHttpSessionConfiguration.java index 8a7951ca..b933b17d 100644 --- a/spring-session-jdbc/src/main/java/org/springframework/session/jdbc/config/annotation/web/http/JdbcHttpSessionConfiguration.java +++ b/spring-session-jdbc/src/main/java/org/springframework/session/jdbc/config/annotation/web/http/JdbcHttpSessionConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 the original author or authors. + * Copyright 2014-2024 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. @@ -24,10 +24,14 @@ import java.util.stream.Collectors; import javax.sql.DataSource; +import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; import org.springframework.context.EmbeddedValueResolverAware; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -54,6 +58,7 @@ import org.springframework.session.config.SessionRepositoryCustomizer; import org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration; import org.springframework.session.jdbc.JdbcIndexedSessionRepository; import org.springframework.session.jdbc.config.annotation.SpringSessionDataSource; +import org.springframework.session.jdbc.config.annotation.SpringSessionTransactionManager; import org.springframework.session.web.http.SessionRepositoryFilter; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionDefinition; @@ -77,7 +82,8 @@ import org.springframework.util.StringValueResolver; */ @Configuration(proxyBeanMethods = false) @Import(SpringHttpSessionConfiguration.class) -public class JdbcHttpSessionConfiguration implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware { +public class JdbcHttpSessionConfiguration implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware, + ApplicationContextAware, InitializingBean { private Duration maxInactiveInterval = MapSession.DEFAULT_MAX_INACTIVE_INTERVAL; @@ -109,6 +115,21 @@ public class JdbcHttpSessionConfiguration implements BeanClassLoaderAware, Embed private StringValueResolver embeddedValueResolver; + private ApplicationContext applicationContext; + + @Override + public void afterPropertiesSet() throws Exception { + if (this.transactionOperations == null && this.transactionManager == null) { + this.transactionManager = getUniqueTransactionManager(); + if (this.transactionManager == null) { + throw new IllegalStateException( + """ + Could not resolve an unique PlatformTransactionManager bean from the application context. + Please provide either a TransactionOperations bean named springSessionTransactionOperations or a PlatformTransactionManager bean qualified with @SpringSessionTransactionManager"""); + } + } + } + @Bean public JdbcIndexedSessionRepository sessionRepository() { JdbcTemplate jdbcTemplate = createJdbcTemplate(this.dataSource); @@ -195,7 +216,8 @@ public class JdbcHttpSessionConfiguration implements BeanClassLoaderAware, Embed this.dataSource = dataSourceToUse; } - @Autowired + @Autowired(required = false) + @SpringSessionTransactionManager public void setTransactionManager(PlatformTransactionManager transactionManager) { this.transactionManager = transactionManager; } @@ -266,6 +288,15 @@ public class JdbcHttpSessionConfiguration implements BeanClassLoaderAware, Embed this.saveMode = attributes.getEnum("saveMode"); } + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + private PlatformTransactionManager getUniqueTransactionManager() { + return this.applicationContext.getBeanProvider(PlatformTransactionManager.class).getIfUnique(); + } + private static JdbcTemplate createJdbcTemplate(DataSource dataSource) { JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); jdbcTemplate.setExceptionTranslator(new SQLErrorCodeSQLExceptionTranslator(dataSource)); @@ -273,7 +304,7 @@ public class JdbcHttpSessionConfiguration implements BeanClassLoaderAware, Embed return jdbcTemplate; } - private static TransactionTemplate createTransactionTemplate(PlatformTransactionManager transactionManager) { + private TransactionTemplate createTransactionTemplate(PlatformTransactionManager transactionManager) { TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager); transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); transactionTemplate.afterPropertiesSet(); diff --git a/spring-session-jdbc/src/test/java/org/springframework/session/jdbc/config/annotation/web/http/JdbcHttpSessionConfigurationTests.java b/spring-session-jdbc/src/test/java/org/springframework/session/jdbc/config/annotation/web/http/JdbcHttpSessionConfigurationTests.java index b12787db..35d2083b 100644 --- a/spring-session-jdbc/src/test/java/org/springframework/session/jdbc/config/annotation/web/http/JdbcHttpSessionConfigurationTests.java +++ b/spring-session-jdbc/src/test/java/org/springframework/session/jdbc/config/annotation/web/http/JdbcHttpSessionConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 the original author or authors. + * Copyright 2014-2024 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. @@ -46,13 +46,19 @@ import org.springframework.session.Session; import org.springframework.session.config.SessionRepositoryCustomizer; import org.springframework.session.jdbc.JdbcIndexedSessionRepository; import org.springframework.session.jdbc.config.annotation.SpringSessionDataSource; +import org.springframework.session.jdbc.config.annotation.SpringSessionTransactionManager; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.TransactionManager; +import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionOperations; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatException; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatNoException; import static org.mockito.Mockito.mock; /** @@ -319,6 +325,34 @@ class JdbcHttpSessionConfigurationTests { assertThat(sessionRepository).extracting("defaultMaxInactiveInterval").isEqualTo(Duration.ZERO); } + // gh-2801 + @Test + void configureWhenMultipleTransactionManagersAndQualifiedTransactionOperationsThenApplicationShouldStart() { + assertThatNoException().isThrownBy(() -> registerAndRefresh(MultipleTransactionManagerConfig.class, + CustomTransactionOperationsConfiguration.class)); + } + + // gh-2801 + @Test + void configureWhenMultipleTransactionManagersAndQualifiedTransactionManagerThenApplicationShouldStartAndUseQualified() { + assertThatNoException().isThrownBy(() -> registerAndRefresh(MultipleTransactionManagerConfig.class, + QualifiedTransactionManagerConfig.class, DefaultConfiguration.class)); + JdbcHttpSessionConfiguration configuration = this.context.getBean(JdbcHttpSessionConfiguration.class); + Object transactionManager = ReflectionTestUtils.getField(configuration, "transactionManager"); + assertThat(transactionManager).isInstanceOf(MyTransactionManager.class); + } + + // gh-2801 + @Test + void configureWhenMultipleTransactionManagersAndNoQualifiedBeanThenApplicationShouldFailToStart() { + assertThatException() + .isThrownBy(() -> registerAndRefresh(MultipleTransactionManagerConfig.class, DefaultConfiguration.class)) + .havingRootCause() + .isInstanceOf(IllegalStateException.class) + .withMessage("Could not resolve an unique PlatformTransactionManager bean from the application context.\n" + + "Please provide either a TransactionOperations bean named springSessionTransactionOperations or a PlatformTransactionManager bean qualified with @SpringSessionTransactionManager"); + } + private void registerAndRefresh(Class... annotatedClasses) { this.context.register(annotatedClasses); this.context.refresh(); @@ -576,4 +610,59 @@ class JdbcHttpSessionConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class MultipleTransactionManagerConfig { + + @Bean + DataSource dataSource() { + return mock(DataSource.class); + } + + @Bean + TransactionManager transactionManager1() { + return new MyTransactionManager(); + } + + @Bean + TransactionManager transactionManager2() { + return mock(PlatformTransactionManager.class); + } + + } + + @Configuration(proxyBeanMethods = false) + static class QualifiedTransactionManagerConfig { + + @Bean + DataSource dataSource() { + return mock(DataSource.class); + } + + @Bean + @SpringSessionTransactionManager + TransactionManager myTransactionManager() { + return new MyTransactionManager(); + } + + } + + static class MyTransactionManager implements PlatformTransactionManager { + + @Override + public TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException { + return null; + } + + @Override + public void commit(TransactionStatus status) throws TransactionException { + + } + + @Override + public void rollback(TransactionStatus status) throws TransactionException { + + } + + } + }