Skip to content

Commit

Permalink
Fix issue where application fails to start with multiple TransactionM…
Browse files Browse the repository at this point in the history
…anager 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
  • Loading branch information
marcusdacoregio committed Feb 23, 2024
1 parent e3be498 commit 4920499
Show file tree
Hide file tree
Showing 3 changed files with 167 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -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 {

}
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -24,10 +24,14 @@

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;
Expand All @@ -54,6 +58,7 @@
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;
Expand All @@ -77,7 +82,8 @@
*/
@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;

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -195,7 +216,8 @@ public void setDataSource(@SpringSessionDataSource ObjectProvider<DataSource> sp
this.dataSource = dataSourceToUse;
}

@Autowired
@Autowired(required = false)
@SpringSessionTransactionManager
public void setTransactionManager(PlatformTransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
Expand Down Expand Up @@ -266,14 +288,23 @@ public void setImportMetadata(AnnotationMetadata importMetadata) {
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));
jdbcTemplate.afterPropertiesSet();
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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -46,13 +46,19 @@
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;

/**
Expand Down Expand Up @@ -319,6 +325,34 @@ void importConfigAndCustomize() {
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();
Expand Down Expand Up @@ -576,4 +610,59 @@ SessionRepositoryCustomizer<JdbcIndexedSessionRepository> sessionRepositoryCusto

}

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

}

}

}

0 comments on commit 4920499

Please sign in to comment.