mirror of
https://github.com/spring-projects/spring-framework.git
synced 2026-09-17 08:24:13 +00:00
Merge branch '7.0.x'
This commit is contained in:
+69
-6
@@ -22,7 +22,9 @@ import java.lang.reflect.Method;
|
|||||||
import java.lang.reflect.Proxy;
|
import java.lang.reflect.Proxy;
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
|
||||||
import javax.sql.DataSource;
|
import javax.sql.DataSource;
|
||||||
|
|
||||||
@@ -35,12 +37,36 @@ import org.springframework.util.Assert;
|
|||||||
/**
|
/**
|
||||||
* Proxy for a target DataSource, fetching actual JDBC Connections lazily,
|
* Proxy for a target DataSource, fetching actual JDBC Connections lazily,
|
||||||
* i.e. not until first creation of a Statement. Connection initialization
|
* i.e. not until first creation of a Statement. Connection initialization
|
||||||
* properties like auto-commit mode, transaction isolation and read-only mode
|
* properties like auto-commit mode, transaction isolation, read-only mode,
|
||||||
* will be kept and applied to the actual JDBC Connection as soon as an actual
|
* catalog, schema, holdability, client info, and network timeout will be kept
|
||||||
* Connection is fetched (if ever). Consequently, commit and rollback calls will
|
* and applied to the actual JDBC Connection as soon as an actual Connection
|
||||||
* be ignored if no Statements have been created. As of 6.1.2, there is also
|
* is fetched (if ever). Consequently, commit and rollback calls will be ignored
|
||||||
* special support for a {@link #setReadOnlyDataSource read-only DataSource} to use
|
* if no Statements have been created.
|
||||||
* during a read-only transaction, in addition to the regular target DataSource.
|
*
|
||||||
|
* <p>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.
|
||||||
|
*
|
||||||
|
* <p>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.
|
||||||
|
*
|
||||||
|
* <p>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.
|
||||||
|
*
|
||||||
|
* <p>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.
|
||||||
*
|
*
|
||||||
* <p>This DataSource proxy allows to avoid fetching JDBC Connections from
|
* <p>This DataSource proxy allows to avoid fetching JDBC Connections from
|
||||||
* a pool unless actually necessary. JDBC transaction control can happen
|
* a pool unless actually necessary. JDBC transaction control can happen
|
||||||
@@ -86,6 +112,7 @@ import org.springframework.util.Assert;
|
|||||||
*
|
*
|
||||||
* @author Juergen Hoeller
|
* @author Juergen Hoeller
|
||||||
* @author Sam Brannen
|
* @author Sam Brannen
|
||||||
|
* @author Chengang Guan
|
||||||
* @since 1.1.4
|
* @since 1.1.4
|
||||||
* @see DataSourceTransactionManager
|
* @see DataSourceTransactionManager
|
||||||
* @see #setTargetDataSource
|
* @see #setTargetDataSource
|
||||||
@@ -311,6 +338,12 @@ public class LazyConnectionDataSourceProxy extends DelegatingDataSource {
|
|||||||
|
|
||||||
private @Nullable Boolean autoCommit;
|
private @Nullable Boolean autoCommit;
|
||||||
|
|
||||||
|
private @Nullable Executor networkTimeoutExecutor;
|
||||||
|
|
||||||
|
private @Nullable Integer networkTimeout;
|
||||||
|
|
||||||
|
private @Nullable Map<String, String> clientInfo;
|
||||||
|
|
||||||
private boolean closed = false;
|
private boolean closed = false;
|
||||||
|
|
||||||
private @Nullable Connection target;
|
private @Nullable Connection target;
|
||||||
@@ -434,6 +467,28 @@ public class LazyConnectionDataSourceProxy extends DelegatingDataSource {
|
|||||||
// Ignore: no warnings to expose yet.
|
// Ignore: no warnings to expose yet.
|
||||||
return null;
|
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" -> {
|
case "close" -> {
|
||||||
// Ignore: no target connection yet.
|
// Ignore: no target connection yet.
|
||||||
this.closed = true;
|
this.closed = true;
|
||||||
@@ -530,6 +585,14 @@ public class LazyConnectionDataSourceProxy extends DelegatingDataSource {
|
|||||||
if (this.autoCommit != null && this.autoCommit != defaultAutoCommit()) {
|
if (this.autoCommit != null && this.autoCommit != defaultAutoCommit()) {
|
||||||
target.setAutoCommit(this.autoCommit);
|
target.setAutoCommit(this.autoCommit);
|
||||||
}
|
}
|
||||||
|
if (this.networkTimeout != null) {
|
||||||
|
target.setNetworkTimeout(this.networkTimeoutExecutor, this.networkTimeout);
|
||||||
|
}
|
||||||
|
if (this.clientInfo != null) {
|
||||||
|
for (Map.Entry<String, String> entry: this.clientInfo.entrySet()) {
|
||||||
|
target.setClientInfo(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Throwable settingsEx) {
|
catch (Throwable settingsEx) {
|
||||||
logger.debug("Failed to apply transaction settings to JDBC Connection", settingsEx);
|
logger.debug("Failed to apply transaction settings to JDBC Connection", settingsEx);
|
||||||
|
|||||||
+181
@@ -18,11 +18,17 @@ package org.springframework.jdbc.datasource;
|
|||||||
|
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
|
import java.sql.SQLException;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.Properties;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import javax.sql.DataSource;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import org.springframework.util.ReflectionUtils;
|
import org.springframework.util.ReflectionUtils;
|
||||||
@@ -32,19 +38,37 @@ import static java.sql.Connection.TRANSACTION_READ_COMMITTED;
|
|||||||
import static java.sql.Connection.TRANSACTION_READ_UNCOMMITTED;
|
import static java.sql.Connection.TRANSACTION_READ_UNCOMMITTED;
|
||||||
import static java.sql.Connection.TRANSACTION_REPEATABLE_READ;
|
import static java.sql.Connection.TRANSACTION_REPEATABLE_READ;
|
||||||
import static java.sql.Connection.TRANSACTION_SERIALIZABLE;
|
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.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||||
|
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}.
|
* Tests for {@link LazyConnectionDataSourceProxy}.
|
||||||
*
|
*
|
||||||
* @author Sam Brannen
|
* @author Sam Brannen
|
||||||
|
* @author Chengang Guan
|
||||||
* @since 6.1
|
* @since 6.1
|
||||||
*/
|
*/
|
||||||
class LazyConnectionDataSourceProxyTests {
|
class LazyConnectionDataSourceProxyTests {
|
||||||
|
|
||||||
private final LazyConnectionDataSourceProxy proxy = new LazyConnectionDataSourceProxy();
|
private final LazyConnectionDataSourceProxy proxy = new LazyConnectionDataSourceProxy();
|
||||||
|
|
||||||
|
private final LazyConnectionDataSourceProxy lazyProxy = new LazyConnectionDataSourceProxy();
|
||||||
|
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setup() {
|
||||||
|
lazyProxy.setDefaultAutoCommit(false);
|
||||||
|
lazyProxy.setDefaultTransactionIsolation(TRANSACTION_READ_UNCOMMITTED);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void setDefaultTransactionIsolationNameToUnsupportedValues() {
|
void setDefaultTransactionIsolationNameToUnsupportedValues() {
|
||||||
@@ -94,6 +118,159 @@ class LazyConnectionDataSourceProxyTests {
|
|||||||
assertThat(proxy.defaultTransactionIsolation()).isEqualTo(TRANSACTION_SERIALIZABLE);
|
assertThat(proxy.defaultTransactionIsolation()).isEqualTo(TRANSACTION_SERIALIZABLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void lazyHandlingOfCatalog() 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 lazyHandlingOfSchema() 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 lazyHandlingOfHoldability() 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 lazyHandlingOfTransactionIsolation() 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 lazyHandlingOfAutoCommit() 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 lazyHandlingOfNetworkTimeoutExecutor() 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);
|
||||||
|
assertThatExceptionOfType(SQLException.class)
|
||||||
|
.isThrownBy(() -> establishPhysicalConnection(lazyConnection2));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void lazyHandlingOfClientInfoForKeyValuePairs() 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).setClientInfo("k2", "v2");
|
||||||
|
verify(physicalConnection).getClientInfo("k1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void nonLazyHandlingOfClientInfoForProperties() 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<String> streamIsolationConstants() {
|
private static Stream<String> streamIsolationConstants() {
|
||||||
return Arrays.stream(Connection.class.getFields())
|
return Arrays.stream(Connection.class.getFields())
|
||||||
@@ -102,4 +279,8 @@ class LazyConnectionDataSourceProxyTests {
|
|||||||
.filter(name -> name.startsWith("TRANSACTION_"));
|
.filter(name -> name.startsWith("TRANSACTION_"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void establishPhysicalConnection(Connection lazyConnection) throws SQLException {
|
||||||
|
lazyConnection.prepareStatement("SELECT 1");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user