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