You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by gg...@apache.org on 2019/07/30 21:42:37 UTC

[commons-dbcp] branch master updated: [DBCP-551] org.apache.commons.dbcp2.DelegatingStatement.close() should try to close ALL of its result sets.

This is an automated email from the ASF dual-hosted git repository.

ggregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-dbcp.git


The following commit(s) were added to refs/heads/master by this push:
     new 6e86e0d  [DBCP-551] org.apache.commons.dbcp2.DelegatingStatement.close() should try to close ALL of its result sets.
6e86e0d is described below

commit 6e86e0de00af300293d2182e5395e17f4447f1ab
Author: Gary Gregory <ga...@gmail.com>
AuthorDate: Tue Jul 30 17:42:32 2019 -0400

    [DBCP-551] org.apache.commons.dbcp2.DelegatingStatement.close() should
    try to close ALL of its result sets.
---
 src/changes/changes.xml                            |   3 +
 .../apache/commons/dbcp2/DelegatingConnection.java |  20 +-
 .../apache/commons/dbcp2/DelegatingStatement.java  |  59 +--
 .../org/apache/commons/dbcp2/SQLExceptionList.java |  51 +++
 .../commons/dbcp2/TestDelegatingStatement.java     | 400 +++++++++++----------
 .../apache/commons/dbcp2/TestSQLExceptionList.java |  38 ++
 .../org/apache/commons/dbcp2/TesterConnection.java |   3 +-
 .../commons/dbcp2/TesterPreparedStatement.java     |   1 +
 .../org/apache/commons/dbcp2/TesterResultSet.java  |  28 +-
 .../org/apache/commons/dbcp2/TesterStatement.java  |  21 +-
 10 files changed, 406 insertions(+), 218 deletions(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index cd94f1d..b69108a 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -124,6 +124,9 @@ The <action> type attribute can be add,update,fix,remove.
       <action dev="ggregory" type="update" due-to="Gary Gregory">
         Update tests from org.mockito:mockito-core 2.28.2 to 3.0.0.
       </action>
+      <action dev="ggregory" type="update" issue="DBCP-551" due-to="Gary Gregory">
+        org.apache.commons.dbcp2.DelegatingStatement.close() should try to close ALL of its result sets.
+      </action>
     </release>
     <release version="2.6.0" date="2019-02-14" description="This is a minor release, including bug fixes and enhancements.">
       <action dev="chtompki" type="add" issue="DBCP-534" due-to="Peter Wicks">
diff --git a/src/main/java/org/apache/commons/dbcp2/DelegatingConnection.java b/src/main/java/org/apache/commons/dbcp2/DelegatingConnection.java
index fe0de9d..2b8853f 100644
--- a/src/main/java/org/apache/commons/dbcp2/DelegatingConnection.java
+++ b/src/main/java/org/apache/commons/dbcp2/DelegatingConnection.java
@@ -246,7 +246,23 @@ public class DelegatingConnection<C extends Connection> extends AbandonedTrace i
     }
 
     protected void handleException(final SQLException e) throws SQLException {
-        throw e;
+        handleException(e, true);
+    }
+
+    /**
+     * Rethrows the given {@code SQLException} if {@code rethrow} is true.
+     * 
+     * @param e       The SQLException
+     * @param rethrow Wether or not to rethrow
+     * @return the given {@code SQLException}
+     * @throws SQLException the given {@code SQLException}
+     * @since 2.7.0
+     */
+    protected SQLException handleException(final SQLException e, final boolean rethrow) throws SQLException {
+        if (rethrow) {
+            throw e;
+        }
+        return e;
     }
 
     private void initializeStatement(final DelegatingStatement ds) throws SQLException {
@@ -608,7 +624,7 @@ public class DelegatingConnection<C extends Connection> extends AbandonedTrace i
         // Statement's when it is closed.
         // DBCP-288. Not all the traced objects will be statements
         final List<AbandonedTrace> traces = getTrace();
-        if (traces != null && traces.size() > 0) {
+        if (traces != null && !traces.isEmpty()) {
             final Iterator<AbandonedTrace> traceIter = traces.iterator();
             while (traceIter.hasNext()) {
                 final Object trace = traceIter.next();
diff --git a/src/main/java/org/apache/commons/dbcp2/DelegatingStatement.java b/src/main/java/org/apache/commons/dbcp2/DelegatingStatement.java
index 8a1725f..88f01e5 100644
--- a/src/main/java/org/apache/commons/dbcp2/DelegatingStatement.java
+++ b/src/main/java/org/apache/commons/dbcp2/DelegatingStatement.java
@@ -21,6 +21,7 @@ import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.sql.SQLWarning;
 import java.sql.Statement;
+import java.util.ArrayList;
 import java.util.List;
 
 /**
@@ -125,35 +126,53 @@ public class DelegatingStatement extends AbandonedTrace implements Statement {
         if (isClosed()) {
             return;
         }
+        final List<SQLException> thrown = new ArrayList<>();
         try {
-            try {
-                if (connection != null) {
-                    connection.removeTrace(this);
-                    connection = null;
-                }
+            if (connection != null) {
+                connection.removeTrace(this);
+                connection = null;
+            }
 
-                // The JDBC spec requires that a statement close any open
-                // ResultSet's when it is closed.
-                // FIXME The PreparedStatement we're wrapping should handle this for us.
-                // See bug 17301 for what could happen when ResultSets are closed twice.
-                final List<AbandonedTrace> resultSets = getTrace();
-                if (resultSets != null) {
-                    final ResultSet[] set = resultSets.toArray(new ResultSet[resultSets.size()]);
-                    for (final ResultSet element : set) {
-                        element.close();
+            // The JDBC spec requires that a statement close any open
+            // ResultSet's when it is closed.
+            // FIXME The PreparedStatement we're wrapping should handle this for us.
+            // See bug 17301 for what could happen when ResultSets are closed twice.
+            final List<AbandonedTrace> resultSetList = getTrace();
+            if (resultSetList != null) {
+                final int size = resultSetList.size();
+                final ResultSet[] resultSets = resultSetList.toArray(new ResultSet[size]);
+                for (final ResultSet resultSet : resultSets) {
+                    if (resultSet != null) {
+                        try {
+                            resultSet.close();
+                        } catch (SQLException e) {
+                            if (connection != null) {
+                                // Does not rethrow e.
+                                connection.handleException(e, false);
+                            }
+                            thrown.add(e);
+                        }
                     }
-                    clearTrace();
                 }
-
-                if (statement != null) {
+                clearTrace();
+            }
+            if (statement != null) {
+                try {
                     statement.close();
+                } catch (SQLException e) {
+                    if (connection != null) {
+                        // Does not rethrow e.
+                        connection.handleException(e, false);
+                    }
+                    thrown.add(e);
                 }
-            } catch (final SQLException e) {
-                handleException(e);
             }
         } finally {
             closed = true;
             statement = null;
+            if (!thrown.isEmpty()) {
+                throw new SQLExceptionList(thrown);
+            }
         }
     }
 
@@ -615,7 +634,7 @@ public class DelegatingStatement extends AbandonedTrace implements Statement {
     }
 
     /*
-     * Note was protected prior to JDBC 4
+     * Note: This method was protected prior to JDBC 4.
      */
     @Override
     public boolean isClosed() throws SQLException {
diff --git a/src/main/java/org/apache/commons/dbcp2/SQLExceptionList.java b/src/main/java/org/apache/commons/dbcp2/SQLExceptionList.java
new file mode 100644
index 0000000..1df9356
--- /dev/null
+++ b/src/main/java/org/apache/commons/dbcp2/SQLExceptionList.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ *      http://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.apache.commons.dbcp2;
+
+import java.sql.SQLException;
+import java.util.List;
+
+/**
+ * A SQLException based on a list of SQLException causes.
+ * <p>
+ * The first exception in the list is used as this exception's cause and is accessible with the usual
+ * {@link #getCause()} while the complete list is accessible with {@link #getCauseList()}.
+ * </p>
+ * 
+ * @since 2.7.0
+ */
+public class SQLExceptionList extends SQLException {
+
+    private static final long serialVersionUID = 1L;
+    private final List<? extends SQLException> causeList;
+
+    /**
+     * Creates a new exception caused by a list of exceptions.
+     * 
+     * @param causeList a list of cause exceptions.
+     */
+    public SQLExceptionList(List<? extends SQLException> causeList) {
+        super(String.format("%,d SQLExceptions: %s", causeList.size(), causeList), causeList.get(0));
+        this.causeList = causeList;
+    }
+
+    public List<? extends SQLException> getCauseList() {
+        return causeList;
+    }
+
+}
diff --git a/src/test/java/org/apache/commons/dbcp2/TestDelegatingStatement.java b/src/test/java/org/apache/commons/dbcp2/TestDelegatingStatement.java
index f9867f8..f187cbf 100644
--- a/src/test/java/org/apache/commons/dbcp2/TestDelegatingStatement.java
+++ b/src/test/java/org/apache/commons/dbcp2/TestDelegatingStatement.java
@@ -28,43 +28,71 @@ import static org.mockito.Mockito.verify;
 
 import java.lang.reflect.Proxy;
 import java.sql.Connection;
+import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.sql.Statement;
 
+import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
 public class TestDelegatingStatement {
 
-    private DelegatingConnection<Connection> conn = null;
-    private Connection delegateConn = null;
-    private Statement obj = null;
-    private DelegatingStatement delegate = null;
+    private static class TesterStatementNonWrapping extends TesterStatement {
+
+        public TesterStatementNonWrapping(final Connection conn) {
+            super(conn);
+        }
+
+        @Override
+        public boolean isWrapperFor(final Class<?> iface) throws SQLException {
+            return false;
+        }
+    }
+    
+    private DelegatingConnection<Connection> delegatingConnection;
+    private TesterConnection testerConnection;
+    private Statement mockedStatement;
+    private DelegatingStatement delegatingStatement;
+    private DelegatingStatement delegatingTesterStatement;
+    private TesterResultSet testerResultSet;
+    private TesterStatement testerStatement;
 
     @BeforeEach
     public void setUp() throws Exception {
-        delegateConn = new TesterConnection("test", "test");
-        conn = new DelegatingConnection<>(delegateConn);
-        obj = mock(Statement.class);
-        delegate = new DelegatingStatement(conn, obj);
+        testerConnection = new TesterConnection("test", "test");
+        delegatingConnection = new DelegatingConnection<>(testerConnection);
+        mockedStatement = mock(Statement.class);
+        testerStatement = new TesterStatement(testerConnection);
+        delegatingStatement = new DelegatingStatement(delegatingConnection, mockedStatement);
+        delegatingTesterStatement = new DelegatingStatement(delegatingConnection, testerStatement);
+        testerResultSet = new TesterResultSet(mockedStatement);
     }
 
     @Test
-    public void testExecuteQueryReturnsNull() throws Exception {
-        assertNull(delegate.executeQuery("null"));
+    public void testAddBatchString() throws Exception {
+        try {
+            delegatingStatement.addBatch("foo");
+        } catch (final SQLException e) {
+        }
+        verify(mockedStatement, times(1)).addBatch("foo");
     }
 
     @Test
-    public void testGetDelegate() throws Exception {
-        assertEquals(obj,delegate.getDelegate());
+    public void testCancel() throws Exception {
+        try {
+            delegatingStatement.cancel();
+        } catch (final SQLException e) {
+        }
+        verify(mockedStatement, times(1)).cancel();
     }
 
     @Test
     public void testCheckOpen() throws Exception {
-        delegate.checkOpen();
-        delegate.close();
+        delegatingStatement.checkOpen();
+        delegatingStatement.close();
         try {
-            delegate.checkOpen();
+            delegatingStatement.checkOpen();
             fail("Expecting SQLException");
         } catch (final SQLException ex) {
             // expected
@@ -72,222 +100,207 @@ public class TestDelegatingStatement {
     }
 
     @Test
-    public void testIsWrapperFor() throws Exception {
-        final TesterConnection tstConn = new TesterConnection("test", "test");
-        final TesterStatement tstStmt = new TesterStatementNonWrapping(tstConn);
-        final DelegatingConnection<TesterConnection> dconn = new DelegatingConnection<>(tstConn);
-        final DelegatingStatement stamt = new DelegatingStatement(dconn, tstStmt);
-
-        final Class<?> stmtProxyClass = Proxy.getProxyClass(
-                this.getClass().getClassLoader(),
-                Statement.class);
-
-        assertTrue(stamt.isWrapperFor(DelegatingStatement.class));
-        assertTrue(stamt.isWrapperFor(TesterStatement.class));
-        assertFalse(stamt.isWrapperFor(stmtProxyClass));
-
-        stamt.close();
-    }
-
-    private static class TesterStatementNonWrapping extends TesterStatement {
-
-        public TesterStatementNonWrapping(final Connection conn) {
-            super(conn);
-        }
-
-        @Override
-        public boolean isWrapperFor(final Class<?> iface) throws SQLException {
-            return false;
-        }
-    }
-
-    @Test
-    public void testAddBatchString() throws Exception {
+    public void testClearBatch() throws Exception {
         try {
-            delegate.addBatch("foo");
+            delegatingStatement.clearBatch();
         } catch (final SQLException e) {
         }
-        verify(obj, times(1)).addBatch("foo");
+        verify(mockedStatement, times(1)).clearBatch();
     }
 
     @Test
-    public void testCancel() throws Exception {
+    public void testClearWarnings() throws Exception {
         try {
-            delegate.cancel();
+            delegatingStatement.clearWarnings();
         } catch (final SQLException e) {
         }
-        verify(obj, times(1)).cancel();
+        verify(mockedStatement, times(1)).clearWarnings();
     }
 
     @Test
-    public void testClearBatch() throws Exception {
+    public void testClose() throws Exception {
         try {
-            delegate.clearBatch();
+            delegatingStatement.close();
         } catch (final SQLException e) {
         }
-        verify(obj, times(1)).clearBatch();
+        verify(mockedStatement, times(1)).close();
     }
 
     @Test
-    public void testClearWarnings() throws Exception {
+    public void testCloseWithResultSetCloseException() throws Exception {
         try {
-            delegate.clearWarnings();
+            testerResultSet.setSqlExceptionOnClose(true);
+            delegatingStatement.addTrace(testerResultSet);
+            delegatingStatement.close();
+            Assertions.fail("Excpected a SQLExceptionList");
         } catch (final SQLException e) {
+            Assertions.assertTrue(e instanceof SQLExceptionList);
+        } finally {
+            testerResultSet.setSqlExceptionOnClose(false);
         }
-        verify(obj, times(1)).clearWarnings();
+        verify(mockedStatement, times(1)).close();
     }
 
     @Test
-    public void testClose() throws Exception {
+    public void testCloseWithStatementCloseException() throws Exception {
         try {
-            delegate.close();
+            testerStatement.setSqlExceptionOnClose(true);
+            delegatingTesterStatement.close();
+            Assertions.fail("Excpected a SQLExceptionList");
         } catch (final SQLException e) {
+            Assertions.assertTrue(e instanceof SQLExceptionList);
+        } finally {
+            testerStatement.setSqlExceptionOnClose(false);
         }
-        verify(obj, times(1)).close();
     }
 
     @Test
     public void testCloseOnCompletion() throws Exception {
         try {
-            delegate.closeOnCompletion();
+            delegatingStatement.closeOnCompletion();
         } catch (final SQLException e) {
         }
-        verify(obj, times(1)).closeOnCompletion();
+        verify(mockedStatement, times(1)).closeOnCompletion();
     }
 
     @Test
-    public void testExecuteStringIntegerArray() throws Exception {
+    public void testExecuteBatch() throws Exception {
         try {
-            delegate.execute("foo", (int[]) null);
+            delegatingStatement.executeBatch();
         } catch (final SQLException e) {
         }
-        verify(obj, times(1)).execute("foo", (int[]) null);
+        verify(mockedStatement, times(1)).executeBatch();
     }
 
     @Test
-    public void testExecuteString() throws Exception {
+    public void testExecuteLargeBatch() throws Exception {
         try {
-            delegate.execute("foo");
+            delegatingStatement.executeLargeBatch();
         } catch (final SQLException e) {
         }
-        verify(obj, times(1)).execute("foo");
+        verify(mockedStatement, times(1)).executeLargeBatch();
     }
 
     @Test
-    public void testExecuteStringStringArray() throws Exception {
+    public void testExecuteLargeUpdateString() throws Exception {
         try {
-            delegate.execute("foo", (String[]) null);
+            delegatingStatement.executeLargeUpdate("foo");
         } catch (final SQLException e) {
         }
-        verify(obj, times(1)).execute("foo", (String[]) null);
+        verify(mockedStatement, times(1)).executeLargeUpdate("foo");
     }
 
     @Test
-    public void testExecuteStringInteger() throws Exception {
+    public void testExecuteLargeUpdateStringInteger() throws Exception {
         try {
-            delegate.execute("foo", 1);
+            delegatingStatement.executeLargeUpdate("foo", 1);
         } catch (final SQLException e) {
         }
-        verify(obj, times(1)).execute("foo", 1);
+        verify(mockedStatement, times(1)).executeLargeUpdate("foo", 1);
     }
 
     @Test
-    public void testExecuteBatch() throws Exception {
+    public void testExecuteLargeUpdateStringIntegerArray() throws Exception {
         try {
-            delegate.executeBatch();
+            delegatingStatement.executeLargeUpdate("foo", (int[]) null);
         } catch (final SQLException e) {
         }
-        verify(obj, times(1)).executeBatch();
+        verify(mockedStatement, times(1)).executeLargeUpdate("foo", (int[]) null);
     }
 
     @Test
-    public void testExecuteLargeBatch() throws Exception {
+    public void testExecuteLargeUpdateStringStringArray() throws Exception {
         try {
-            delegate.executeLargeBatch();
+            delegatingStatement.executeLargeUpdate("foo", (String[]) null);
         } catch (final SQLException e) {
         }
-        verify(obj, times(1)).executeLargeBatch();
+        verify(mockedStatement, times(1)).executeLargeUpdate("foo", (String[]) null);
     }
 
     @Test
-    public void testExecuteLargeUpdateStringInteger() throws Exception {
+    public void testExecuteQueryReturnsNull() throws Exception {
+        assertNull(delegatingStatement.executeQuery("null"));
+    }
+
+    @Test
+    public void testExecuteQueryString() throws Exception {
         try {
-            delegate.executeLargeUpdate("foo", 1);
+            delegatingStatement.executeQuery("foo");
         } catch (final SQLException e) {
         }
-        verify(obj, times(1)).executeLargeUpdate("foo", 1);
+        verify(mockedStatement, times(1)).executeQuery("foo");
     }
 
     @Test
-    public void testExecuteLargeUpdateStringIntegerArray() throws Exception {
+    public void testExecuteString() throws Exception {
         try {
-            delegate.executeLargeUpdate("foo", (int[]) null);
+            delegatingStatement.execute("foo");
         } catch (final SQLException e) {
         }
-        verify(obj, times(1)).executeLargeUpdate("foo", (int[]) null);
+        verify(mockedStatement, times(1)).execute("foo");
     }
 
     @Test
-    public void testExecuteLargeUpdateString() throws Exception {
+    public void testExecuteStringInteger() throws Exception {
         try {
-            delegate.executeLargeUpdate("foo");
+            delegatingStatement.execute("foo", 1);
         } catch (final SQLException e) {
         }
-        verify(obj, times(1)).executeLargeUpdate("foo");
+        verify(mockedStatement, times(1)).execute("foo", 1);
     }
 
     @Test
-    public void testExecuteLargeUpdateStringStringArray() throws Exception {
+    public void testExecuteStringIntegerArray() throws Exception {
         try {
-            delegate.executeLargeUpdate("foo", (String[]) null);
+            delegatingStatement.execute("foo", (int[]) null);
         } catch (final SQLException e) {
         }
-        verify(obj, times(1)).executeLargeUpdate("foo", (String[]) null);
+        verify(mockedStatement, times(1)).execute("foo", (int[]) null);
     }
 
     @Test
-    public void testExecuteQueryString() throws Exception {
+    public void testExecuteStringStringArray() throws Exception {
         try {
-            delegate.executeQuery("foo");
+            delegatingStatement.execute("foo", (String[]) null);
         } catch (final SQLException e) {
         }
-        verify(obj, times(1)).executeQuery("foo");
+        verify(mockedStatement, times(1)).execute("foo", (String[]) null);
     }
 
     @Test
-    public void testExecuteUpdateStringIntegerArray() throws Exception {
+    public void testExecuteUpdateString() throws Exception {
         try {
-            delegate.executeUpdate("foo", (int[]) null);
+            delegatingStatement.executeUpdate("foo");
         } catch (final SQLException e) {
         }
-        verify(obj, times(1)).executeUpdate("foo", (int[]) null);
+        verify(mockedStatement, times(1)).executeUpdate("foo");
     }
 
     @Test
-    public void testExecuteUpdateStringStringArray() throws Exception {
+    public void testExecuteUpdateStringInteger() throws Exception {
         try {
-            delegate.executeUpdate("foo", (String[]) null);
+            delegatingStatement.executeUpdate("foo", 1);
         } catch (final SQLException e) {
         }
-        verify(obj, times(1)).executeUpdate("foo", (String[]) null);
+        verify(mockedStatement, times(1)).executeUpdate("foo", 1);
     }
 
     @Test
-    public void testExecuteUpdateString() throws Exception {
+    public void testExecuteUpdateStringIntegerArray() throws Exception {
         try {
-            delegate.executeUpdate("foo");
+            delegatingStatement.executeUpdate("foo", (int[]) null);
         } catch (final SQLException e) {
         }
-        verify(obj, times(1)).executeUpdate("foo");
+        verify(mockedStatement, times(1)).executeUpdate("foo", (int[]) null);
     }
 
     @Test
-    public void testExecuteUpdateStringInteger() throws Exception {
+    public void testExecuteUpdateStringStringArray() throws Exception {
         try {
-            delegate.executeUpdate("foo", 1);
+            delegatingStatement.executeUpdate("foo", (String[]) null);
         } catch (final SQLException e) {
         }
-        verify(obj, times(1)).executeUpdate("foo", 1);
+        verify(mockedStatement, times(1)).executeUpdate("foo", (String[]) null);
     }
 
     /**
@@ -299,163 +312,159 @@ public class TestDelegatingStatement {
     @Test
     public void testGetConnection() throws Exception {
         try {
-            delegate.getConnection();
+            delegatingStatement.getConnection();
         } catch (final SQLException e) {
         }
-        verify(obj, times(0)).getConnection();
+        verify(mockedStatement, times(0)).getConnection();
+    }
+
+    @Test
+    public void testGetDelegate() throws Exception {
+        assertEquals(mockedStatement,delegatingStatement.getDelegate());
     }
 
     @Test
     public void testGetFetchDirection() throws Exception {
         try {
-            delegate.getFetchDirection();
+            delegatingStatement.getFetchDirection();
         } catch (final SQLException e) {
         }
-        verify(obj, times(1)).getFetchDirection();
+        verify(mockedStatement, times(1)).getFetchDirection();
     }
 
     @Test
     public void testGetFetchSize() throws Exception {
         try {
-            delegate.getFetchSize();
+            delegatingStatement.getFetchSize();
         } catch (final SQLException e) {
         }
-        verify(obj, times(1)).getFetchSize();
+        verify(mockedStatement, times(1)).getFetchSize();
     }
 
     @Test
     public void testGetGeneratedKeys() throws Exception {
         try {
-            delegate.getGeneratedKeys();
+            delegatingStatement.getGeneratedKeys();
         } catch (final SQLException e) {
         }
-        verify(obj, times(1)).getGeneratedKeys();
+        verify(mockedStatement, times(1)).getGeneratedKeys();
     }
 
     @Test
     public void testGetLargeMaxRows() throws Exception {
         try {
-            delegate.getLargeMaxRows();
+            delegatingStatement.getLargeMaxRows();
         } catch (final SQLException e) {
         }
-        verify(obj, times(1)).getLargeMaxRows();
+        verify(mockedStatement, times(1)).getLargeMaxRows();
     }
 
     @Test
     public void testGetLargeUpdateCount() throws Exception {
         try {
-            delegate.getLargeUpdateCount();
+            delegatingStatement.getLargeUpdateCount();
         } catch (final SQLException e) {
         }
-        verify(obj, times(1)).getLargeUpdateCount();
+        verify(mockedStatement, times(1)).getLargeUpdateCount();
     }
 
     @Test
     public void testGetMaxFieldSize() throws Exception {
         try {
-            delegate.getMaxFieldSize();
+            delegatingStatement.getMaxFieldSize();
         } catch (final SQLException e) {
         }
-        verify(obj, times(1)).getMaxFieldSize();
+        verify(mockedStatement, times(1)).getMaxFieldSize();
     }
 
     @Test
     public void testGetMaxRows() throws Exception {
         try {
-            delegate.getMaxRows();
+            delegatingStatement.getMaxRows();
         } catch (final SQLException e) {
         }
-        verify(obj, times(1)).getMaxRows();
+        verify(mockedStatement, times(1)).getMaxRows();
     }
 
     @Test
-    public void testGetMoreResultsInteger() throws Exception {
+    public void testGetMoreResults() throws Exception {
         try {
-            delegate.getMoreResults(1);
+            delegatingStatement.getMoreResults();
         } catch (final SQLException e) {
         }
-        verify(obj, times(1)).getMoreResults(1);
+        verify(mockedStatement, times(1)).getMoreResults();
     }
 
     @Test
-    public void testGetMoreResults() throws Exception {
+    public void testGetMoreResultsInteger() throws Exception {
         try {
-            delegate.getMoreResults();
+            delegatingStatement.getMoreResults(1);
         } catch (final SQLException e) {
         }
-        verify(obj, times(1)).getMoreResults();
+        verify(mockedStatement, times(1)).getMoreResults(1);
     }
 
     @Test
     public void testGetQueryTimeout() throws Exception {
         try {
-            delegate.getQueryTimeout();
+            delegatingStatement.getQueryTimeout();
         } catch (final SQLException e) {
         }
-        verify(obj, times(1)).getQueryTimeout();
+        verify(mockedStatement, times(1)).getQueryTimeout();
     }
 
     @Test
     public void testGetResultSet() throws Exception {
         try {
-            delegate.getResultSet();
+            delegatingStatement.getResultSet();
         } catch (final SQLException e) {
         }
-        verify(obj, times(1)).getResultSet();
+        verify(mockedStatement, times(1)).getResultSet();
     }
 
     @Test
     public void testGetResultSetConcurrency() throws Exception {
         try {
-            delegate.getResultSetConcurrency();
+            delegatingStatement.getResultSetConcurrency();
         } catch (final SQLException e) {
         }
-        verify(obj, times(1)).getResultSetConcurrency();
+        verify(mockedStatement, times(1)).getResultSetConcurrency();
     }
 
     @Test
     public void testGetResultSetHoldability() throws Exception {
         try {
-            delegate.getResultSetHoldability();
+            delegatingStatement.getResultSetHoldability();
         } catch (final SQLException e) {
         }
-        verify(obj, times(1)).getResultSetHoldability();
+        verify(mockedStatement, times(1)).getResultSetHoldability();
     }
 
     @Test
     public void testGetResultSetType() throws Exception {
         try {
-            delegate.getResultSetType();
+            delegatingStatement.getResultSetType();
         } catch (final SQLException e) {
         }
-        verify(obj, times(1)).getResultSetType();
+        verify(mockedStatement, times(1)).getResultSetType();
     }
 
     @Test
     public void testGetUpdateCount() throws Exception {
         try {
-            delegate.getUpdateCount();
+            delegatingStatement.getUpdateCount();
         } catch (final SQLException e) {
         }
-        verify(obj, times(1)).getUpdateCount();
+        verify(mockedStatement, times(1)).getUpdateCount();
     }
 
     @Test
     public void testGetWarnings() throws Exception {
         try {
-            delegate.getWarnings();
+            delegatingStatement.getWarnings();
         } catch (final SQLException e) {
         }
-        verify(obj, times(1)).getWarnings();
-    }
-
-    @Test
-    public void testIsCloseOnCompletion() throws Exception {
-        try {
-            delegate.isCloseOnCompletion();
-        } catch (final SQLException e) {
-        }
-        verify(obj, times(1)).isCloseOnCompletion();
+        verify(mockedStatement, times(1)).getWarnings();
     }
 
     /**
@@ -467,111 +476,138 @@ public class TestDelegatingStatement {
     @Test
     public void testIsClosed() throws Exception {
         try {
-            delegate.isClosed();
+            delegatingStatement.isClosed();
         } catch (final SQLException e) {
         }
-        verify(obj, times(0)).isClosed();
+        verify(mockedStatement, times(0)).isClosed();
+    }
+
+    @Test
+    public void testIsCloseOnCompletion() throws Exception {
+        try {
+            delegatingStatement.isCloseOnCompletion();
+        } catch (final SQLException e) {
+        }
+        verify(mockedStatement, times(1)).isCloseOnCompletion();
     }
 
     @Test
     public void testIsPoolable() throws Exception {
         try {
-            delegate.isPoolable();
+            delegatingStatement.isPoolable();
         } catch (final SQLException e) {
         }
-        verify(obj, times(1)).isPoolable();
+        verify(mockedStatement, times(1)).isPoolable();
+    }
+
+    @Test
+    public void testIsWrapperFor() throws Exception {
+        final TesterConnection tstConn = new TesterConnection("test", "test");
+        final TesterStatement tstStmt = new TesterStatementNonWrapping(tstConn);
+        final DelegatingConnection<TesterConnection> dconn = new DelegatingConnection<>(tstConn);
+        final DelegatingStatement stamt = new DelegatingStatement(dconn, tstStmt);
+
+        final Class<?> stmtProxyClass = Proxy.getProxyClass(
+                this.getClass().getClassLoader(),
+                Statement.class);
+
+        assertTrue(stamt.isWrapperFor(DelegatingStatement.class));
+        assertTrue(stamt.isWrapperFor(TesterStatement.class));
+        assertFalse(stamt.isWrapperFor(stmtProxyClass));
+
+        stamt.close();
     }
 
     @Test
     public void testSetCursorNameString() throws Exception {
         try {
-            delegate.setCursorName("foo");
+            delegatingStatement.setCursorName("foo");
         } catch (final SQLException e) {
         }
-        verify(obj, times(1)).setCursorName("foo");
+        verify(mockedStatement, times(1)).setCursorName("foo");
     }
 
     @Test
     public void testSetEscapeProcessingBoolean() throws Exception {
         try {
-            delegate.setEscapeProcessing(Boolean.TRUE);
+            delegatingStatement.setEscapeProcessing(Boolean.TRUE);
         } catch (final SQLException e) {
         }
-        verify(obj, times(1)).setEscapeProcessing(Boolean.TRUE);
+        verify(mockedStatement, times(1)).setEscapeProcessing(Boolean.TRUE);
     }
 
     @Test
     public void testSetFetchDirectionInteger() throws Exception {
         try {
-            delegate.setFetchDirection(1);
+            delegatingStatement.setFetchDirection(1);
         } catch (final SQLException e) {
         }
-        verify(obj, times(1)).setFetchDirection(1);
+        verify(mockedStatement, times(1)).setFetchDirection(1);
     }
 
     @Test
     public void testSetFetchSizeInteger() throws Exception {
         try {
-            delegate.setFetchSize(1);
+            delegatingStatement.setFetchSize(1);
         } catch (final SQLException e) {
         }
-        verify(obj, times(1)).setFetchSize(1);
+        verify(mockedStatement, times(1)).setFetchSize(1);
     }
 
     @Test
     public void testSetLargeMaxRowsLong() throws Exception {
         try {
-            delegate.setLargeMaxRows(1l);
+            delegatingStatement.setLargeMaxRows(1l);
         } catch (final SQLException e) {
         }
-        verify(obj, times(1)).setLargeMaxRows(1l);
+        verify(mockedStatement, times(1)).setLargeMaxRows(1l);
     }
 
     @Test
     public void testSetMaxFieldSizeInteger() throws Exception {
         try {
-            delegate.setMaxFieldSize(1);
+            delegatingStatement.setMaxFieldSize(1);
         } catch (final SQLException e) {
         }
-        verify(obj, times(1)).setMaxFieldSize(1);
+        verify(mockedStatement, times(1)).setMaxFieldSize(1);
     }
 
     @Test
     public void testSetMaxRowsInteger() throws Exception {
         try {
-            delegate.setMaxRows(1);
+            delegatingStatement.setMaxRows(1);
         } catch (final SQLException e) {
         }
-        verify(obj, times(1)).setMaxRows(1);
+        verify(mockedStatement, times(1)).setMaxRows(1);
     }
 
     @Test
     public void testSetPoolableBoolean() throws Exception {
         try {
-            delegate.setPoolable(Boolean.TRUE);
+            delegatingStatement.setPoolable(Boolean.TRUE);
         } catch (final SQLException e) {
         }
-        verify(obj, times(1)).setPoolable(Boolean.TRUE);
+        verify(mockedStatement, times(1)).setPoolable(Boolean.TRUE);
     }
 
     @Test
     public void testSetQueryTimeoutInteger() throws Exception {
         try {
-            delegate.setQueryTimeout(1);
+            delegatingStatement.setQueryTimeout(1);
         } catch (final SQLException e) {
         }
-        verify(obj, times(1)).setQueryTimeout(1);
+        verify(mockedStatement, times(1)).setQueryTimeout(1);
     }
 
     @Test
     public void testWrap() throws SQLException {
-        assertEquals(delegate, delegate.unwrap(Statement.class));
-        assertEquals(delegate, delegate.unwrap(DelegatingStatement.class));
-        assertEquals(obj, delegate.unwrap(obj.getClass()));
-        assertNull(delegate.unwrap(String.class));
-        assertTrue(delegate.isWrapperFor(Statement.class));
-        assertTrue(delegate.isWrapperFor(DelegatingStatement.class));
-        assertTrue(delegate.isWrapperFor(obj.getClass()));
-        assertFalse(delegate.isWrapperFor(String.class));
+        assertEquals(delegatingStatement, delegatingStatement.unwrap(Statement.class));
+        assertEquals(delegatingStatement, delegatingStatement.unwrap(DelegatingStatement.class));
+        assertEquals(mockedStatement, delegatingStatement.unwrap(mockedStatement.getClass()));
+        assertNull(delegatingStatement.unwrap(String.class));
+        assertTrue(delegatingStatement.isWrapperFor(Statement.class));
+        assertTrue(delegatingStatement.isWrapperFor(DelegatingStatement.class));
+        assertTrue(delegatingStatement.isWrapperFor(mockedStatement.getClass()));
+        assertFalse(delegatingStatement.isWrapperFor(String.class));
     }
 }
diff --git a/src/test/java/org/apache/commons/dbcp2/TestSQLExceptionList.java b/src/test/java/org/apache/commons/dbcp2/TestSQLExceptionList.java
new file mode 100644
index 0000000..ebb0bd8
--- /dev/null
+++ b/src/test/java/org/apache/commons/dbcp2/TestSQLExceptionList.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ *      http://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.apache.commons.dbcp2;
+
+import java.sql.SQLTransientException;
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestSQLExceptionList {
+
+    @Test
+    public void testCause() {
+        final SQLTransientException cause = new SQLTransientException();
+        final List<SQLTransientException> list = Collections.singletonList(cause);
+        final SQLExceptionList sqlExceptionList = new SQLExceptionList(list);
+        Assertions.assertEquals(cause, sqlExceptionList.getCause());
+        Assertions.assertEquals(list, sqlExceptionList.getCauseList());
+        sqlExceptionList.printStackTrace();
+    }
+}
diff --git a/src/test/java/org/apache/commons/dbcp2/TesterConnection.java b/src/test/java/org/apache/commons/dbcp2/TesterConnection.java
index 4329b1f..dae4380 100644
--- a/src/test/java/org/apache/commons/dbcp2/TesterConnection.java
+++ b/src/test/java/org/apache/commons/dbcp2/TesterConnection.java
@@ -38,7 +38,8 @@ import java.util.concurrent.Executor;
 /**
  * A dummy {@link Connection}, for testing purposes.
  */
-public class TesterConnection implements Connection {
+public class TesterConnection extends AbandonedTrace implements Connection {
+    
     protected boolean _open = true;
     protected boolean _autoCommit = true;
     protected int _transactionIsolation = 1;
diff --git a/src/test/java/org/apache/commons/dbcp2/TesterPreparedStatement.java b/src/test/java/org/apache/commons/dbcp2/TesterPreparedStatement.java
index 5883cb8..5ee864d 100644
--- a/src/test/java/org/apache/commons/dbcp2/TesterPreparedStatement.java
+++ b/src/test/java/org/apache/commons/dbcp2/TesterPreparedStatement.java
@@ -39,6 +39,7 @@ import java.sql.SQLXML;
  * A dummy {@link PreparedStatement}, for testing purposes.
  */
 public class TesterPreparedStatement extends TesterStatement implements PreparedStatement {
+    
     private final ResultSetMetaData _resultSetMetaData = null;
     private String _sql = null;
     private String _catalog = null;
diff --git a/src/test/java/org/apache/commons/dbcp2/TesterResultSet.java b/src/test/java/org/apache/commons/dbcp2/TesterResultSet.java
index afc4c1e..0d37499 100644
--- a/src/test/java/org/apache/commons/dbcp2/TesterResultSet.java
+++ b/src/test/java/org/apache/commons/dbcp2/TesterResultSet.java
@@ -40,18 +40,16 @@ import java.sql.SQLXML;
 /**
  * A dummy {@link ResultSet}, for testing purposes.
  */
-public class TesterResultSet implements ResultSet {
+public class TesterResultSet extends AbandonedTrace implements ResultSet {
+    
     protected int _type = ResultSet.TYPE_FORWARD_ONLY;
-
     protected int _concurrency = ResultSet.CONCUR_READ_ONLY;
-
     protected Object[][] _data = null;
-
     protected int _currentRow = -1;
     protected Statement _statement = null;
-
     protected int _rowsLeft = 2;
     protected boolean _open = true;
+    protected boolean _sqlExceptionOnClose = false;
 
     public TesterResultSet(final Statement stmt) {
         _statement = stmt;
@@ -101,6 +99,10 @@ public class TesterResultSet implements ResultSet {
 
     @Override
     public void close() throws SQLException {
+        if (_sqlExceptionOnClose) {
+            throw new SQLException("TestSQLExceptionOnClose");
+        }
+        
         if (!_open) {
             return;
         }
@@ -641,6 +643,10 @@ public java.sql.Date getDate(final int columnIndex, final Calendar cal) throws S
         return _rowsLeft == 0;
     }
 
+    public boolean isSqlExceptionOnClose() {
+        return _sqlExceptionOnClose;
+    }
+
     @Override
     public boolean isWrapperFor(final Class<?> iface) throws SQLException {
         throw new SQLException("Not implemented.");
@@ -720,6 +726,10 @@ public java.sql.Date getDate(final int columnIndex, final Calendar cal) throws S
         checkOpen();
     }
 
+    public void setSqlExceptionOnClose(final boolean sqlExceptionOnClose) {
+        this._sqlExceptionOnClose = sqlExceptionOnClose;
+    }
+
     @Override
     public <T> T unwrap(final Class<T> iface) throws SQLException {
         throw new SQLException("Not implemented.");
@@ -742,6 +752,7 @@ public java.sql.Date getDate(final int columnIndex, final Calendar cal) throws S
         throw new SQLException("Not implemented.");
     }
 
+
     @Override
     public void updateAsciiStream(final int columnIndex, final InputStream inputStream, final long length) throws SQLException {
         throw new SQLException("Not implemented.");
@@ -754,7 +765,6 @@ public java.sql.Date getDate(final int columnIndex, final Calendar cal) throws S
         checkOpen();
     }
 
-
     @Override
     public void updateAsciiStream(final String columnLabel, final InputStream inputStream) throws SQLException {
         throw new SQLException("Not implemented.");
@@ -890,6 +900,7 @@ public java.sql.Date getDate(final int columnIndex, final Calendar cal) throws S
         throw new SQLException("Not implemented.");
     }
 
+
     @Override
     public void updateCharacterStream(final int columnIndex, final Reader reader, final long length) throws SQLException {
         throw new SQLException("Not implemented.");
@@ -902,7 +913,6 @@ public java.sql.Date getDate(final int columnIndex, final Calendar cal) throws S
         checkOpen();
     }
 
-
     @Override
     public void updateCharacterStream(final String columnLabel, final Reader reader) throws SQLException {
         throw new SQLException("Not implemented.");
@@ -1175,13 +1185,13 @@ public java.sql.Date getDate(final int columnIndex, final Calendar cal) throws S
     public void updateTimestamp(final int columnIndex, final java.sql.Timestamp x) throws SQLException {
         checkOpen();
     }
-
+    
     @Override
     public void updateTimestamp(final String columnName, final java.sql.Timestamp x)
       throws SQLException {
         checkOpen();
     }
-
+    
     @Override
     public boolean wasNull() throws SQLException {
         checkOpen();
diff --git a/src/test/java/org/apache/commons/dbcp2/TesterStatement.java b/src/test/java/org/apache/commons/dbcp2/TesterStatement.java
index 99cc9f9..5a70f64 100644
--- a/src/test/java/org/apache/commons/dbcp2/TesterStatement.java
+++ b/src/test/java/org/apache/commons/dbcp2/TesterStatement.java
@@ -26,13 +26,11 @@ import java.sql.Statement;
 /**
  * A dummy {@link Statement}, for testing purposes.
  */
-public class TesterStatement implements Statement {
+public class TesterStatement extends AbandonedTrace implements Statement {
+    
     protected Connection _connection = null;
-
     protected boolean _open = true;
-
     protected long _rowsUpdated = 1;
-
     protected boolean _executeResponse = true;
     protected int _maxFieldSize = 1024;
     protected long _maxRows = 1024;
@@ -45,15 +43,18 @@ public class TesterStatement implements Statement {
     protected int _resultSetType = 1;
     private int _resultSetHoldability = 1;
     protected ResultSet _resultSet = null;
+    protected boolean _sqlExceptionOnClose = false;
 
     public TesterStatement(final Connection conn) {
         _connection = conn;
     }
+    
     public TesterStatement(final Connection conn, final int resultSetType, final int resultSetConcurrency) {
         _connection = conn;
         _resultSetType = resultSetType;
         _resultSetConcurrency = resultSetConcurrency;
     }
+    
     public TesterStatement(final Connection conn, final int resultSetType, final int resultSetConcurrency,
             final int resultSetHoldability) {
         _connection = conn;
@@ -90,6 +91,10 @@ public class TesterStatement implements Statement {
 
     @Override
     public void close() throws SQLException {
+        if (_sqlExceptionOnClose) {
+            throw new SQLException("TestSQLExceptionOnClose");
+        }
+
         // calling close twice has no effect
         if (!_open) {
             return;
@@ -331,6 +336,10 @@ public class TesterStatement implements Statement {
         throw new SQLException("Not implemented.");
     }
 
+    public boolean isSqlExceptionOnClose() {
+        return _sqlExceptionOnClose;
+    }
+
     @Override
     public boolean isWrapperFor(final Class<?> iface) throws SQLException {
         throw new SQLException("Not implemented.");
@@ -389,6 +398,10 @@ public class TesterStatement implements Statement {
         _queryTimeout = seconds;
     }
 
+    public void setSqlExceptionOnClose(final boolean _sqlExceptionOnClose) {
+        this._sqlExceptionOnClose = _sqlExceptionOnClose;
+    }
+
     @Override
     public <T> T unwrap(final Class<T> iface) throws SQLException {
         throw new SQLException("Not implemented.");