diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/SingleConnectionDataSource.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/SingleConnectionDataSource.java index f61b3e7b66..17026d8d6f 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/SingleConnectionDataSource.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/SingleConnectionDataSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -52,12 +52,16 @@ import org.springframework.util.ObjectUtils; * @see java.sql.Connection#close() * @see DataSourceUtils#releaseConnection */ -public class SingleConnectionDataSource extends DriverManagerDataSource implements SmartDataSource, DisposableBean { +public class SingleConnectionDataSource extends DriverManagerDataSource + implements SmartDataSource, AutoCloseable, DisposableBean { - /** Create a close-suppressing proxy?. */ + /** Create a close-suppressing proxy? */ private boolean suppressClose; - /** Override auto-commit state?. */ + /** Explicit rollback before close? */ + private boolean rollbackBeforeClose; + + /** Override auto-commit state? */ @Nullable private Boolean autoCommit; @@ -124,7 +128,7 @@ public class SingleConnectionDataSource extends DriverManagerDataSource implemen /** - * Set whether the returned Connection should be a close-suppressing proxy + * Specify whether the returned Connection should be a close-suppressing proxy * or the physical Connection. */ public void setSuppressClose(boolean suppressClose) { @@ -140,10 +144,29 @@ public class SingleConnectionDataSource extends DriverManagerDataSource implemen } /** - * Set whether the returned Connection's "autoCommit" setting should be overridden. + * Specify whether the shared Connection should be explicitly rolled back + * before close (if not in auto-commit mode). + *
This is recommended for the Oracle JDBC driver in testing scenarios. + * @since 6.1.2 + */ + public void setRollbackBeforeClose(boolean rollbackBeforeClose) { + this.rollbackBeforeClose = rollbackBeforeClose; + } + + /** + * Return whether the shared Connection should be explicitly rolled back + * before close (if not in auto-commit mode). + * @since 6.1.2 + */ + protected boolean isRollbackBeforeClose() { + return this.rollbackBeforeClose; + } + + /** + * Specify whether the returned Connection's "autoCommit" setting should be overridden. */ public void setAutoCommit(boolean autoCommit) { - this.autoCommit = (autoCommit); + this.autoCommit = autoCommit; } /** @@ -201,13 +224,27 @@ public class SingleConnectionDataSource extends DriverManagerDataSource implemen /** * Close the underlying Connection. * The provider of this DataSource needs to care for proper shutdown. - *
As this bean implements DisposableBean, a bean factory will - * automatically invoke this on destruction of its cached singletons. + *
As this class implements {@link AutoCloseable}, it can be used + * with a try-with-resource statement. + * @since 6.1.2 + */ + @Override + public void close() { + destroy(); + } + + /** + * Close the underlying Connection. + * The provider of this DataSource needs to care for proper shutdown. + *
As this bean implements {@link DisposableBean}, a bean factory + * will automatically invoke this on destruction of the bean. */ @Override public void destroy() { synchronized (this.connectionMonitor) { - closeConnection(); + if (this.target != null) { + closeConnection(this.target); + } } } @@ -220,7 +257,9 @@ public class SingleConnectionDataSource extends DriverManagerDataSource implemen throw new IllegalStateException("'url' property is required for lazily initializing a Connection"); } synchronized (this.connectionMonitor) { - closeConnection(); + if (this.target != null) { + closeConnection(this.target); + } this.target = getConnectionFromDriver(getUsername(), getPassword()); prepareConnection(this.target); if (logger.isDebugEnabled()) { @@ -235,7 +274,9 @@ public class SingleConnectionDataSource extends DriverManagerDataSource implemen */ public void resetConnection() { synchronized (this.connectionMonitor) { - closeConnection(); + if (this.target != null) { + closeConnection(this.target); + } this.target = null; this.connection = null; } @@ -257,16 +298,25 @@ public class SingleConnectionDataSource extends DriverManagerDataSource implemen /** * Close the underlying shared Connection. + * @since 6.1.2 */ - private void closeConnection() { - if (this.target != null) { + protected void closeConnection(Connection con) { + if (isRollbackBeforeClose()) { try { - this.target.close(); + if (!con.getAutoCommit()) { + con.rollback(); + } } catch (Throwable ex) { - logger.info("Could not close shared JDBC Connection", ex); + logger.info("Could not roll back shared JDBC Connection before close", ex); } } + try { + con.close(); + } + catch (Throwable ex) { + logger.info("Could not close shared JDBC Connection", ex); + } } /** diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/SingleConnectionDataSourceTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/SingleConnectionDataSourceTests.java new file mode 100644 index 0000000000..0f9d674260 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/SingleConnectionDataSourceTests.java @@ -0,0 +1,100 @@ +/* + * Copyright 2002-2023 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.jdbc.datasource; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Properties; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * @author Juergen Hoeller + * @since 6.1.2 + */ +public class SingleConnectionDataSourceTests { + + private final Connection connection = mock(); + + + @Test + public void plainConnection() throws Exception { + SingleConnectionDataSource ds = new SingleConnectionDataSource(connection, false); + + ds.getConnection().close(); + verify(connection, times(1)).close(); + } + + @Test + public void withAutoCloseable() throws Exception { + try (SingleConnectionDataSource ds = new SingleConnectionDataSource(connection, false)) { + ds.getConnection(); + } + + verify(connection, times(1)).close(); + } + + @Test + public void withSuppressClose() throws Exception { + SingleConnectionDataSource ds = new SingleConnectionDataSource(connection, true); + + ds.getConnection().close(); + verify(connection, never()).close(); + + ds.destroy(); + verify(connection, times(1)).close(); + + given(connection.isClosed()).willReturn(true); + assertThatExceptionOfType(SQLException.class).isThrownBy(ds::getConnection); + } + + @Test + public void withRollbackBeforeClose() throws Exception { + SingleConnectionDataSource ds = new SingleConnectionDataSource(connection, true); + ds.setRollbackBeforeClose(true); + + ds.destroy(); + verify(connection, times(1)).rollback(); + verify(connection, times(1)).close(); + } + + @Test + public void withEnforcedAutoCommit() throws Exception { + SingleConnectionDataSource ds = new SingleConnectionDataSource() { + @Override + protected Connection getConnectionFromDriverManager(String url, Properties props) { + return connection; + } + }; + ds.setUrl("url"); + ds.setAutoCommit(true); + + ds.getConnection(); + verify(connection, times(1)).setAutoCommit(true); + + ds.destroy(); + verify(connection, times(1)).close(); + } + +}