From 4a7a2258f9ad8771e9a8225f5a18db156372162a Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Fri, 22 Jan 2021 18:22:00 +0100 Subject: [PATCH] Unwrap proxied DataSources in SqlScriptsTestExecutionListener Prior to this commit, if the DataSource in the DataSourceFromTransactionManager was wrapped in a proxy implementing InfrastructureProxy, SqlScriptsTestExecutionListener would throw an exception stating that the DataSource in the ApplicationContext and the DataSource in the DataSourceFromTransactionManager were not the same. This commit unwraps both data sources and compares the underlying target instances to check for equality. In addition, this commit makes the unwrapResourceIfNecessary() method in TransactionSynchronizationUtils public. Closes gh-26422 --- .../jdbc/SqlScriptsTestExecutionListener.java | 14 +- ...tureProxyTransactionalSqlScriptsTests.java | 128 ++++++++++++++++++ .../TransactionSynchronizationUtils.java | 6 +- 3 files changed, 144 insertions(+), 4 deletions(-) create mode 100644 spring-test/src/test/java/org/springframework/test/context/jdbc/InfrastructureProxyTransactionalSqlScriptsTests.java diff --git a/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java index 845b67bbe5..6f1ca25e97 100644 --- a/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java @@ -46,6 +46,7 @@ import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.interceptor.DefaultTransactionAttribute; import org.springframework.transaction.interceptor.TransactionAttribute; +import org.springframework.transaction.support.TransactionSynchronizationUtils; import org.springframework.transaction.support.TransactionTemplate; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -258,7 +259,7 @@ public class SqlScriptsTestExecutionListener extends AbstractTestExecutionListen else { DataSource dataSourceFromTxMgr = getDataSourceFromTransactionManager(txMgr); // Ensure user configured an appropriate DataSource/TransactionManager pair. - if (dataSource != null && dataSourceFromTxMgr != null && !dataSource.equals(dataSourceFromTxMgr)) { + if (dataSource != null && dataSourceFromTxMgr != null && !sameDataSource(dataSource, dataSourceFromTxMgr)) { throw new IllegalStateException(String.format("Failed to execute SQL scripts for test context %s: " + "the configured DataSource [%s] (named '%s') is not the one associated with " + "transaction manager [%s] (named '%s').", testContext, dataSource.getClass().getName(), @@ -292,6 +293,17 @@ public class SqlScriptsTestExecutionListener extends AbstractTestExecutionListen return populator; } + /** + * Determine if the two data sources are effectively the same, unwrapping + * proxies as necessary to compare the target instances. + * @since 5.3.4 + * @see TransactionSynchronizationUtils#unwrapResourceIfNecessary(Object) + */ + private static boolean sameDataSource(DataSource ds1, DataSource ds2) { + return TransactionSynchronizationUtils.unwrapResourceIfNecessary(ds1) + .equals(TransactionSynchronizationUtils.unwrapResourceIfNecessary(ds2)); + } + @Nullable private DataSource getDataSourceFromTransactionManager(PlatformTransactionManager transactionManager) { try { diff --git a/spring-test/src/test/java/org/springframework/test/context/jdbc/InfrastructureProxyTransactionalSqlScriptsTests.java b/spring-test/src/test/java/org/springframework/test/context/jdbc/InfrastructureProxyTransactionalSqlScriptsTests.java new file mode 100644 index 0000000000..bd3d00732c --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/jdbc/InfrastructureProxyTransactionalSqlScriptsTests.java @@ -0,0 +1,128 @@ +/* + * Copyright 2002-2021 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.test.context.jdbc; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.InfrastructureProxy; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.transaction.PlatformTransactionManager; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Transactional integration tests for {@link Sql @Sql} support when the + * {@link DataSource} is wrapped in a proxy that implements + * {@link InfrastructureProxy}. + * + * @author Sam Brannen + * @since 5.3.4 + */ +@SpringJUnitConfig +@DirtiesContext +class InfrastructureProxyTransactionalSqlScriptsTests extends AbstractTransactionalTests { + + @BeforeEach + void preconditions(@Autowired DataSource dataSource, @Autowired DataSourceTransactionManager transactionManager) { + assertThat(dataSource).isNotEqualTo(transactionManager.getDataSource()); + assertThat(transactionManager.getDataSource()).isNotEqualTo(dataSource); + assertThat(transactionManager.getDataSource()).isInstanceOf(InfrastructureProxy.class); + } + + @Test + @Sql({ "schema.sql", "data.sql", "data-add-dogbert.sql" }) + void methodLevelScripts() { + assertNumUsers(2); + } + + + @Configuration + static class DatabaseConfig { + + @Bean + JdbcTemplate jdbcTemplate(DataSource dataSource) { + return new JdbcTemplate(dataSource); + } + + @Bean + PlatformTransactionManager transactionManager(DataSource dataSource) { + return new DataSourceTransactionManager(wrapDataSource(dataSource)); + } + + @Bean + DataSource dataSource() { + return new EmbeddedDatabaseBuilder()// + .setName("empty-sql-scripts-test-db")// + .build(); + } + + } + + + private static DataSource wrapDataSource(DataSource dataSource) { + return (DataSource) Proxy.newProxyInstance( + InfrastructureProxyTransactionalSqlScriptsTests.class.getClassLoader(), + new Class[] { DataSource.class, InfrastructureProxy.class }, + new DataSourceInvocationHandler(dataSource)); + } + + + private static class DataSourceInvocationHandler implements InvocationHandler { + + private final DataSource dataSource; + + + DataSourceInvocationHandler(DataSource dataSource) { + this.dataSource = dataSource; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + switch (method.getName()) { + case "equals": + return (proxy == args[0]); + case "hashCode": + return System.identityHashCode(proxy); + case "getWrappedObject": + return this.dataSource; + default: + try { + return method.invoke(this.dataSource, args); + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + } + } + } + +} diff --git a/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationUtils.java b/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationUtils.java index f9c99a8f33..c319922200 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationUtils.java +++ b/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 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. @@ -45,7 +45,7 @@ public abstract class TransactionSynchronizationUtils { /** - * Check whether the given resource transaction managers refers to the given + * Check whether the given resource transaction manager refers to the given * (underlying) resource factory. * @see ResourceTransactionManager#getResourceFactory() * @see org.springframework.core.InfrastructureProxy#getWrappedObject() @@ -59,7 +59,7 @@ public abstract class TransactionSynchronizationUtils { * the given handle as-is. * @see org.springframework.core.InfrastructureProxy#getWrappedObject() */ - static Object unwrapResourceIfNecessary(Object resource) { + public static Object unwrapResourceIfNecessary(Object resource) { Assert.notNull(resource, "Resource must not be null"); Object resourceRef = resource; // unwrap infrastructure proxy