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 11c82745..7cb5ab7f 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; @@ -56,6 +60,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; @@ -79,7 +84,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; @@ -113,6 +119,21 @@ public class JdbcHttpSessionConfiguration implements BeanClassLoaderAware, Embed private SessionIdGenerator sessionIdGenerator = UuidSessionIdGenerator.getInstance(); + 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); @@ -200,7 +221,8 @@ public class JdbcHttpSessionConfiguration implements BeanClassLoaderAware, Embed this.dataSource = dataSourceToUse; } - @Autowired + @Autowired(required = false) + @SpringSessionTransactionManager public void setTransactionManager(PlatformTransactionManager transactionManager) { this.transactionManager = transactionManager; } @@ -276,6 +298,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)); @@ -283,7 +314,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 7dc57f73..29081c76 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-2023 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. @@ -49,13 +49,19 @@ import org.springframework.session.config.SessionRepositoryCustomizer; import org.springframework.session.jdbc.FixedSessionIdGenerator; 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; /** @@ -338,6 +344,34 @@ class JdbcHttpSessionConfigurationTests { assertThat(sessionIdGenerator).isInstanceOf(UuidSessionIdGenerator.class); } + // 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(); @@ -606,4 +640,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 { + + } + + } + }