diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/LazyConnectionDataSourceProxy.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/LazyConnectionDataSourceProxy.java index 8a9241c10e12..34c751d08c6a 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/LazyConnectionDataSourceProxy.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/LazyConnectionDataSourceProxy.java @@ -22,7 +22,9 @@ import java.lang.reflect.Proxy; import java.sql.Connection; import java.sql.SQLException; +import java.util.LinkedHashMap; import java.util.Map; +import java.util.concurrent.Executor; import javax.sql.DataSource; @@ -35,12 +37,36 @@ /** * Proxy for a target DataSource, fetching actual JDBC Connections lazily, * i.e. not until first creation of a Statement. Connection initialization - * properties like auto-commit mode, transaction isolation and read-only mode - * will be kept and applied to the actual JDBC Connection as soon as an actual - * Connection is fetched (if ever). Consequently, commit and rollback calls will - * be ignored if no Statements have been created. As of 6.1.2, there is also - * special support for a {@link #setReadOnlyDataSource read-only DataSource} to use - * during a read-only transaction, in addition to the regular target DataSource. + * properties like auto-commit mode, transaction isolation, read-only mode, + * catalog, schema, holdability, client info and network timeout will be kept + * and applied to the actual JDBC Connection as soon as an actual Connection + * is fetched (if ever). Consequently, commit and rollback calls will be ignored + * if no Statements have been created. + * + *

Once a property has been set, the corresponding getter method returns the + * set value until the actual Connection is fetched. If the property has not been + * set, invoking the getter triggers a fetch of the actual connection in order to + * obtain the default value. + * + *

Although client info is listed among the deferred properties above, + * the following methods are exceptions to the lazy acquisition behavior and + * force immediate acquisition of the underlying Connection. + * The {@link java.sql.Connection#getClientInfo()} and + * {@link java.sql.Connection#getClientInfo(java.lang.String)} + * methods are read operations whose values cannot be reliably cached due to + * driver defaults, remnants from pooled connections, or external session + * modifications. + * + *

The {@link java.sql.Connection#setClientInfo(java.util.Properties)} + * method also forces immediate acquisition. JDBC driver implementations are + * inconsistent: some treat it as an overwrite, while others treat it as an + * append/merge. To guarantee behavior identical to that of a non-lazy DataSource + * across all drivers, the proxy does not cache or replay it, thereby avoiding any + * risk of semantic mismatch. + * + *

As of 6.1.2, there is also special support for a + * {@link #setReadOnlyDataSource read-only DataSource} to use during a + * read-only transaction, in addition to the regular target DataSource. * *

This DataSource proxy allows to avoid fetching JDBC Connections from * a pool unless actually necessary. JDBC transaction control can happen @@ -86,6 +112,7 @@ * * @author Juergen Hoeller * @author Sam Brannen + * @author Chengang Guan * @since 1.1.4 * @see DataSourceTransactionManager * @see #setTargetDataSource @@ -311,6 +338,12 @@ private class LazyConnectionInvocationHandler implements InvocationHandler { private @Nullable Boolean autoCommit; + private @Nullable Executor networkTimeoutExecutor; + + private @Nullable Integer networkTimeout; + + private @Nullable Map clientInfo; + private boolean closed = false; private @Nullable Connection target; @@ -434,6 +467,28 @@ public LazyConnectionInvocationHandler(String username, String password) { // Ignore: no warnings to expose yet. return null; } + case "setNetworkTimeout" -> { + this.networkTimeoutExecutor = (Executor) args[0]; + this.networkTimeout = (Integer) args[1]; + return null; + } + case "getNetworkTimeout" -> { + if (this.networkTimeout != null) { + return this.networkTimeout; + } + // Else fetch actual Connection and check there. + } + case "setClientInfo" -> { + if (args.length == 2) { + if (this.clientInfo == null) { + this.clientInfo = new LinkedHashMap<>(); + } + this.clientInfo.put((String) args[0], (String) args[1]); + return null; + } + // Else fetch actual Connection and check there. + // setClientInfo(Properties) will fall-through + } case "close" -> { // Ignore: no target connection yet. this.closed = true; @@ -530,6 +585,14 @@ private Connection getTargetConnection(Method operation) throws Throwable { if (this.autoCommit != null && this.autoCommit != defaultAutoCommit()) { target.setAutoCommit(this.autoCommit); } + if (this.networkTimeout != null) { + target.setNetworkTimeout(this.networkTimeoutExecutor, this.networkTimeout); + } + if (this.clientInfo != null) { + for (Map.Entry entry: this.clientInfo.entrySet()) { + target.setClientInfo(entry.getKey(), entry.getValue()); + } + } } catch (Throwable settingsEx) { logger.debug("Failed to apply transaction settings to JDBC Connection", settingsEx); diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/LazyConnectionDataSourceProxyTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/LazyConnectionDataSourceProxyTests.java index 943a8075aece..47ac4d4bd561 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/LazyConnectionDataSourceProxyTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/LazyConnectionDataSourceProxyTests.java @@ -18,11 +18,17 @@ import java.lang.reflect.Field; import java.sql.Connection; +import java.sql.SQLException; import java.util.Arrays; import java.util.HashSet; +import java.util.Properties; import java.util.Set; +import java.util.concurrent.Executor; import java.util.stream.Stream; +import javax.sql.DataSource; + +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.util.ReflectionUtils; @@ -32,19 +38,37 @@ import static java.sql.Connection.TRANSACTION_READ_UNCOMMITTED; import static java.sql.Connection.TRANSACTION_REPEATABLE_READ; import static java.sql.Connection.TRANSACTION_SERIALIZABLE; +import static java.sql.ResultSet.CLOSE_CURSORS_AT_COMMIT; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; /** * Tests for {@link LazyConnectionDataSourceProxy}. * * @author Sam Brannen + * @author Chengang Guan * @since 6.1 */ class LazyConnectionDataSourceProxyTests { private final LazyConnectionDataSourceProxy proxy = new LazyConnectionDataSourceProxy(); + private final LazyConnectionDataSourceProxy lazyProxy = new LazyConnectionDataSourceProxy(); + + + @BeforeEach + void setup() { + lazyProxy.setDefaultAutoCommit(false); + lazyProxy.setDefaultTransactionIsolation(TRANSACTION_READ_UNCOMMITTED); + } @Test void setDefaultTransactionIsolationNameToUnsupportedValues() { @@ -94,6 +118,157 @@ void setDefaultTransactionIsolation() { assertThat(proxy.defaultTransactionIsolation()).isEqualTo(TRANSACTION_SERIALIZABLE); } + @Test + void lazyHandlingCatalog() throws SQLException { + DataSource mockDataSource = mock(); + Connection physicalConnection1 = mock(); + Connection physicalConnection2 = mock(); + when(mockDataSource.getConnection()).thenReturn(physicalConnection1).thenReturn(physicalConnection2); + lazyProxy.setTargetDataSource(mockDataSource); + + Connection lazyConnection1 = lazyProxy.getConnection(); + lazyConnection1.setCatalog("catalogName"); + assertThat(lazyConnection1.getCatalog()).isEqualTo("catalogName"); + verify(physicalConnection1, never()).setCatalog("catalogName"); + verify(physicalConnection1, never()).getCatalog(); + establishPhysicalConnection(lazyConnection1); + verify(physicalConnection1).setCatalog("catalogName"); + + Connection lazyConnection2 = lazyProxy.getConnection(); + lazyConnection2.getCatalog(); // establish physical connection immediately + verify(physicalConnection2).getCatalog(); + } + + @Test + void lazyHandlingSchema() throws SQLException { + DataSource mockDataSource = mock(); + Connection physicalConnection1 = mock(); + Connection physicalConnection2 = mock(); + when(mockDataSource.getConnection()).thenReturn(physicalConnection1).thenReturn(physicalConnection2); + lazyProxy.setTargetDataSource(mockDataSource); + + Connection lazyConnection1 = lazyProxy.getConnection(); + lazyConnection1.setSchema("schemaName"); + assertThat(lazyConnection1.getSchema()).isEqualTo("schemaName"); + verify(physicalConnection1, never()).setSchema("schemaName"); + verify(physicalConnection1, never()).getSchema(); + establishPhysicalConnection(lazyConnection1); + verify(physicalConnection1).setSchema("schemaName"); + + Connection lazyConnection2 = lazyProxy.getConnection(); + lazyConnection2.getSchema(); // establish physical connection immediately + verify(physicalConnection2).getSchema(); + } + + @Test + void lazyHandlingHoldability() throws SQLException { + DataSource mockDataSource = mock(); + Connection physicalConnection1 = mock(); + Connection physicalConnection2 = mock(); + when(mockDataSource.getConnection()).thenReturn(physicalConnection1).thenReturn(physicalConnection2); + lazyProxy.setTargetDataSource(mockDataSource); + + Connection lazyConnection1 = lazyProxy.getConnection(); + lazyConnection1.setHoldability(CLOSE_CURSORS_AT_COMMIT); + assertThat(lazyConnection1.getHoldability()).isEqualTo(CLOSE_CURSORS_AT_COMMIT); + verify(physicalConnection1, never()).setHoldability(CLOSE_CURSORS_AT_COMMIT); + verify(physicalConnection1, never()).getHoldability(); + establishPhysicalConnection(lazyConnection1); + verify(physicalConnection1).setHoldability(CLOSE_CURSORS_AT_COMMIT); + + Connection lazyConnection2 = lazyProxy.getConnection(); + lazyConnection2.getHoldability(); // establish physical connection immediately + verify(physicalConnection2).getHoldability(); + } + + @Test + void lazyHandlingTransactionIsolation() throws SQLException { + DataSource mockDataSource = mock(); + Connection physicalConnection = mock(); + when(mockDataSource.getConnection()).thenReturn(physicalConnection); + lazyProxy.setTargetDataSource(mockDataSource); + + Connection lazyConnection = lazyProxy.getConnection(); + lazyConnection.setTransactionIsolation(TRANSACTION_READ_COMMITTED); + assertThat(lazyConnection.getTransactionIsolation()).isEqualTo(TRANSACTION_READ_COMMITTED); + verify(physicalConnection, never()).setTransactionIsolation(TRANSACTION_READ_COMMITTED); + verify(physicalConnection, never()).getTransactionIsolation(); + establishPhysicalConnection(lazyConnection); + verify(physicalConnection).setTransactionIsolation(TRANSACTION_READ_COMMITTED); + } + + @Test + void lazyHandlingAutoCommit() throws SQLException { + DataSource mockDataSource = mock(); + Connection physicalConnection = mock(); + when(mockDataSource.getConnection()).thenReturn(physicalConnection); + lazyProxy.setTargetDataSource(mockDataSource); + + Connection lazyConnection = lazyProxy.getConnection(); + lazyConnection.setAutoCommit(true); + assertThat(lazyConnection.getAutoCommit()).isTrue(); + verify(physicalConnection, never()).setAutoCommit(true); + verify(physicalConnection, never()).getAutoCommit(); + establishPhysicalConnection(lazyConnection); + verify(physicalConnection).setAutoCommit(true); + } + + @Test + void lazyHandlingNetworkTimeoutExecutor() throws SQLException { + DataSource mockDataSource = mock(); + Connection physicalConnection1 = mock(); + Connection physicalConnection2 = mock(); + Executor executor = mock(); + when(mockDataSource.getConnection()).thenReturn(physicalConnection1).thenReturn(physicalConnection2); + doThrow(SQLException.class).when(physicalConnection2).setNetworkTimeout(eq(null), anyInt()); + lazyProxy.setTargetDataSource(mockDataSource); + + Connection lazyConnection1 = lazyProxy.getConnection(); + lazyConnection1.setNetworkTimeout(executor, 1000); + assertThat(lazyConnection1.getNetworkTimeout()).isEqualTo(1000); + verify(physicalConnection1, never()).setNetworkTimeout(executor, 1000); + verify(physicalConnection1, never()).getNetworkTimeout(); + establishPhysicalConnection(lazyConnection1); + verify(physicalConnection1).setNetworkTimeout(executor, 1000); + + // null executor + Connection lazyConnection2 = lazyProxy.getConnection(); + lazyConnection2.setNetworkTimeout(null, 1000); + assertThatThrownBy(() -> establishPhysicalConnection(lazyConnection2)).isInstanceOf(SQLException.class); + } + + @Test + void lazyHandlingClientInfoForKV() throws SQLException { + DataSource mockDataSource = mock(); + Connection physicalConnection = mock(); + when(mockDataSource.getConnection()).thenReturn(physicalConnection); + lazyProxy.setTargetDataSource(mockDataSource); + + Connection lazyConnection = lazyProxy.getConnection(); + lazyConnection.setClientInfo("k1", "v1"); + lazyConnection.setClientInfo("k2", "v2"); + verify(physicalConnection, never()).setClientInfo("k1", "v1"); + verify(physicalConnection, never()).setClientInfo("k2", "v2"); + lazyConnection.getClientInfo("k1"); // establish physical connection immediately + verify(physicalConnection).setClientInfo("k1", "v1"); + verify(physicalConnection).getClientInfo("k1"); + } + + @Test + void nonLazyHandlingClientInfoForProperties() throws SQLException { + DataSource mockDataSource = mock(); + Connection physicalConnection = mock(); + when(mockDataSource.getConnection()).thenReturn(physicalConnection); + lazyProxy.setTargetDataSource(mockDataSource); + + Connection lazyConnection = lazyProxy.getConnection(); + Properties properties = new Properties(); + properties.setProperty("k1", "v1"); + properties.setProperty("k2", "v2"); + lazyConnection.setClientInfo(properties); // establish physical connection immediately + verify(physicalConnection).setClientInfo(properties); + } + private static Stream streamIsolationConstants() { return Arrays.stream(Connection.class.getFields()) @@ -102,4 +277,8 @@ private static Stream streamIsolationConstants() { .filter(name -> name.startsWith("TRANSACTION_")); } + private static void establishPhysicalConnection(Connection lazyConnection) throws SQLException { + lazyConnection.prepareStatement("SELECT 1"); + } + }