New Upstream Snapshot - commons-pool2

Ready changes

Summary

Merged new upstream version: 2.11.1+git20220429.1.6ad0b61 (was: 2.11.1).

Resulting package

Built on 2022-05-05T11:36 (took 7m15s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-snapshots libcommons-pool2-java

Lintian Result

Diff

diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 5b47509..9ebcd0e 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -18,8 +18,10 @@ updates:
   - package-ecosystem: "maven"
     directory: "/"
     schedule:
-      interval: "daily"
+      interval: "weekly"
+      day: "friday"
   - package-ecosystem: "github-actions"
     directory: "/"
     schedule:
-      interval: "daily"
+      interval: "weekly"
+      day: "friday"
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
new file mode 100644
index 0000000..0067a01
--- /dev/null
+++ b/.github/workflows/codeql-analysis.yml
@@ -0,0 +1,74 @@
+# 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.
+
+name: "CodeQL"
+
+on:
+  push:
+    branches: [ master ]
+  pull_request:
+    # The branches below must be a subset of the branches above
+    branches: [ master ]
+  schedule:
+    - cron: '33 9 * * 4'
+
+jobs:
+  analyze:
+    name: Analyze
+    runs-on: ubuntu-latest
+    permissions:
+      actions: read
+      contents: read
+      security-events: write
+
+    strategy:
+      fail-fast: false
+      matrix:
+        language: [ 'java' ]
+        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
+        # Learn more about CodeQL language support at https://git.io/codeql-language-support
+
+    steps:
+    - name: Checkout repository
+      uses: actions/checkout@v3
+
+    # Initializes the CodeQL tools for scanning.
+    - name: Initialize CodeQL
+      uses: github/codeql-action/init@v2
+      with:
+        languages: ${{ matrix.language }}
+        # If you wish to specify custom queries, you can do so here or in a config file.
+        # By default, queries listed here will override any specified in a config file.
+        # Prefix the list here with "+" to use these queries and those in the config file.
+        # queries: ./path/to/local/query, your-org/your-repo/queries@main
+
+    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
+    # If this step fails, then you should remove it and run the build manually (see below)
+    - name: Autobuild
+      uses: github/codeql-action/autobuild@v2
+
+    # ℹ️ Command-line programs to run using the OS shell.
+    # 📚 https://git.io/JvXDl
+
+    # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
+    #    and modify them (or add more) to build your code if your project
+    #    uses a compiled language
+
+    #- run: |
+    #   make bootstrap
+    #   make release
+
+    - name: Perform CodeQL Analysis
+      uses: github/codeql-action/analyze@v2
diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
index e981a0d..0c013f6 100644
--- a/.github/workflows/maven.yml
+++ b/.github/workflows/maven.yml
@@ -24,24 +24,24 @@ jobs:
     continue-on-error: ${{ matrix.experimental }}
     strategy:
       matrix:
-        java: [ 8, 11, 16 ]
+        java: [ 8, 11, 17 ]
         experimental: [false]
-        include:
-          - java: 17-ea
-            experimental: true        
+#        include:
+#          - java: 18-ea
+#            experimental: true
         
     steps:
-    - uses: actions/checkout@v2.3.4
-    - uses: actions/cache@v2.1.6
+    - uses: actions/checkout@v3
+    - uses: actions/cache@v3.0.2
       with:
         path: ~/.m2/repository
         key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
         restore-keys: |
           ${{ runner.os }}-maven-
     - name: Set up JDK ${{ matrix.java }}
-      uses: actions/setup-java@v2
+      uses: actions/setup-java@v3
       with:
-        distribution: 'adopt'
+        distribution: 'temurin'
         java-version: ${{ matrix.java }}
     - name: Build with Maven
       run: mvn -V -B --file pom.xml --no-transfer-progress
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index c27cf14..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,36 +0,0 @@
-# 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.
-
-language: java
-
-cache:
-  directories:
-    - $HOME/.m2
-
-jdk:
-  - openjdk8
-  - openjdk11
-  - openjdk16
-  - openjdk-ea
-
-matrix:
-  allow_failures:
-    - jdk: openjdk-ea
-
-script:
-  - mvn -V --no-transfer-progress
-
-after_success:
-  - mvn -V --no-transfer-progress clean cobertura:cobertura coveralls:report -Ptravis-cobertura
diff --git a/NOTICE.txt b/NOTICE.txt
index 75e95b7..747ac68 100644
--- a/NOTICE.txt
+++ b/NOTICE.txt
@@ -1,5 +1,5 @@
 Apache Commons Pool
-Copyright 2001-2021 The Apache Software Foundation
+Copyright 2001-2022 The Apache Software Foundation
 
 This product includes software developed at
 The Apache Software Foundation (https://www.apache.org/).
diff --git a/README.md b/README.md
index 59a4796..8d9f208 100644
--- a/README.md
+++ b/README.md
@@ -43,7 +43,6 @@
 Apache Commons Pool
 ===================
 
-[![Travis-CI Status](https://travis-ci.org/apache/commons-pool.svg)](https://travis-ci.org/apache/commons-pool)
 [![GitHub Actions Status](https://github.com/apache/commons-pool/workflows/Java%20CI/badge.svg)](https://github.com/apache/commons-pool/actions)
 [![Coverage Status](https://coveralls.io/repos/apache/commons-pool/badge.svg)](https://coveralls.io/r/apache/commons-pool)
 [![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.apache.commons/commons-pool2/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.apache.commons/commons-pool2/)
diff --git a/debian/changelog b/debian/changelog
index 3239e89..e475de1 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+commons-pool2 (2.11.1+git20220429.1.6ad0b61-1) UNRELEASED; urgency=low
+
+  * New upstream snapshot.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Thu, 05 May 2022 11:30:57 -0000
+
 commons-pool2 (2.11.1-1) unstable; urgency=medium
 
   * New upstream release
diff --git a/pom.xml b/pom.xml
index 51da0d6..011336a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -24,11 +24,11 @@
   <parent>
     <groupId>org.apache.commons</groupId>
     <artifactId>commons-parent</artifactId>
-    <version>52</version>
+    <version>53</version>
   </parent>
 
   <artifactId>commons-pool2</artifactId>
-  <version>2.11.1</version>
+  <version>2.12.0-SNAPSHOT</version>
   <name>Apache Commons Pool</name>
 
   <inceptionYear>2001</inceptionYear>
@@ -127,6 +127,9 @@
       <name>Todd Carmichael</name>
       <email>toddc@concur.com</email>
     </contributor>
+    <contributor>
+      <name>Arturo Bernal</name>
+    </contributor>
   </contributors>
 
   <dependencies>
@@ -139,7 +142,7 @@
     <dependency>
       <groupId>org.ow2.asm</groupId>
       <artifactId>asm-util</artifactId>
-      <version>9.2</version>
+      <version>9.3</version>
       <optional>true</optional>
     </dependency>
     <!-- testing -->
@@ -179,36 +182,28 @@
     <!-- Java 7 -->
     <commons.release.2.version>2.6.2</commons.release.2.version>
     <commons.release.2.desc>(Java 7)</commons.release.2.desc>
-    <!-- Java 6 -->
-    <commons.release.3.version>2.4.3</commons.release.3.version>
-    <commons.release.3.desc>(Java 6)</commons.release.3.desc>
-    <!-- Java 5 -->
-    <commons.release.4.version>1.6</commons.release.4.version>
-    <commons.release.4.desc>(Java 5)</commons.release.4.desc>
-    <commons.release.4.binary.suffix>-bin</commons.release.4.binary.suffix>
-    <!-- override parent name, because 1.x uses different artifactId -->
-    <commons.release.4.name>commons-pool-${commons.release.4.version}</commons.release.4.name>
+
     <commons.jira.id>POOL</commons.jira.id>
     <commons.jira.pid>12310488</commons.jira.pid>
     <commons.scmPubCheckoutDirectory>site-content</commons.scmPubCheckoutDirectory>
     <commons.osgi.import>net.sf.cglib.proxy;resolution:=optional,*</commons.osgi.import>
     <commons.animal-sniffer.version>1.20</commons.animal-sniffer.version>
     <commons.checkstyle-plugin.version>3.1.2</commons.checkstyle-plugin.version>
-    <commons.checkstyle.version>8.45.1</commons.checkstyle.version>
-    <commons.felix.version>5.1.2</commons.felix.version>
-    <spotbugs.plugin.version>4.3.0</spotbugs.plugin.version>
-    <spotbugs.impl.version>4.3.0</spotbugs.impl.version>
+    <commons.checkstyle.version>9.3</commons.checkstyle.version>
+    <commons.felix.version>5.1.4</commons.felix.version>
+    <spotbugs.plugin.version>4.6.0.0</spotbugs.plugin.version>
+    <spotbugs.impl.version>4.6.0</spotbugs.impl.version>
 
     <!-- Commons Release Plugin -->
-    <commons.bc.version>2.11.0</commons.bc.version>
+    <commons.bc.version>2.11.1</commons.bc.version>
     <commons.release.isDistModule>true</commons.release.isDistModule>
     <commons.releaseManagerName>Gary Gregory</commons.releaseManagerName>    
     <commons.releaseManagerKey>86fdc7e2a11262cb</commons.releaseManagerKey>
     
-    <commons.japicmp.version>0.15.3</commons.japicmp.version>
+    <commons.japicmp.version>0.15.7</commons.japicmp.version>
     <clirr.skip>true</clirr.skip>
     <japicmp.skip>false</japicmp.skip>
-    <junit.version>5.8.0-M1</junit.version>
+    <junit.version>5.8.2</junit.version>
     <spotbugs.skip>false</spotbugs.skip>
   </properties> 
 
@@ -224,7 +219,7 @@
               <dependency>
                 <groupId>biz.aQute.bnd</groupId>
                 <artifactId>biz.aQute.bndlib</artifactId>
-                <version>5.3.0</version>
+                <version>6.2.0</version>
               </dependency>
             </dependencies>
           </plugin>
@@ -243,6 +238,7 @@
             <artifactId>maven-project-info-reports-plugin</artifactId>
             <version>${commons.project-info.version}</version>
             <dependencies>
+              <!-- Fix org.apache.bcel.classfile.ClassFormatException: Invalid byte tag in constant pool: 19 -->
               <dependency>
                 <groupId>org.apache.bcel</groupId>
                 <artifactId>bcel</artifactId>
@@ -301,7 +297,7 @@
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-surefire-plugin</artifactId>
-          <version>3.0.0-M5</version>
+          <version>3.0.0-M6</version>
             <configuration>
              <!-- Don't allow test to run for more than 30 minutes -->
               <forkedProcessTimeoutInSeconds>1800</forkedProcessTimeoutInSeconds>
@@ -424,7 +420,7 @@
         </plugin>    
         <plugin>
           <artifactId>maven-pmd-plugin</artifactId>
-          <version>3.14.0</version>
+          <version>3.16.0</version>
           <configuration>
             <targetJdk>${maven.compiler.target}</targetJdk>
           </configuration>
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 4682ffc..b198552 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -43,8 +43,85 @@ The <action> type attribute can be add,update,fix,remove.
     <title>Apache Commons Pool Release Notes</title>
   </properties>
   <body>
-  <release version="2.11.1" date="2021-08-DD" description="This is a maintenance release (Java 8).">
-    <!-- FIXES -->
+  <release version="2.12.0" date="2021-MM-DD" description="This is a feature and maintenance release (Java 8).">
+    <!-- FIX -->
+    <action dev="psteitz" type="fix" issue="POOL-401">
+      Ensure that capacity freed by invalidateObject is available to all keyed pools.
+    </action>
+    <action dev="psteitz" type="fix" due-to="Codievilky August" issue="POOL-391">
+      Ensure capacity freed by clear is made available to GKOP borrowers.
+    </action>
+    <action dev="ggregory" type="fix" issue="POOL-402" due-to="Cp-John, Phil Steitz, Bruno P. Kinoshita, Gary Gregory">
+      Check blockWhenExhausted in hasBorrowWaiters #116.
+    </action>
+    <action dev="ggregory" type="fix" due-to="Arturo Bernal">
+      Simplify test assertion with similar call but simpler. #131.
+    </action>       
+    <action dev="ggregory" type="fix" due-to="Gary Gregory" issue="POOL-269">
+      Use generic exceptions instead of java.lang.Exception.
+    </action>       
+    <action dev="ggregory" type="fix" due-to="Gary Gregory" issue="POOL-405">
+      NullPointerException at org.apache.commons.pool2.impl.GenericKeyedObjectPool.invalidateObject(GenericKeyedObjectPool.java:1343).
+    </action>       
+    <!-- ADD -->
+    <action dev="ggregory" type="add" due-to="Gary Gregory">
+      Add PooledObject.getFullDuration().
+    </action>
+    <action dev="ggregory" type="add" due-to="Vamsi Pavan Kumar Sanka, Phil Steitz, Gary Gregory">
+      Add GenericKeyedObjectPool.getKeys().
+    </action>
+    <action dev="ggregory" type="add" due-to="Gary Gregory">
+      Add KeyedObjectPool.getKeys().
+    </action>
+    <action dev="ggregory" type="add">
+      Add github/codeql-action.
+    </action>
+    <!-- UPDATE -->
+    <action dev="ggregory" type="update" due-to="Dependabot">
+      Bump actions/checkout from 2.3.4 to 3 #109, #112, #134.
+    </action>
+    <action dev="kinow" type="update" due-to="Dependabot, Gary Gregory">
+      Bump actions/cache from 2.1.6 to 3.0.2 #117, #138.
+    </action>
+    <action dev="kinow" type="update" due-to="Gary Gregory">
+      Bump actions/setup-java from 2 to 3.
+    </action>
+    <action dev="ggregory" type="update" due-to="Dependabot">
+      Bump spotbugs from 4.3.0 to 4.6.0 #94, #99, #106, #114, #122, #129, #137.
+    </action>
+    <action dev="ggregory" type="update" due-to="Dependabot">
+      Bump spotbugs-maven-plugin from 4.3.0 to 4.6.0.0 #102, #110, #119, #125, #128, #139.
+    </action>
+    <action dev="ggregory" type="update" due-to="Dependabot">
+      Bump junit-bom from 5.8.0-M1 to 5.8.2 #96, #100, #103, #120.
+    </action>
+    <action dev="ggregory" type="update" due-to="Dependabot">
+      Bump checkstyle from 8.45.1 to 9.3 #97, #104, #111, #121, #126, #132.
+    </action>
+    <action dev="ggregory" type="update" due-to="Dependabot">
+      Bump maven-pmd-plugin from 3.14.0 to 3.15.0 #101.
+    </action>
+    <action dev="ggregory" type="update" due-to="Dependabot">
+      Bump biz.aQute.bndlib from 5.3.0 to 6.2.0 #105, #118, #135.
+    </action>
+    <action dev="kinow" type="update" due-to="Dependabot">
+      Bump maven-bundle-plugin from 5.1.3 to 5.1.4 #127.
+    </action>
+    <action dev="kinow" type="update" due-to="Dependabot">
+      Bump maven-surefire-plugin from 3.0.0-M5 to 3.0.0-M6 #142.
+    </action>
+    <action dev="kinow" type="update" due-to="Dependabot">
+      Bump asm-util from 9.2 to 9.3 #141.
+    </action>
+    <action dev="ggregory" type="update" due-to="Gary Gregory">
+      Bump commons-parent from 52 to 53.
+    </action>
+    <action dev="ggregory" type="update" due-to="Gary Gregory">
+      Bump japicmp-maven-plugin from 0.15.3 to 0.15.7.
+    </action>
+  </release>
+  <release version="2.11.1" date="2021-08-18" description="This is a maintenance release (Java 8).">
+    <!-- FIX -->
     <action dev="ggregory" type="fix" due-to="Gary Gregory">
       Getting a PooledObject's active duration returns a negative duration when the object is borrowed but not returned. Affects:
       - PooledObject.getActiveDuration()
@@ -76,7 +153,7 @@ The <action> type attribute can be add,update,fix,remove.
     <action dev="ggregory" type="fix" due-to="Arturo Bernal">
       Minors Changes #89.
     </action>
-    <!-- UPDATES -->
+    <!-- UPDATE -->
     <action dev="ggregory" type="update" due-to="Dependabot">
       Bump checkstyle from 8.45 to 8.45.1 #93.
     </action>
@@ -85,7 +162,7 @@ The <action> type attribute can be add,update,fix,remove.
     </action>
   </release>
   <release version="2.11.0" date="2021-08-08" description="This is a feature release (Java 8).">
-    <!-- FIXES -->
+    <!-- FIX -->
     <action dev="ggregory" type="fix" due-to="Gary Gregory">
       Fix "[WARNING] Old version of checkstyle detected. Consider updating to >= v8.30." Update Checktyle to 8.44.
     </action>
diff --git a/src/main/java/org/apache/commons/pool2/BaseKeyedPooledObjectFactory.java b/src/main/java/org/apache/commons/pool2/BaseKeyedPooledObjectFactory.java
index b375ce2..a1762c8 100644
--- a/src/main/java/org/apache/commons/pool2/BaseKeyedPooledObjectFactory.java
+++ b/src/main/java/org/apache/commons/pool2/BaseKeyedPooledObjectFactory.java
@@ -29,11 +29,11 @@ package org.apache.commons.pool2;
  *
  * @param <K> The type of keys managed by this factory.
  * @param <V> Type of element managed by this factory.
+ * @param <E> Type of exception thrown by this factory.
  *
  * @since 2.0
  */
-public abstract class BaseKeyedPooledObjectFactory<K, V> extends BaseObject
-        implements KeyedPooledObjectFactory<K, V> {
+public abstract class BaseKeyedPooledObjectFactory<K, V, E extends Exception> extends BaseObject implements KeyedPooledObjectFactory<K, V, E> {
 
     /**
      * Reinitialize an instance to be returned by the pool.
@@ -45,8 +45,7 @@ public abstract class BaseKeyedPooledObjectFactory<K, V> extends BaseObject
      * @param p a {@code PooledObject} wrapping the instance to be activated
      */
     @Override
-    public void activateObject(final K key, final PooledObject<V> p)
-        throws Exception {
+    public void activateObject(final K key, final PooledObject<V> p) throws E {
         // The default implementation is a no-op.
     }
 
@@ -56,11 +55,10 @@ public abstract class BaseKeyedPooledObjectFactory<K, V> extends BaseObject
      * @param key the key used when constructing the object
      * @return an instance that can be served by the pool
      *
-     * @throws Exception if there is a problem creating a new instance,
+     * @throws E if there is a problem creating a new instance,
      *    this will be propagated to the code requesting an object.
      */
-    public abstract V create(K key)
-        throws Exception;
+    public abstract V create(K key) throws E;
 
     /**
      * Destroy an instance no longer needed by the pool.
@@ -72,13 +70,12 @@ public abstract class BaseKeyedPooledObjectFactory<K, V> extends BaseObject
      * @param p a {@code PooledObject} wrapping the instance to be destroyed
      */
     @Override
-    public void destroyObject(final K key, final PooledObject<V> p)
-        throws Exception {
+    public void destroyObject(final K key, final PooledObject<V> p) throws E {
         // The default implementation is a no-op.
     }
 
     @Override
-    public PooledObject<V> makeObject(final K key) throws Exception {
+    public PooledObject<V> makeObject(final K key) throws E {
         return wrap(create(key));
     }
 
@@ -92,8 +89,7 @@ public abstract class BaseKeyedPooledObjectFactory<K, V> extends BaseObject
      * @param p a {@code PooledObject} wrapping the instance to be passivated
      */
     @Override
-    public void passivateObject(final K key, final PooledObject<V> p)
-        throws Exception {
+    public void passivateObject(final K key, final PooledObject<V> p) throws E {
         // The default implementation is a no-op.
     }
 
diff --git a/src/main/java/org/apache/commons/pool2/BaseObjectPool.java b/src/main/java/org/apache/commons/pool2/BaseObjectPool.java
index 53c7deb..62b94d4 100644
--- a/src/main/java/org/apache/commons/pool2/BaseObjectPool.java
+++ b/src/main/java/org/apache/commons/pool2/BaseObjectPool.java
@@ -25,10 +25,11 @@ package org.apache.commons.pool2;
  * </p>
  *
  * @param <T> Type of element pooled in this pool.
+ * @param <E> Type of exception thrown by this pool.
  *
  * @since 2.0
  */
-public abstract class BaseObjectPool<T> extends BaseObject implements ObjectPool<T> {
+public abstract class BaseObjectPool<T, E extends Exception> extends BaseObject implements ObjectPool<T, E> {
 
     private volatile boolean closed;
 
@@ -40,7 +41,7 @@ public abstract class BaseObjectPool<T> extends BaseObject implements ObjectPool
      *          method
      */
     @Override
-    public void addObject() throws Exception, UnsupportedOperationException {
+    public void addObject() throws E, UnsupportedOperationException {
         throw new UnsupportedOperationException();
     }
 
@@ -59,7 +60,7 @@ public abstract class BaseObjectPool<T> extends BaseObject implements ObjectPool
     }
 
     @Override
-    public abstract T borrowObject() throws Exception;
+    public abstract T borrowObject() throws E;
 
     /**
      * Not supported in this base implementation.
@@ -68,7 +69,7 @@ public abstract class BaseObjectPool<T> extends BaseObject implements ObjectPool
      *          method
      */
     @Override
-    public void clear() throws Exception, UnsupportedOperationException {
+    public void clear() throws E, UnsupportedOperationException {
         throw new UnsupportedOperationException();
     }
 
@@ -105,7 +106,7 @@ public abstract class BaseObjectPool<T> extends BaseObject implements ObjectPool
     }
 
     @Override
-    public abstract void invalidateObject(T obj) throws Exception;
+    public abstract void invalidateObject(T obj) throws E;
 
     /**
      * Has this pool instance been closed.
@@ -117,7 +118,7 @@ public abstract class BaseObjectPool<T> extends BaseObject implements ObjectPool
     }
 
     @Override
-    public abstract void returnObject(T obj) throws Exception;
+    public abstract void returnObject(T obj) throws E;
 
     @Override
     protected void toStringAppendFields(final StringBuilder builder) {
diff --git a/src/main/java/org/apache/commons/pool2/BasePooledObjectFactory.java b/src/main/java/org/apache/commons/pool2/BasePooledObjectFactory.java
index 6302f17..d7ae8de 100644
--- a/src/main/java/org/apache/commons/pool2/BasePooledObjectFactory.java
+++ b/src/main/java/org/apache/commons/pool2/BasePooledObjectFactory.java
@@ -24,13 +24,14 @@ package org.apache.commons.pool2;
  * This class is immutable, and therefore thread-safe
  *
  * @param <T> Type of element managed in this factory.
+ * @param <E> Type of exception thrown in this factory.
  *
  * @see PooledObjectFactory
  * @see BaseKeyedPooledObjectFactory
  *
  * @since 2.0
  */
-public abstract class BasePooledObjectFactory<T> extends BaseObject implements PooledObjectFactory<T> {
+public abstract class BasePooledObjectFactory<T, E extends Exception> extends BaseObject implements PooledObjectFactory<T, E> {
 
     /**
      *  No-op.
@@ -38,7 +39,7 @@ public abstract class BasePooledObjectFactory<T> extends BaseObject implements P
      *  @param p ignored
      */
     @Override
-    public void activateObject(final PooledObject<T> p) throws Exception {
+    public void activateObject(final PooledObject<T> p) throws E {
         // The default implementation is a no-op.
     }
 
@@ -49,10 +50,10 @@ public abstract class BasePooledObjectFactory<T> extends BaseObject implements P
      *
      * @return an instance to be served by the pool
      *
-     * @throws Exception if there is a problem creating a new instance,
+     * @throws E if there is a problem creating a new instance,
      *    this will be propagated to the code requesting an object.
      */
-    public abstract T create() throws Exception;
+    public abstract T create() throws E;
 
     /**
      *  No-op.
@@ -61,12 +62,12 @@ public abstract class BasePooledObjectFactory<T> extends BaseObject implements P
      */
     @Override
     public void destroyObject(final PooledObject<T> p)
-        throws Exception  {
+        throws E  {
         // The default implementation is a no-op.
     }
 
     @Override
-    public PooledObject<T> makeObject() throws Exception {
+    public PooledObject<T> makeObject() throws E {
         return wrap(create());
     }
 
@@ -77,7 +78,7 @@ public abstract class BasePooledObjectFactory<T> extends BaseObject implements P
      */
     @Override
     public void passivateObject(final PooledObject<T> p)
-        throws Exception {
+        throws E {
         // The default implementation is a no-op.
     }
 
diff --git a/src/main/java/org/apache/commons/pool2/KeyedObjectPool.java b/src/main/java/org/apache/commons/pool2/KeyedObjectPool.java
index 4b930bd..88a4fbc 100644
--- a/src/main/java/org/apache/commons/pool2/KeyedObjectPool.java
+++ b/src/main/java/org/apache/commons/pool2/KeyedObjectPool.java
@@ -18,7 +18,8 @@ package org.apache.commons.pool2;
 
 import java.io.Closeable;
 import java.util.Collection;
-import java.util.Iterator;
+import java.util.Collections;
+import java.util.List;
 import java.util.NoSuchElementException;
 
 /**
@@ -60,6 +61,7 @@ import java.util.NoSuchElementException;
  *
  * @param <K> The type of keys maintained by this pool.
  * @param <V> Type of element pooled in this pool.
+ * @param <E> Type of exception thrown by this pool.
  *
  * @see KeyedPooledObjectFactory
  * @see ObjectPool
@@ -67,7 +69,7 @@ import java.util.NoSuchElementException;
  *
  * @since 2.0
  */
-public interface KeyedObjectPool<K, V> extends Closeable {
+public interface KeyedObjectPool<K, V, E extends Exception> extends Closeable {
 
     /**
      * Creates an object using the {@link KeyedPooledObjectFactory factory} or
@@ -77,15 +79,14 @@ public interface KeyedObjectPool<K, V> extends Closeable {
      *
      * @param key the key a new instance should be added to
      *
-     * @throws Exception
+     * @throws E
      *              when {@link KeyedPooledObjectFactory#makeObject} fails.
      * @throws IllegalStateException
      *              after {@link #close} has been called on this pool.
      * @throws UnsupportedOperationException
      *              when this pool cannot add new idle objects.
      */
-    void addObject(K key) throws Exception, IllegalStateException,
-            UnsupportedOperationException;
+    void addObject(K key) throws E, IllegalStateException, UnsupportedOperationException;
 
     /**
      * Calls {@link KeyedObjectPool#addObject(Object)} with each
@@ -97,20 +98,19 @@ public interface KeyedObjectPool<K, V> extends Closeable {
      *            {@link Collection} of keys to add objects for.
      * @param count
      *            the number of idle objects to add for each {@code key}.
-     * @throws Exception
+     * @throws E
      *             when {@link KeyedObjectPool#addObject(Object)} fails.
      * @throws IllegalArgumentException
      *             when {@code keyedPool}, {@code keys}, or any value
      *             in {@code keys} is {@code null}.
      * @see #addObjects(Object, int)
      */
-    default void addObjects(final Collection<K> keys, final int count) throws Exception, IllegalArgumentException {
+    default void addObjects(final Collection<K> keys, final int count) throws E, IllegalArgumentException {
         if (keys == null) {
             throw new IllegalArgumentException(PoolUtils.MSG_NULL_KEYS);
         }
-        final Iterator<K> iter = keys.iterator();
-        while (iter.hasNext()) {
-            addObjects(iter.next(), count);
+        for (final K key : keys) {
+            addObjects(key, count);
         }
     }
 
@@ -122,13 +122,13 @@ public interface KeyedObjectPool<K, V> extends Closeable {
      *            the key to add objects for.
      * @param count
      *            the number of idle objects to add for {@code key}.
-     * @throws Exception
+     * @throws E
      *             when {@link KeyedObjectPool#addObject(Object)} fails.
      * @throws IllegalArgumentException
      *             when {@code key} is {@code null}.
      * @since 2.8.0
      */
-    default void addObjects(final K key, final int count) throws Exception, IllegalArgumentException {
+    default void addObjects(final K key, final int count) throws E, IllegalArgumentException {
         if (key == null) {
             throw new IllegalArgumentException(PoolUtils.MSG_NULL_KEY);
         }
@@ -166,14 +166,14 @@ public interface KeyedObjectPool<K, V> extends Closeable {
      *
      * @throws IllegalStateException
      *              after {@link #close close} has been called on this pool
-     * @throws Exception
+     * @throws E
      *              when {@link KeyedPooledObjectFactory#makeObject
      *              makeObject} throws an exception
      * @throws NoSuchElementException
      *              when the pool is exhausted and cannot or will not return
      *              another instance
      */
-    V borrowObject(K key) throws Exception, NoSuchElementException, IllegalStateException;
+    V borrowObject(K key) throws E, NoSuchElementException, IllegalStateException;
 
     /**
      * Clears the pool, removing all pooled instances (optional operation).
@@ -181,9 +181,9 @@ public interface KeyedObjectPool<K, V> extends Closeable {
      * @throws UnsupportedOperationException when this implementation doesn't
      *                                       support the operation
      *
-     * @throws Exception if the pool cannot be cleared
+     * @throws E if the pool cannot be cleared
      */
-    void clear() throws Exception, UnsupportedOperationException;
+    void clear() throws E, UnsupportedOperationException;
 
     /**
      * Clears the specified pool, removing all pooled instances corresponding to
@@ -194,9 +194,9 @@ public interface KeyedObjectPool<K, V> extends Closeable {
      * @throws UnsupportedOperationException when this implementation doesn't
      *                                       support the operation
      *
-     * @throws Exception if the key cannot be cleared
+     * @throws E if the key cannot be cleared
      */
-    void clear(K key) throws Exception, UnsupportedOperationException;
+    void clear(K key) throws E, UnsupportedOperationException;
 
     /**
      * Closes this pool, and free any resources associated with it.
@@ -212,6 +212,16 @@ public interface KeyedObjectPool<K, V> extends Closeable {
     @Override
     void close();
 
+    /**
+     * Gets a copy of the pool key list.
+     *
+     * @return a copy of the pool key list.
+     * @since 2.12.0
+     */
+    default List<K> getKeys() {
+        return Collections.emptyList();
+    }
+
     /**
      * Gets the total number of instances currently borrowed from this pool but
      * not yet returned. Returns a negative value if this information is not
@@ -267,9 +277,9 @@ public interface KeyedObjectPool<K, V> extends Closeable {
      * @param key the key used to obtain the object
      * @param obj a {@link #borrowObject borrowed} instance to be returned.
      *
-     * @throws Exception if the instance cannot be invalidated
+     * @throws E if the instance cannot be invalidated
      */
-    void invalidateObject(K key, V obj) throws Exception;
+    void invalidateObject(K key, V obj) throws E;
 
     /**
      * Invalidates an object from the pool, using the provided
@@ -290,10 +300,10 @@ public interface KeyedObjectPool<K, V> extends Closeable {
      * @param obj a {@link #borrowObject borrowed} instance to be returned.
      * @param destroyMode destroy activation context provided to the factory
      *
-     * @throws Exception if the instance cannot be invalidated
+     * @throws E if the instance cannot be invalidated
      * @since 2.9.0
      */
-    default void invalidateObject(final K key, final V obj, final DestroyMode destroyMode) throws Exception {
+    default void invalidateObject(final K key, final V obj, final DestroyMode destroyMode) throws E {
         invalidateObject(key, obj);
     }
 
@@ -314,7 +324,7 @@ public interface KeyedObjectPool<K, V> extends Closeable {
      *              to return an object that was never borrowed from the pool
      *              will trigger this exception.
      *
-     * @throws Exception if an instance cannot be returned to the pool
+     * @throws E if an instance cannot be returned to the pool
      */
-    void returnObject(K key, V obj) throws Exception;
+    void returnObject(K key, V obj) throws E;
 }
diff --git a/src/main/java/org/apache/commons/pool2/KeyedPooledObjectFactory.java b/src/main/java/org/apache/commons/pool2/KeyedPooledObjectFactory.java
index a7f75a3..0a61497 100644
--- a/src/main/java/org/apache/commons/pool2/KeyedPooledObjectFactory.java
+++ b/src/main/java/org/apache/commons/pool2/KeyedPooledObjectFactory.java
@@ -71,10 +71,11 @@ package org.apache.commons.pool2;
  *
  * @param <K> The type of keys managed by this factory.
  * @param <V> Type of element managed by this factory.
+ * @param <E> Type of exception thrown by this factory.
  *
  * @since 2.0
  */
-public interface KeyedPooledObjectFactory<K, V> {
+public interface KeyedPooledObjectFactory<K, V, E extends Exception> {
 
     /**
      * Reinitializes an instance to be returned by the pool.
@@ -82,12 +83,12 @@ public interface KeyedPooledObjectFactory<K, V> {
      * @param key the key used when selecting the object
      * @param p a {@code PooledObject} wrapping the instance to be activated
      *
-     * @throws Exception if there is a problem activating {@code obj},
+     * @throws E if there is a problem activating {@code obj},
      *    this exception may be swallowed by the pool.
      *
      * @see #destroyObject
      */
-    void activateObject(K key, PooledObject<V> p) throws Exception;
+    void activateObject(K key, PooledObject<V> p) throws E;
 
     /**
      * Destroys an instance no longer needed by the pool.
@@ -104,13 +105,13 @@ public interface KeyedPooledObjectFactory<K, V> {
      * @param key the key used when selecting the instance
      * @param p a {@code PooledObject} wrapping the instance to be destroyed
      *
-     * @throws Exception should be avoided as it may be swallowed by
+     * @throws E should be avoided as it may be swallowed by
      *    the pool implementation.
      *
      * @see #validateObject
      * @see KeyedObjectPool#invalidateObject
      */
-    void destroyObject(K key, PooledObject<V> p) throws Exception;
+    void destroyObject(K key, PooledObject<V> p) throws E;
 
     /**
      * Destroys an instance no longer needed by the pool, using the provided {@link DestroyMode}.
@@ -119,7 +120,7 @@ public interface KeyedPooledObjectFactory<K, V> {
      * @param p a {@code PooledObject} wrapping the instance to be destroyed
      * @param destroyMode DestroyMode providing context to the factory
      *
-     * @throws Exception should be avoided as it may be swallowed by
+     * @throws E should be avoided as it may be swallowed by
      *    the pool implementation.
      *
      * @see #validateObject
@@ -128,7 +129,7 @@ public interface KeyedPooledObjectFactory<K, V> {
      * @see DestroyMode
      * @since 2.9.0
      */
-    default void destroyObject(final K key, final PooledObject<V> p, final DestroyMode destroyMode) throws Exception {
+    default void destroyObject(final K key, final PooledObject<V> p, final DestroyMode destroyMode) throws E {
         destroyObject(key, p);
     }
 
@@ -141,10 +142,10 @@ public interface KeyedPooledObjectFactory<K, V> {
      * @return a {@code PooledObject} wrapping an instance that can
      * be served by the pool.
      *
-     * @throws Exception if there is a problem creating a new instance,
+     * @throws E if there is a problem creating a new instance,
      *    this will be propagated to the code requesting an object.
      */
-    PooledObject<V> makeObject(K key) throws Exception;
+    PooledObject<V> makeObject(K key) throws E;
 
     /**
      * Uninitializes an instance to be returned to the idle object pool.
@@ -152,12 +153,12 @@ public interface KeyedPooledObjectFactory<K, V> {
      * @param key the key used when selecting the object
      * @param p a {@code PooledObject} wrapping the instance to be passivated
      *
-     * @throws Exception if there is a problem passivating {@code obj},
+     * @throws E if there is a problem passivating {@code obj},
      *    this exception may be swallowed by the pool.
      *
      * @see #destroyObject
      */
-    void passivateObject(K key, PooledObject<V> p) throws Exception;
+    void passivateObject(K key, PooledObject<V> p) throws E;
 
     /**
      * Ensures that the instance is safe to be returned by the pool.
diff --git a/src/main/java/org/apache/commons/pool2/ObjectPool.java b/src/main/java/org/apache/commons/pool2/ObjectPool.java
index 8aa2d58..04f8e11 100644
--- a/src/main/java/org/apache/commons/pool2/ObjectPool.java
+++ b/src/main/java/org/apache/commons/pool2/ObjectPool.java
@@ -50,6 +50,7 @@ import java.util.NoSuchElementException;
  * </p>
  *
  * @param <T> Type of element pooled in this pool.
+ * @param <E> Type of exception thrown by this pool.
  *
  * @see PooledObjectFactory
  * @see KeyedObjectPool
@@ -57,7 +58,7 @@ import java.util.NoSuchElementException;
  *
  * @since 2.0
  */
-public interface ObjectPool<T> extends Closeable {
+public interface ObjectPool<T, E extends Exception> extends Closeable {
 
     /**
      * Creates an object using the {@link PooledObjectFactory factory} or other
@@ -65,15 +66,14 @@ public interface ObjectPool<T> extends Closeable {
      * the idle object pool. {@code addObject} is useful for "pre-loading"
      * a pool with idle objects. (Optional operation).
      *
-     * @throws Exception
+     * @throws E
      *              when {@link PooledObjectFactory#makeObject} fails.
      * @throws IllegalStateException
      *              after {@link #close} has been called on this pool.
      * @throws UnsupportedOperationException
      *              when this pool cannot add new idle objects.
      */
-    void addObject() throws Exception, IllegalStateException,
-            UnsupportedOperationException;
+    void addObject() throws E, IllegalStateException, UnsupportedOperationException;
 
     /**
      * Calls {@link ObjectPool#addObject()} {@code count}
@@ -81,11 +81,10 @@ public interface ObjectPool<T> extends Closeable {
      *
      * @param count
      *            the number of idle objects to add.
-     * @throws Exception
-     *             when {@link ObjectPool#addObject()} fails.
+     * @throws E See {@link ObjectPool#addObject()}.
      * @since 2.8.0
      */
-    default void addObjects(final int count) throws Exception {
+    default void addObjects(final int count) throws E {
         for (int i = 0; i < count; i++) {
             addObject();
         }
@@ -115,15 +114,14 @@ public interface ObjectPool<T> extends Closeable {
      *
      * @throws IllegalStateException
      *              after {@link #close close} has been called on this pool.
-     * @throws Exception
+     * @throws E
      *              when {@link PooledObjectFactory#makeObject} throws an
      *              exception.
      * @throws NoSuchElementException
      *              when the pool is exhausted and cannot or will not return
      *              another instance.
      */
-    T borrowObject() throws Exception, NoSuchElementException,
-            IllegalStateException;
+    T borrowObject() throws E, NoSuchElementException, IllegalStateException;
 
     /**
      * Clears any objects sitting idle in the pool, releasing any associated
@@ -133,9 +131,9 @@ public interface ObjectPool<T> extends Closeable {
      * @throws UnsupportedOperationException
      *              if this implementation does not support the operation
      *
-     * @throws Exception if the pool cannot be cleared
+     * @throws E if the pool cannot be cleared
      */
-    void clear() throws Exception, UnsupportedOperationException;
+    void clear() throws E, UnsupportedOperationException;
 
     /**
      * Closes this pool, and free any resources associated with it.
@@ -180,9 +178,9 @@ public interface ObjectPool<T> extends Closeable {
      *
      * @param obj a {@link #borrowObject borrowed} instance to be disposed.
      *
-     * @throws Exception if the instance cannot be invalidated
+     * @throws E if the instance cannot be invalidated
      */
-    void invalidateObject(T obj) throws Exception;
+    void invalidateObject(T obj) throws E;
 
     /**
      * Invalidates an object from the pool, using the provided
@@ -199,11 +197,10 @@ public interface ObjectPool<T> extends Closeable {
      *
      * @param obj a {@link #borrowObject borrowed} instance to be disposed.
      * @param destroyMode destroy activation context provided to the factory
-     *
-     * @throws Exception if the instance cannot be invalidated
+     * @throws E if the instance cannot be invalidated
      * @since 2.9.0
      */
-    default void invalidateObject(final T obj, final DestroyMode destroyMode) throws Exception {
+    default void invalidateObject(final T obj, final DestroyMode destroyMode) throws E {
         invalidateObject(obj);
     }
 
@@ -221,8 +218,8 @@ public interface ObjectPool<T> extends Closeable {
      *              to return an object that was never borrowed from the pool
      *              will trigger this exception.
      *
-     * @throws Exception if an instance cannot be returned to the pool
+     * @throws E if an instance cannot be returned to the pool
      */
-    void returnObject(T obj) throws Exception;
+    void returnObject(T obj) throws E;
 
 }
diff --git a/src/main/java/org/apache/commons/pool2/PoolUtils.java b/src/main/java/org/apache/commons/pool2/PoolUtils.java
index 7b1f4d5..7d64d9a 100644
--- a/src/main/java/org/apache/commons/pool2/PoolUtils.java
+++ b/src/main/java/org/apache/commons/pool2/PoolUtils.java
@@ -19,7 +19,7 @@ package org.apache.commons.pool2;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
 import java.util.NoSuchElementException;
 import java.util.Timer;
@@ -110,12 +110,12 @@ public final class PoolUtils {
      *
      * @param <K> object pool key type
      * @param <V> object pool value type
+     * @param <E> exception thrown by this pool
      */
-    private static class ErodingKeyedObjectPool<K, V> implements
-            KeyedObjectPool<K, V> {
+    private static class ErodingKeyedObjectPool<K, V, E extends Exception> implements KeyedObjectPool<K, V, E> {
 
         /** Underlying pool */
-        private final KeyedObjectPool<K, V> keyedPool;
+        private final KeyedObjectPool<K, V, E> keyedPool;
 
         /** Erosion factor */
         private final ErodingFactor erodingFactor;
@@ -131,7 +131,7 @@ public final class PoolUtils {
          *            events
          * @see #erodingFactor
          */
-        protected ErodingKeyedObjectPool(final KeyedObjectPool<K, V> keyedPool,
+        protected ErodingKeyedObjectPool(final KeyedObjectPool<K, V, E> keyedPool,
                 final ErodingFactor erodingFactor) {
             if (keyedPool == null) {
                 throw new IllegalArgumentException(
@@ -152,7 +152,7 @@ public final class PoolUtils {
          *            events
          * @see #erodingFactor
          */
-        public ErodingKeyedObjectPool(final KeyedObjectPool<K, V> keyedPool,
+        public ErodingKeyedObjectPool(final KeyedObjectPool<K, V, E> keyedPool,
                 final float factor) {
             this(keyedPool, new ErodingFactor(factor));
         }
@@ -161,8 +161,7 @@ public final class PoolUtils {
          * {@inheritDoc}
          */
         @Override
-        public void addObject(final K key) throws Exception,
-                IllegalStateException, UnsupportedOperationException {
+        public void addObject(final K key) throws E, IllegalStateException, UnsupportedOperationException {
             keyedPool.addObject(key);
         }
 
@@ -170,8 +169,7 @@ public final class PoolUtils {
          * {@inheritDoc}
          */
         @Override
-        public V borrowObject(final K key) throws Exception,
-                NoSuchElementException, IllegalStateException {
+        public V borrowObject(final K key) throws E, NoSuchElementException, IllegalStateException {
             return keyedPool.borrowObject(key);
         }
 
@@ -179,7 +177,7 @@ public final class PoolUtils {
          * {@inheritDoc}
          */
         @Override
-        public void clear() throws Exception, UnsupportedOperationException {
+        public void clear() throws E, UnsupportedOperationException {
             keyedPool.clear();
         }
 
@@ -187,8 +185,7 @@ public final class PoolUtils {
          * {@inheritDoc}
          */
         @Override
-        public void clear(final K key) throws Exception,
-                UnsupportedOperationException {
+        public void clear(final K key) throws E, UnsupportedOperationException {
             keyedPool.clear(key);
         }
 
@@ -220,10 +217,18 @@ public final class PoolUtils {
          *
          * @return the keyed pool that this ErodingKeyedObjectPool wraps
          */
-        protected KeyedObjectPool<K, V> getKeyedPool() {
+        protected KeyedObjectPool<K, V, E> getKeyedPool() {
             return keyedPool;
         }
 
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public List<K> getKeys() {
+            return keyedPool.getKeys();
+        }
+
         /**
          * {@inheritDoc}
          */
@@ -282,7 +287,7 @@ public final class PoolUtils {
          * @see #erodingFactor
          */
         @Override
-        public void returnObject(final K key, final V obj) throws Exception {
+        public void returnObject(final K key, final V obj) throws E {
             boolean discard = false;
             final long nowMillis = System.currentTimeMillis();
             final ErodingFactor factor = getErodingFactor(key);
@@ -316,17 +321,19 @@ public final class PoolUtils {
                     erodingFactor + ", keyedPool=" + keyedPool + '}';
         }
     }
+
     /**
      * Decorates an object pool, adding "eroding" behavior. Based on the
      * configured {@link #factor erosion factor}, objects returning to the pool
      * may be invalidated instead of being added to idle capacity.
      *
      * @param <T> type of objects in the pool
+     * @param <E> type of exceptions from the pool
      */
-    private static class ErodingObjectPool<T> implements ObjectPool<T> {
+    private static class ErodingObjectPool<T, E extends Exception> implements ObjectPool<T, E> {
 
         /** Underlying object pool */
-        private final ObjectPool<T> pool;
+        private final ObjectPool<T, E> pool;
 
         /** Erosion factor */
         private final ErodingFactor factor;
@@ -342,7 +349,7 @@ public final class PoolUtils {
          *            events
          * @see #factor
          */
-        public ErodingObjectPool(final ObjectPool<T> pool, final float factor) {
+        public ErodingObjectPool(final ObjectPool<T, E> pool, final float factor) {
             this.pool = pool;
             this.factor = new ErodingFactor(factor);
         }
@@ -351,8 +358,7 @@ public final class PoolUtils {
          * {@inheritDoc}
          */
         @Override
-        public void addObject() throws Exception, IllegalStateException,
-                UnsupportedOperationException {
+        public void addObject() throws E, IllegalStateException, UnsupportedOperationException{
             pool.addObject();
         }
 
@@ -360,8 +366,7 @@ public final class PoolUtils {
          * {@inheritDoc}
          */
         @Override
-        public T borrowObject() throws Exception, NoSuchElementException,
-                IllegalStateException {
+        public T borrowObject() throws E, NoSuchElementException, IllegalStateException {
             return pool.borrowObject();
         }
 
@@ -369,7 +374,7 @@ public final class PoolUtils {
          * {@inheritDoc}
          */
         @Override
-        public void clear() throws Exception, UnsupportedOperationException {
+        public void clear() throws E, UnsupportedOperationException {
             pool.clear();
         }
 
@@ -466,9 +471,9 @@ public final class PoolUtils {
      *
      * @param <K> object pool key type
      * @param <V> object pool value type
+     * @param <E> exception thrown by this pool
      */
-    private static final class ErodingPerKeyKeyedObjectPool<K, V> extends
-            ErodingKeyedObjectPool<K, V> {
+    private static final class ErodingPerKeyKeyedObjectPool<K, V, E extends Exception> extends ErodingKeyedObjectPool<K, V, E> {
 
         /** Erosion factor - same for all pools */
         private final float factor;
@@ -485,8 +490,7 @@ public final class PoolUtils {
          * @param factor
          *            erosion factor
          */
-        public ErodingPerKeyKeyedObjectPool(
-                final KeyedObjectPool<K, V> keyedPool, final float factor) {
+        public ErodingPerKeyKeyedObjectPool(final KeyedObjectPool<K, V, E> keyedPool, final float factor) {
             super(keyedPool, null);
             this.factor = factor;
         }
@@ -523,9 +527,9 @@ public final class PoolUtils {
      *
      * @param <K> object pool key type
      * @param <V> object pool value type
+     * @param <E> exception thrown by this pool
      */
-    private static final class KeyedObjectPoolMinIdleTimerTask<K, V> extends
-            TimerTask {
+    private static final class KeyedObjectPoolMinIdleTimerTask<K, V, E extends Exception> extends TimerTask {
 
         /** Minimum number of idle instances. Not the same as pool.getMinIdle(). */
         private final int minIdle;
@@ -534,7 +538,7 @@ public final class PoolUtils {
         private final K key;
 
         /** Keyed object pool */
-        private final KeyedObjectPool<K, V> keyedPool;
+        private final KeyedObjectPool<K, V, E> keyedPool;
 
         /**
          * Creates a new KeyedObjecPoolMinIdleTimerTask.
@@ -548,7 +552,7 @@ public final class PoolUtils {
          * @throws IllegalArgumentException
          *             if the key is null
          */
-        KeyedObjectPoolMinIdleTimerTask(final KeyedObjectPool<K, V> keyedPool,
+        KeyedObjectPoolMinIdleTimerTask(final KeyedObjectPool<K, V, E> keyedPool,
                 final K key, final int minIdle) throws IllegalArgumentException {
             if (keyedPool == null) {
                 throw new IllegalArgumentException(
@@ -602,14 +606,15 @@ public final class PoolUtils {
      * as the pool's minIdle setting.
      *
      * @param <T> type of objects in the pool
+     * @param <E> type of exceptions from the pool
      */
-    private static final class ObjectPoolMinIdleTimerTask<T> extends TimerTask {
+    private static final class ObjectPoolMinIdleTimerTask<T, E extends Exception> extends TimerTask {
 
         /** Minimum number of idle instances. Not the same as pool.getMinIdle(). */
         private final int minIdle;
 
         /** Object pool */
-        private final ObjectPool<T> pool;
+        private final ObjectPool<T, E> pool;
 
         /**
          * Constructs a new ObjectPoolMinIdleTimerTask for the given pool with the
@@ -622,7 +627,7 @@ public final class PoolUtils {
          * @throws IllegalArgumentException
          *             if the pool is null
          */
-        ObjectPoolMinIdleTimerTask(final ObjectPool<T> pool, final int minIdle)
+        ObjectPoolMinIdleTimerTask(final ObjectPool<T, E> pool, final int minIdle)
                 throws IllegalArgumentException {
             if (pool == null) {
                 throw new IllegalArgumentException(MSG_NULL_POOL);
@@ -681,9 +686,9 @@ public final class PoolUtils {
      *
      * @param <K> object pool key type
      * @param <V> object pool value type
+     * @param <E> exception thrown by this pool
      */
-    private static final class SynchronizedKeyedObjectPool<K, V> implements
-            KeyedObjectPool<K, V> {
+    static final class SynchronizedKeyedObjectPool<K, V, E extends Exception> implements KeyedObjectPool<K, V, E> {
 
         /**
          * Object whose monitor is used to synchronize methods on the wrapped
@@ -692,7 +697,7 @@ public final class PoolUtils {
         private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
 
         /** Underlying object pool */
-        private final KeyedObjectPool<K, V> keyedPool;
+        private final KeyedObjectPool<K, V, E> keyedPool;
 
         /**
          * Creates a new SynchronizedKeyedObjectPool wrapping the given pool
@@ -702,7 +707,7 @@ public final class PoolUtils {
          * @throws IllegalArgumentException
          *             if keyedPool is null
          */
-        SynchronizedKeyedObjectPool(final KeyedObjectPool<K, V> keyedPool)
+        SynchronizedKeyedObjectPool(final KeyedObjectPool<K, V, E> keyedPool)
                 throws IllegalArgumentException {
             if (keyedPool == null) {
                 throw new IllegalArgumentException(
@@ -715,8 +720,7 @@ public final class PoolUtils {
          * {@inheritDoc}
          */
         @Override
-        public void addObject(final K key) throws Exception,
-                IllegalStateException, UnsupportedOperationException {
+        public void addObject(final K key) throws E, IllegalStateException, UnsupportedOperationException {
             final WriteLock writeLock = readWriteLock.writeLock();
             writeLock.lock();
             try {
@@ -730,8 +734,7 @@ public final class PoolUtils {
          * {@inheritDoc}
          */
         @Override
-        public V borrowObject(final K key) throws Exception,
-                NoSuchElementException, IllegalStateException {
+        public V borrowObject(final K key) throws E, NoSuchElementException, IllegalStateException {
             final WriteLock writeLock = readWriteLock.writeLock();
             writeLock.lock();
             try {
@@ -745,7 +748,7 @@ public final class PoolUtils {
          * {@inheritDoc}
          */
         @Override
-        public void clear() throws Exception, UnsupportedOperationException {
+        public void clear() throws E, UnsupportedOperationException {
             final WriteLock writeLock = readWriteLock.writeLock();
             writeLock.lock();
             try {
@@ -759,8 +762,7 @@ public final class PoolUtils {
          * {@inheritDoc}
          */
         @Override
-        public void clear(final K key) throws Exception,
-                UnsupportedOperationException {
+        public void clear(final K key) throws E, UnsupportedOperationException {
             final WriteLock writeLock = readWriteLock.writeLock();
             writeLock.lock();
             try {
@@ -786,6 +788,20 @@ public final class PoolUtils {
             }
         }
 
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public List<K> getKeys() {
+            final ReadLock readLock = readWriteLock.readLock();
+            readLock.lock();
+            try {
+                return keyedPool.getKeys();
+            } finally {
+                readLock.unlock();
+            }
+        }
+
         /**
          * {@inheritDoc}
          */
@@ -898,16 +914,16 @@ public final class PoolUtils {
      * </p>
      *
      * @param <K> pooled object factory key type
-     * @param <V> pooled object factory key value
+     * @param <V> pooled object factory value type
+     * @param <E> pooled object factory exception type
      */
-    private static final class SynchronizedKeyedPooledObjectFactory<K, V>
-            implements KeyedPooledObjectFactory<K, V> {
+    private static final class SynchronizedKeyedPooledObjectFactory<K, V, E extends Exception> implements KeyedPooledObjectFactory<K, V, E> {
 
         /** Synchronization lock */
         private final WriteLock writeLock = new ReentrantReadWriteLock().writeLock();
 
         /** Wrapped factory */
-        private final KeyedPooledObjectFactory<K, V> keyedFactory;
+        private final KeyedPooledObjectFactory<K, V, E> keyedFactory;
 
         /**
          * Creates a SynchronizedKeyedPoolableObjectFactory wrapping the given
@@ -918,9 +934,7 @@ public final class PoolUtils {
          * @throws IllegalArgumentException
          *             if the factory is null
          */
-        SynchronizedKeyedPooledObjectFactory(
-                final KeyedPooledObjectFactory<K, V> keyedFactory)
-                throws IllegalArgumentException {
+        SynchronizedKeyedPooledObjectFactory(final KeyedPooledObjectFactory<K, V, E> keyedFactory) throws IllegalArgumentException {
             if (keyedFactory == null) {
                 throw new IllegalArgumentException(
                         "keyedFactory must not be null.");
@@ -932,7 +946,7 @@ public final class PoolUtils {
          * {@inheritDoc}
          */
         @Override
-        public void activateObject(final K key, final PooledObject<V> p) throws Exception {
+        public void activateObject(final K key, final PooledObject<V> p) throws E {
             writeLock.lock();
             try {
                 keyedFactory.activateObject(key, p);
@@ -945,7 +959,7 @@ public final class PoolUtils {
          * {@inheritDoc}
          */
         @Override
-        public void destroyObject(final K key, final PooledObject<V> p) throws Exception {
+        public void destroyObject(final K key, final PooledObject<V> p) throws E {
             writeLock.lock();
             try {
                 keyedFactory.destroyObject(key, p);
@@ -958,7 +972,7 @@ public final class PoolUtils {
          * {@inheritDoc}
          */
         @Override
-        public PooledObject<V> makeObject(final K key) throws Exception {
+        public PooledObject<V> makeObject(final K key) throws E {
             writeLock.lock();
             try {
                 return keyedFactory.makeObject(key);
@@ -971,7 +985,7 @@ public final class PoolUtils {
          * {@inheritDoc}
          */
         @Override
-        public void passivateObject(final K key, final PooledObject<V> p) throws Exception {
+        public void passivateObject(final K key, final PooledObject<V> p) throws E {
             writeLock.lock();
             try {
                 keyedFactory.passivateObject(key, p);
@@ -1019,8 +1033,9 @@ public final class PoolUtils {
      * </p>
      *
      * @param <T> type of objects in the pool
+     * @param <E> type of exceptions from the pool
      */
-    private static final class SynchronizedObjectPool<T> implements ObjectPool<T> {
+    private static final class SynchronizedObjectPool<T, E extends Exception> implements ObjectPool<T, E> {
 
         /**
          * Object whose monitor is used to synchronize methods on the wrapped
@@ -1029,7 +1044,7 @@ public final class PoolUtils {
         private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
 
         /** the underlying object pool */
-        private final ObjectPool<T> pool;
+        private final ObjectPool<T, E> pool;
 
         /**
          * Creates a new SynchronizedObjectPool wrapping the given pool.
@@ -1040,7 +1055,7 @@ public final class PoolUtils {
          * @throws IllegalArgumentException
          *             if the pool is null
          */
-        SynchronizedObjectPool(final ObjectPool<T> pool)
+        SynchronizedObjectPool(final ObjectPool<T, E> pool)
                 throws IllegalArgumentException {
             if (pool == null) {
                 throw new IllegalArgumentException(MSG_NULL_POOL);
@@ -1052,8 +1067,7 @@ public final class PoolUtils {
          * {@inheritDoc}
          */
         @Override
-        public void addObject() throws Exception, IllegalStateException,
-                UnsupportedOperationException {
+        public void addObject() throws E, IllegalStateException, UnsupportedOperationException {
             final WriteLock writeLock = readWriteLock.writeLock();
             writeLock.lock();
             try {
@@ -1067,8 +1081,7 @@ public final class PoolUtils {
          * {@inheritDoc}
          */
         @Override
-        public T borrowObject() throws Exception, NoSuchElementException,
-                IllegalStateException {
+        public T borrowObject() throws E, NoSuchElementException, IllegalStateException {
             final WriteLock writeLock = readWriteLock.writeLock();
             writeLock.lock();
             try {
@@ -1082,7 +1095,7 @@ public final class PoolUtils {
          * {@inheritDoc}
          */
         @Override
-        public void clear() throws Exception, UnsupportedOperationException {
+        public void clear() throws E, UnsupportedOperationException {
             final WriteLock writeLock = readWriteLock.writeLock();
             writeLock.lock();
             try {
@@ -1192,15 +1205,16 @@ public final class PoolUtils {
      * </p>
      *
      * @param <T> pooled object factory type
+     * @param <E> exception type
      */
-    private static final class SynchronizedPooledObjectFactory<T> implements
-            PooledObjectFactory<T> {
+    private static final class SynchronizedPooledObjectFactory<T, E extends Exception> implements
+            PooledObjectFactory<T, E> {
 
         /** Synchronization lock */
         private final WriteLock writeLock = new ReentrantReadWriteLock().writeLock();
 
         /** Wrapped factory */
-        private final PooledObjectFactory<T> factory;
+        private final PooledObjectFactory<T, E> factory;
 
         /**
          * Creates a SynchronizedPoolableObjectFactory wrapping the given
@@ -1211,7 +1225,7 @@ public final class PoolUtils {
          * @throws IllegalArgumentException
          *             if the factory is null
          */
-        SynchronizedPooledObjectFactory(final PooledObjectFactory<T> factory)
+        SynchronizedPooledObjectFactory(final PooledObjectFactory<T, E> factory)
                 throws IllegalArgumentException {
             if (factory == null) {
                 throw new IllegalArgumentException("factory must not be null.");
@@ -1223,7 +1237,7 @@ public final class PoolUtils {
          * {@inheritDoc}
          */
         @Override
-        public void activateObject(final PooledObject<T> p) throws Exception {
+        public void activateObject(final PooledObject<T> p) throws E {
             writeLock.lock();
             try {
                 factory.activateObject(p);
@@ -1236,7 +1250,7 @@ public final class PoolUtils {
          * {@inheritDoc}
          */
         @Override
-        public void destroyObject(final PooledObject<T> p) throws Exception {
+        public void destroyObject(final PooledObject<T> p) throws E {
             writeLock.lock();
             try {
                 factory.destroyObject(p);
@@ -1249,7 +1263,7 @@ public final class PoolUtils {
          * {@inheritDoc}
          */
         @Override
-        public PooledObject<T> makeObject() throws Exception {
+        public PooledObject<T> makeObject() throws E {
             writeLock.lock();
             try {
                 return factory.makeObject();
@@ -1262,7 +1276,7 @@ public final class PoolUtils {
          * {@inheritDoc}
          */
         @Override
-        public void passivateObject(final PooledObject<T> p) throws Exception {
+        public void passivateObject(final PooledObject<T> p) throws E {
             writeLock.lock();
             try {
                 factory.passivateObject(p);
@@ -1334,6 +1348,7 @@ public final class PoolUtils {
      *            keyedPool, see {@link Timer#schedule(TimerTask, long, long)}.
      * @param <K> the type of the pool key
      * @param <V> the type of pool entries
+     * @param <E> the type of exception thrown by a pool
      * @return a {@link Map} of key and {@link TimerTask} pairs that will
      *         periodically check the pools idle object count.
      * @throws IllegalArgumentException
@@ -1343,17 +1358,15 @@ public final class PoolUtils {
      *             {@link Timer#schedule(TimerTask, long, long)}.
      * @see #checkMinIdle(KeyedObjectPool, Object, int, long)
      */
-    public static <K, V> Map<K, TimerTask> checkMinIdle(
-            final KeyedObjectPool<K, V> keyedPool, final Collection<K> keys,
+    public static <K, V, E extends Exception> Map<K, TimerTask> checkMinIdle(
+            final KeyedObjectPool<K, V, E> keyedPool, final Collection<K> keys,
             final int minIdle, final long periodMillis)
             throws IllegalArgumentException {
         if (keys == null) {
             throw new IllegalArgumentException(MSG_NULL_KEYS);
         }
         final Map<K, TimerTask> tasks = new HashMap<>(keys.size());
-        final Iterator<K> iter = keys.iterator();
-        while (iter.hasNext()) {
-            final K key = iter.next();
+        for (final K key : keys) {
             final TimerTask task = checkMinIdle(keyedPool, key, minIdle, periodMillis);
             tasks.put(key, task);
         }
@@ -1378,6 +1391,7 @@ public final class PoolUtils {
      *            keyedPool, see {@link Timer#schedule(TimerTask, long, long)}.
      * @param <K> the type of the pool key
      * @param <V> the type of pool entries
+     * @param <E> the type of exception thrown by a pool
      * @return the {@link TimerTask} that will periodically check the pools idle
      *         object count.
      * @throws IllegalArgumentException
@@ -1385,8 +1399,8 @@ public final class PoolUtils {
      *             when {@code minIdle} is negative or when {@code period} isn't
      *             valid for {@link Timer#schedule(TimerTask, long, long)}.
      */
-    public static <K, V> TimerTask checkMinIdle(
-            final KeyedObjectPool<K, V> keyedPool, final K key,
+    public static <K, V, E extends Exception> TimerTask checkMinIdle(
+            final KeyedObjectPool<K, V, E> keyedPool, final K key,
             final int minIdle, final long periodMillis)
             throws IllegalArgumentException {
         if (keyedPool == null) {
@@ -1418,6 +1432,7 @@ public final class PoolUtils {
      *            the frequency in milliseconds to check the number of idle objects in a pool,
      *            see {@link Timer#schedule(TimerTask, long, long)}.
      * @param <T> the type of objects in the pool
+     * @param <E> type of exceptions from the pool
      * @return the {@link TimerTask} that will periodically check the pools idle
      *         object count.
      * @throws IllegalArgumentException
@@ -1425,7 +1440,7 @@ public final class PoolUtils {
      *             negative or when {@code period} isn't valid for
      *             {@link Timer#schedule(TimerTask, long, long)}
      */
-    public static <T> TimerTask checkMinIdle(final ObjectPool<T> pool,
+    public static <T, E extends Exception> TimerTask checkMinIdle(final ObjectPool<T, E> pool,
             final int minIdle, final long periodMillis)
             throws IllegalArgumentException {
         if (pool == null) {
@@ -1474,6 +1489,7 @@ public final class PoolUtils {
      *            count when possible.
      * @param <K> the type of the pool key
      * @param <V> the type of pool entries
+     * @param <E> the type of exception thrown by a pool
      * @throws IllegalArgumentException
      *             when {@code keyedPool} is {@code null}.
      * @return a pool that adaptively decreases its size when idle objects are
@@ -1481,8 +1497,7 @@ public final class PoolUtils {
      * @see #erodingPool(KeyedObjectPool, float)
      * @see #erodingPool(KeyedObjectPool, float, boolean)
      */
-    public static <K, V> KeyedObjectPool<K, V> erodingPool(
-            final KeyedObjectPool<K, V> keyedPool) {
+    public static <K, V, E extends Exception> KeyedObjectPool<K, V, E> erodingPool(final KeyedObjectPool<K, V, E> keyedPool) {
         return erodingPool(keyedPool, 1f);
     }
 
@@ -1509,6 +1524,7 @@ public final class PoolUtils {
      *            shrinks less aggressively.
      * @param <K> the type of the pool key
      * @param <V> the type of pool entries
+     * @param <E> the type of exception thrown by a pool
      * @throws IllegalArgumentException
      *             when {@code keyedPool} is {@code null} or when {@code factor}
      *             is not positive.
@@ -1516,8 +1532,7 @@ public final class PoolUtils {
      *         no longer needed.
      * @see #erodingPool(KeyedObjectPool, float, boolean)
      */
-    public static <K, V> KeyedObjectPool<K, V> erodingPool(
-            final KeyedObjectPool<K, V> keyedPool, final float factor) {
+    public static <K, V, E extends Exception> KeyedObjectPool<K, V, E> erodingPool(final KeyedObjectPool<K, V, E> keyedPool, final float factor) {
         return erodingPool(keyedPool, factor, false);
     }
 
@@ -1552,6 +1567,7 @@ public final class PoolUtils {
      *            when true, each key is treated independently.
      * @param <K> the type of the pool key
      * @param <V> the type of pool entries
+     * @param <E> the type of exception thrown by a pool
      * @throws IllegalArgumentException
      *             when {@code keyedPool} is {@code null} or when {@code factor}
      *             is not positive.
@@ -1560,8 +1576,8 @@ public final class PoolUtils {
      * @see #erodingPool(KeyedObjectPool)
      * @see #erodingPool(KeyedObjectPool, float)
      */
-    public static <K, V> KeyedObjectPool<K, V> erodingPool(
-            final KeyedObjectPool<K, V> keyedPool, final float factor,
+    public static <K, V, E extends Exception> KeyedObjectPool<K, V, E> erodingPool(
+            final KeyedObjectPool<K, V, E> keyedPool, final float factor,
             final boolean perKey) {
         if (keyedPool == null) {
             throw new IllegalArgumentException(MSG_NULL_KEYED_POOL);
@@ -1586,13 +1602,14 @@ public final class PoolUtils {
      *            the ObjectPool to be decorated so it shrinks its idle count
      *            when possible.
      * @param <T> the type of objects in the pool
+     * @param <E> type of exceptions from the pool
      * @throws IllegalArgumentException
      *             when {@code pool} is {@code null}.
      * @return a pool that adaptively decreases its size when idle objects are
      *         no longer needed.
      * @see #erodingPool(ObjectPool, float)
      */
-    public static <T> ObjectPool<T> erodingPool(final ObjectPool<T> pool) {
+    public static <T, E extends Exception> ObjectPool<T, E> erodingPool(final ObjectPool<T, E> pool) {
         return erodingPool(pool, 1f);
     }
 
@@ -1618,6 +1635,7 @@ public final class PoolUtils {
      *            shrinks more aggressively. If 1 &lt; factor then the pool
      *            shrinks less aggressively.
      * @param <T> the type of objects in the pool
+     * @param <E> type of exceptions from the pool
      * @throws IllegalArgumentException
      *             when {@code pool} is {@code null} or when {@code factor} is
      *             not positive.
@@ -1625,8 +1643,7 @@ public final class PoolUtils {
      *         no longer needed.
      * @see #erodingPool(ObjectPool)
      */
-    public static <T> ObjectPool<T> erodingPool(final ObjectPool<T> pool,
-            final float factor) {
+    public static <T, E extends Exception> ObjectPool<T, E> erodingPool(final ObjectPool<T, E> pool, final float factor) {
         if (pool == null) {
             throw new IllegalArgumentException(MSG_NULL_POOL);
         }
@@ -1659,7 +1676,8 @@ public final class PoolUtils {
      *            the number of idle objects to add for each {@code key}.
      * @param <K> the type of the pool key
      * @param <V> the type of pool entries
-     * @throws Exception
+     * @param <E> the type of exception thrown by a pool
+     * @throws E
      *             when {@link KeyedObjectPool#addObject(Object)} fails.
      * @throws IllegalArgumentException
      *             when {@code keyedPool}, {@code keys}, or any value in
@@ -1668,8 +1686,8 @@ public final class PoolUtils {
      * @deprecated Use {@link KeyedObjectPool#addObjects(Collection, int)}.
      */
     @Deprecated
-    public static <K, V> void prefill(final KeyedObjectPool<K, V> keyedPool,
-            final Collection<K> keys, final int count) throws Exception,
+    public static <K, V, E extends Exception> void prefill(final KeyedObjectPool<K, V, E> keyedPool,
+            final Collection<K> keys, final int count) throws E,
             IllegalArgumentException {
         if (keys == null) {
             throw new IllegalArgumentException(MSG_NULL_KEYS);
@@ -1689,15 +1707,16 @@ public final class PoolUtils {
      *            the number of idle objects to add for {@code key}.
      * @param <K> the type of the pool key
      * @param <V> the type of pool entries
-     * @throws Exception
+     * @param <E> the type of exception thrown by a pool
+     * @throws E
      *             when {@link KeyedObjectPool#addObject(Object)} fails.
      * @throws IllegalArgumentException
      *             when {@code keyedPool} or {@code key} is {@code null}.
      * @deprecated Use {@link KeyedObjectPool#addObjects(Object, int)}.
      */
     @Deprecated
-    public static <K, V> void prefill(final KeyedObjectPool<K, V> keyedPool,
-            final K key, final int count) throws Exception,
+    public static <K, V, E extends Exception> void prefill(final KeyedObjectPool<K, V, E> keyedPool,
+            final K key, final int count) throws E,
             IllegalArgumentException {
         if (keyedPool == null) {
             throw new IllegalArgumentException(MSG_NULL_KEYED_POOL);
@@ -1714,15 +1733,16 @@ public final class PoolUtils {
      * @param count
      *            the number of idle objects to add.
      * @param <T> the type of objects in the pool
-     * @throws Exception
+     * @param <E> type of exceptions from the pool
+     * @throws E
      *             when {@link ObjectPool#addObject()} fails.
      * @throws IllegalArgumentException
      *             when {@code pool} is {@code null}.
      * @deprecated Use {@link ObjectPool#addObjects(int)}.
      */
     @Deprecated
-    public static <T> void prefill(final ObjectPool<T> pool, final int count)
-            throws Exception, IllegalArgumentException {
+    public static <T, E extends Exception> void prefill(final ObjectPool<T, E> pool, final int count)
+            throws E, IllegalArgumentException {
         if (pool == null) {
             throw new IllegalArgumentException(MSG_NULL_POOL);
         }
@@ -1738,10 +1758,11 @@ public final class PoolUtils {
      *            synchronized KeyedPooledObjectFactory.
      * @param <K> the type of the pool key
      * @param <V> the type of pool entries
+     * @param <E> the type of pool exceptions
      * @return a synchronized view of the specified KeyedPooledObjectFactory.
      */
-    public static <K, V> KeyedPooledObjectFactory<K, V> synchronizedKeyedPooledFactory(
-            final KeyedPooledObjectFactory<K, V> keyedFactory) {
+    public static <K, V, E extends Exception> KeyedPooledObjectFactory<K, V, E> synchronizedKeyedPooledFactory(
+        final KeyedPooledObjectFactory<K, V, E> keyedFactory) {
         return new SynchronizedKeyedPooledObjectFactory<>(keyedFactory);
     }
 
@@ -1762,10 +1783,10 @@ public final class PoolUtils {
      *            KeyedObjectPool.
      * @param <K> the type of the pool key
      * @param <V> the type of pool entries
+     * @param <E> the type of exception thrown by a pool
      * @return a synchronized view of the specified KeyedObjectPool.
      */
-    public static <K, V> KeyedObjectPool<K, V> synchronizedPool(
-            final KeyedObjectPool<K, V> keyedPool) {
+    public static <K, V, E extends Exception> KeyedObjectPool<K, V, E> synchronizedPool(final KeyedObjectPool<K, V, E> keyedPool) {
         /*
          * assert !(keyedPool instanceof GenericKeyedObjectPool) :
          * "GenericKeyedObjectPool is already thread-safe"; assert !(keyedPool
@@ -1790,14 +1811,15 @@ public final class PoolUtils {
      * deadlock.
      * </p>
      *
+     * @param <T> the type of objects in the pool
+     * @param <E> the type of exceptions thrown by the pool
      * @param pool
      *            the ObjectPool to be "wrapped" in a synchronized ObjectPool.
-     * @param <T> the type of objects in the pool
      * @throws IllegalArgumentException
      *             when {@code pool} is {@code null}.
      * @return a synchronized view of the specified ObjectPool.
      */
-    public static <T> ObjectPool<T> synchronizedPool(final ObjectPool<T> pool) {
+    public static <T, E extends Exception> ObjectPool<T, E> synchronizedPool(final ObjectPool<T, E> pool) {
         if (pool == null) {
             throw new IllegalArgumentException(MSG_NULL_POOL);
         }
@@ -1824,10 +1846,10 @@ public final class PoolUtils {
      *            the PooledObjectFactory to be "wrapped" in a synchronized
      *            PooledObjectFactory.
      * @param <T> the type of objects in the pool
+     * @param <E> the type of exceptions thrown by the pool
      * @return a synchronized view of the specified PooledObjectFactory.
      */
-    public static <T> PooledObjectFactory<T> synchronizedPooledFactory(
-            final PooledObjectFactory<T> factory) {
+    public static <T, E extends Exception> PooledObjectFactory<T, E> synchronizedPooledFactory(final PooledObjectFactory<T, E> factory) {
         return new SynchronizedPooledObjectFactory<>(factory);
     }
 
diff --git a/src/main/java/org/apache/commons/pool2/PooledObject.java b/src/main/java/org/apache/commons/pool2/PooledObject.java
index ae55dad..ac1d4b3 100644
--- a/src/main/java/org/apache/commons/pool2/PooledObject.java
+++ b/src/main/java/org/apache/commons/pool2/PooledObject.java
@@ -148,6 +148,16 @@ public interface PooledObject<T> extends Comparable<PooledObject<T>> {
     @Deprecated
     long getCreateTime();
 
+    /**
+     * Computes the duration since this object was created (using {@link Instant#now()}).
+     *
+     * @return The duration since this object was created.
+     * @since 2.12.0
+     */
+    default Duration getFullDuration() {
+        return Duration.between(getCreateInstant(), Instant.now());
+    }
+
     /**
      * Gets the amount of time that this object last spend in the
      * idle state (it may still be idle in which case subsequent calls will
diff --git a/src/main/java/org/apache/commons/pool2/PooledObjectFactory.java b/src/main/java/org/apache/commons/pool2/PooledObjectFactory.java
index 839e866..a302a28 100644
--- a/src/main/java/org/apache/commons/pool2/PooledObjectFactory.java
+++ b/src/main/java/org/apache/commons/pool2/PooledObjectFactory.java
@@ -65,24 +65,25 @@ package org.apache.commons.pool2;
  * </p>
  *
  * @param <T> Type of element managed in this factory.
+ * @param <E> Type of exception thrown in this factory.
  *
  * @see ObjectPool
  *
  * @since 2.0
  */
-public interface PooledObjectFactory<T> {
+public interface PooledObjectFactory<T, E extends Exception> {
 
   /**
    * Reinitializes an instance to be returned by the pool.
    *
    * @param p a {@code PooledObject} wrapping the instance to be activated
    *
-   * @throws Exception if there is a problem activating {@code obj},
+   * @throws E if there is a problem activating {@code obj},
    *    this exception may be swallowed by the pool.
    *
    * @see #destroyObject
    */
-  void activateObject(PooledObject<T> p) throws Exception;
+  void activateObject(PooledObject<T> p) throws E;
 
   /**
    * Destroys an instance no longer needed by the pool, using the default (NORMAL)
@@ -99,13 +100,13 @@ public interface PooledObjectFactory<T> {
    *
    * @param p a {@code PooledObject} wrapping the instance to be destroyed
    *
-   * @throws Exception should be avoided as it may be swallowed by
+   * @throws E should be avoided as it may be swallowed by
    *    the pool implementation.
    *
    * @see #validateObject
    * @see ObjectPool#invalidateObject
    */
-  void destroyObject(PooledObject<T> p) throws Exception;
+  void destroyObject(PooledObject<T> p) throws E;
 
   /**
    * Destroys an instance no longer needed by the pool, using the provided
@@ -114,7 +115,7 @@ public interface PooledObjectFactory<T> {
    * @param p a {@code PooledObject} wrapping the instance to be destroyed
    * @param destroyMode DestroyMode providing context to the factory
    *
-   * @throws Exception should be avoided as it may be swallowed by
+   * @throws E should be avoided as it may be swallowed by
    *    the pool implementation.
    *
    * @see #validateObject
@@ -123,7 +124,7 @@ public interface PooledObjectFactory<T> {
    * @see DestroyMode
    * @since 2.9.0
    */
-  default void destroyObject(final PooledObject<T> p, final DestroyMode destroyMode) throws Exception {
+  default void destroyObject(final PooledObject<T> p, final DestroyMode destroyMode) throws E {
       destroyObject(p);
   }
 
@@ -133,22 +134,22 @@ public interface PooledObjectFactory<T> {
    *
    * @return a {@code PooledObject} wrapping an instance that can be served by the pool
    *
-   * @throws Exception if there is a problem creating a new instance,
+   * @throws E if there is a problem creating a new instance,
    *    this will be propagated to the code requesting an object.
    */
-  PooledObject<T> makeObject() throws Exception;
+  PooledObject<T> makeObject() throws E;
 
   /**
    * Uninitializes an instance to be returned to the idle object pool.
    *
    * @param p a {@code PooledObject} wrapping the instance to be passivated
    *
-   * @throws Exception if there is a problem passivating {@code obj},
+   * @throws E if there is a problem passivating {@code obj},
    *    this exception may be swallowed by the pool.
    *
    * @see #destroyObject
    */
-  void passivateObject(PooledObject<T> p) throws Exception;
+  void passivateObject(PooledObject<T> p) throws E;
 
   /**
    * Ensures that the instance is safe to be returned by the pool.
diff --git a/src/main/java/org/apache/commons/pool2/impl/AbandonedConfig.java b/src/main/java/org/apache/commons/pool2/impl/AbandonedConfig.java
index ef728db..5b86a15 100644
--- a/src/main/java/org/apache/commons/pool2/impl/AbandonedConfig.java
+++ b/src/main/java/org/apache/commons/pool2/impl/AbandonedConfig.java
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package org.apache.commons.pool2.impl;
 
 import java.io.OutputStreamWriter;
diff --git a/src/main/java/org/apache/commons/pool2/impl/BaseGenericObjectPool.java b/src/main/java/org/apache/commons/pool2/impl/BaseGenericObjectPool.java
index ad864ae..d23b030 100644
--- a/src/main/java/org/apache/commons/pool2/impl/BaseGenericObjectPool.java
+++ b/src/main/java/org/apache/commons/pool2/impl/BaseGenericObjectPool.java
@@ -55,12 +55,13 @@ import org.apache.commons.pool2.SwallowedExceptionListener;
  * reduce code duplication between the two pool implementations.
  *
  * @param <T> Type of element pooled in this pool.
+ * @param <E> Type of exception thrown in this pool.
  *
  * This class is intended to be thread-safe.
  *
  * @since 2.0
  */
-public abstract class BaseGenericObjectPool<T> extends BaseObject {
+public abstract class BaseGenericObjectPool<T, E extends Exception> extends BaseObject {
 
     /**
      * The idle object eviction iterator. Holds a reference to the idle objects.
@@ -128,6 +129,9 @@ public abstract class BaseGenericObjectPool<T> extends BaseObject {
             scheduledFuture.cancel(false);
         }
 
+        BaseGenericObjectPool<T, E> owner() {
+            return BaseGenericObjectPool.this;
+        }
 
         /**
          * Run pool maintenance.  Evict objects qualifying for eviction and then
@@ -177,7 +181,6 @@ public abstract class BaseGenericObjectPool<T> extends BaseObject {
             }
         }
 
-
         /**
          * Sets the scheduled future.
          *
@@ -441,6 +444,18 @@ public abstract class BaseGenericObjectPool<T> extends BaseObject {
         }
     }
 
+    /**
+     * Casts the given throwable to E.
+     *
+     * @param throwable the throwable.
+     * @return the input.
+     * @since 2.12.0
+     */
+    @SuppressWarnings("unchecked")
+    protected E cast(final Throwable throwable) {
+        return (E) throwable;
+    }
+
     /**
      * Closes the pool, destroys the remaining idle objects and, if registered
      * in JMX, deregisters it.
@@ -471,9 +486,9 @@ public abstract class BaseGenericObjectPool<T> extends BaseObject {
     /**
      * Tries to ensure that the configured minimum number of idle instances are
      * available in the pool.
-     * @throws Exception if an error occurs creating idle instances
+     * @throws E if an error occurs creating idle instances
      */
-    abstract void ensureMinIdle() throws Exception;
+    abstract void ensureMinIdle() throws E;
 
     /**
      * Perform {@code numTests} idle object eviction tests, evicting
@@ -483,9 +498,9 @@ public abstract class BaseGenericObjectPool<T> extends BaseObject {
      * have been idle for more than {@code minEvicableIdleTimeMillis}
      * are removed.
      *
-     * @throws Exception when there is a problem evicting idle objects.
+     * @throws E when there is a problem evicting idle objects.
      */
-    public abstract void evict() throws Exception;
+    public abstract void evict() throws E;
 
     /**
      * Gets whether to block when the {@code borrowObject()} method is
@@ -559,6 +574,20 @@ public abstract class BaseGenericObjectPool<T> extends BaseObject {
         return destroyedCount.get();
     }
 
+    /**
+     * Gets the duration to sleep between runs of the idle
+     * object evictor thread. When non-positive, no idle object evictor thread
+     * will be run.
+     *
+     * @return number of milliseconds to sleep between evictor runs
+     *
+     * @see #setTimeBetweenEvictionRuns
+     * @since 2.11.0
+     */
+    public final Duration getDurationBetweenEvictionRuns() {
+        return durationBetweenEvictionRuns;
+    }
+
     /**
      * Gets the {@link EvictionPolicy} defined for this pool.
      *
@@ -1104,20 +1133,6 @@ public abstract class BaseGenericObjectPool<T> extends BaseObject {
         return testWhileIdle;
     }
 
-    /**
-     * Gets the duration to sleep between runs of the idle
-     * object evictor thread. When non-positive, no idle object evictor thread
-     * will be run.
-     *
-     * @return number of milliseconds to sleep between evictor runs
-     *
-     * @see #setTimeBetweenEvictionRuns
-     * @since 2.11.0
-     */
-    public final Duration getDurationBetweenEvictionRuns() {
-        return durationBetweenEvictionRuns;
-    }
-
     /**
      * Gets the duration to sleep between runs of the idle
      * object evictor thread. When non-positive, no idle object evictor thread
@@ -1325,6 +1340,20 @@ public abstract class BaseGenericObjectPool<T> extends BaseObject {
      *
      * @param className Eviction policy class name.
      * @param classLoader Load the class from this class loader.
+     * @throws LinkageError if the linkage fails
+     * @throws ExceptionInInitializerError if the initialization provoked by this method fails
+     * @throws ClassNotFoundException if the class cannot be located by the specified class loader
+     * @throws IllegalAccessException if this {@code Constructor} object is enforcing Java language access control and the underlying constructor is
+     *         inaccessible.
+     * @throws IllegalArgumentException if the number of actual and formal parameters differ; if an unwrapping conversion for primitive arguments fails; or if,
+     *         after possible unwrapping, a parameter value cannot be converted to the corresponding formal parameter type by a method invocation conversion; if
+     *         this constructor pertains to an enum type.
+     * @throws InstantiationException if the class that declares the underlying constructor represents an abstract class.
+     * @throws InvocationTargetException if the underlying constructor throws an exception.
+     * @throws ExceptionInInitializerError if the initialization provoked by this method fails.
+     * @throws NoSuchMethodException if a matching method is not found.
+     * @throws SecurityException If a security manage is present and the caller's class loader is not the same as or an ancestor of the class loader for the
+     *         current class and invocation of {@link SecurityManager#checkPackageAccess s.checkPackageAccess()} denies access to the package of this class.
      */
     @SuppressWarnings("unchecked")
     private void setEvictionPolicy(final String className, final ClassLoader classLoader)
@@ -1736,7 +1765,7 @@ public abstract class BaseGenericObjectPool<T> extends BaseObject {
      * @param timeBetweenEvictionRuns
      *            duration to sleep between evictor runs
      *
-     * @see #getTimeBetweenEvictionRunsMillis
+     * @see #getDurationBetweenEvictionRuns()
      * @since 2.10.0
      */
     public final void setTimeBetweenEvictionRuns(final Duration timeBetweenEvictionRuns) {
@@ -1754,7 +1783,7 @@ public abstract class BaseGenericObjectPool<T> extends BaseObject {
      * @param timeBetweenEvictionRunsMillis
      *            number of milliseconds to sleep between evictor runs
      *
-     * @see #getTimeBetweenEvictionRunsMillis
+     * @see #getDurationBetweenEvictionRuns()
      * @deprecated Use {@link #setTimeBetweenEvictionRuns(Duration)}.
      */
     @Deprecated
diff --git a/src/main/java/org/apache/commons/pool2/impl/BaseObjectPoolConfig.java b/src/main/java/org/apache/commons/pool2/impl/BaseObjectPoolConfig.java
index d06f380..0811054 100644
--- a/src/main/java/org/apache/commons/pool2/impl/BaseObjectPoolConfig.java
+++ b/src/main/java/org/apache/commons/pool2/impl/BaseObjectPoolConfig.java
@@ -296,6 +296,21 @@ public abstract class BaseObjectPoolConfig<T> extends BaseObject implements Clon
         return blockWhenExhausted;
     }
 
+    /**
+     * Gets the value for the {@code timeBetweenEvictionRuns} configuration
+     * attribute for pools created with this configuration instance.
+     *
+     * @return  The current setting of {@code timeBetweenEvictionRuns} for
+     *          this configuration instance
+     *
+     * @see GenericObjectPool#getDurationBetweenEvictionRuns()
+     * @see GenericKeyedObjectPool#getDurationBetweenEvictionRuns()
+     * @since 2.11.0
+     */
+    public Duration getDurationBetweenEvictionRuns() {
+        return durationBetweenEvictionRuns;
+    }
+
     /**
      * Gets the value for the {@code evictionPolicyClass} configuration
      * attribute for pools created with this configuration instance.
@@ -639,21 +654,6 @@ public abstract class BaseObjectPoolConfig<T> extends BaseObject implements Clon
         return testWhileIdle;
     }
 
-    /**
-     * Gets the value for the {@code timeBetweenEvictionRuns} configuration
-     * attribute for pools created with this configuration instance.
-     *
-     * @return  The current setting of {@code timeBetweenEvictionRuns} for
-     *          this configuration instance
-     *
-     * @see GenericObjectPool#getDurationBetweenEvictionRuns()
-     * @see GenericKeyedObjectPool#getDurationBetweenEvictionRuns()
-     * @since 2.11.0
-     */
-    public Duration getDurationBetweenEvictionRuns() {
-        return durationBetweenEvictionRuns;
-    }
-
     /**
      * Gets the value for the {@code timeBetweenEvictionRuns} configuration
      * attribute for pools created with this configuration instance.
@@ -734,34 +734,34 @@ public abstract class BaseObjectPoolConfig<T> extends BaseObject implements Clon
      * Sets the value for the {@code evictorShutdownTimeout} configuration
      * attribute for pools created with this configuration instance.
      *
-     * @param evictorShutdownTimeout The new setting of
+     * @param evictorShutdownTimeoutDuration The new setting of
      *        {@code evictorShutdownTimeout} for this configuration
      *        instance
      *
      * @see GenericObjectPool#getEvictorShutdownTimeoutDuration()
      * @see GenericKeyedObjectPool#getEvictorShutdownTimeoutDuration()
-     * @since 2.10.0
-     * @deprecated Use {@link #setEvictorShutdownTimeout(Duration)}.
+     * @since 2.11.0
      */
-    @Deprecated
-    public void setEvictorShutdownTimeoutMillis(final Duration evictorShutdownTimeout) {
-        setEvictorShutdownTimeout(evictorShutdownTimeout);
+    public void setEvictorShutdownTimeout(final Duration evictorShutdownTimeoutDuration) {
+        this.evictorShutdownTimeoutDuration = PoolImplUtils.nonNull(evictorShutdownTimeoutDuration, DEFAULT_EVICTOR_SHUTDOWN_TIMEOUT);
     }
 
     /**
      * Sets the value for the {@code evictorShutdownTimeout} configuration
      * attribute for pools created with this configuration instance.
      *
-     * @param evictorShutdownTimeoutDuration The new setting of
+     * @param evictorShutdownTimeout The new setting of
      *        {@code evictorShutdownTimeout} for this configuration
      *        instance
      *
      * @see GenericObjectPool#getEvictorShutdownTimeoutDuration()
      * @see GenericKeyedObjectPool#getEvictorShutdownTimeoutDuration()
-     * @since 2.11.0
+     * @since 2.10.0
+     * @deprecated Use {@link #setEvictorShutdownTimeout(Duration)}.
      */
-    public void setEvictorShutdownTimeout(final Duration evictorShutdownTimeoutDuration) {
-        this.evictorShutdownTimeoutDuration = PoolImplUtils.nonNull(evictorShutdownTimeoutDuration, DEFAULT_EVICTOR_SHUTDOWN_TIMEOUT);
+    @Deprecated
+    public void setEvictorShutdownTimeoutMillis(final Duration evictorShutdownTimeout) {
+        setEvictorShutdownTimeout(evictorShutdownTimeout);
     }
 
     /**
@@ -849,31 +849,31 @@ public abstract class BaseObjectPoolConfig<T> extends BaseObject implements Clon
      * Sets the value for the {@code maxWait} configuration attribute for pools
      * created with this configuration instance.
      *
-     * @param maxWaitMillis The new setting of {@code maxWaitMillis}
+     * @param maxWaitDuration The new setting of {@code maxWaitDuration}
      *        for this configuration instance
      *
      * @see GenericObjectPool#getMaxWaitDuration()
      * @see GenericKeyedObjectPool#getMaxWaitDuration()
-     * @deprecated Use {@link #setMaxWait(Duration)}.
+     * @since 2.11.0
      */
-    @Deprecated
-    public void setMaxWaitMillis(final long maxWaitMillis) {
-        setMaxWait(Duration.ofMillis(maxWaitMillis));
+    public void setMaxWait(final Duration maxWaitDuration) {
+        this.maxWaitDuration = PoolImplUtils.nonNull(maxWaitDuration, DEFAULT_MAX_WAIT);
     }
 
     /**
      * Sets the value for the {@code maxWait} configuration attribute for pools
      * created with this configuration instance.
      *
-     * @param maxWaitDuration The new setting of {@code maxWaitDuration}
+     * @param maxWaitMillis The new setting of {@code maxWaitMillis}
      *        for this configuration instance
      *
      * @see GenericObjectPool#getMaxWaitDuration()
      * @see GenericKeyedObjectPool#getMaxWaitDuration()
-     * @since 2.11.0
+     * @deprecated Use {@link #setMaxWait(Duration)}.
      */
-    public void setMaxWait(final Duration maxWaitDuration) {
-        this.maxWaitDuration = PoolImplUtils.nonNull(maxWaitDuration, DEFAULT_MAX_WAIT);
+    @Deprecated
+    public void setMaxWaitMillis(final long maxWaitMillis) {
+        setMaxWait(Duration.ofMillis(maxWaitMillis));
     }
 
     /**
diff --git a/src/main/java/org/apache/commons/pool2/impl/DefaultPooledObjectInfo.java b/src/main/java/org/apache/commons/pool2/impl/DefaultPooledObjectInfo.java
index 6294484..c284e93 100644
--- a/src/main/java/org/apache/commons/pool2/impl/DefaultPooledObjectInfo.java
+++ b/src/main/java/org/apache/commons/pool2/impl/DefaultPooledObjectInfo.java
@@ -55,7 +55,7 @@ public class DefaultPooledObjectInfo implements DefaultPooledObjectInfoMBean {
 
     @Override
     public String getCreateTimeFormatted() {
-        return getTimeFormatted(getCreateTime());
+        return getTimeMillisFormatted(getCreateTime());
     }
 
     @Override
@@ -66,7 +66,7 @@ public class DefaultPooledObjectInfo implements DefaultPooledObjectInfoMBean {
 
     @Override
     public String getLastBorrowTimeFormatted() {
-        return getTimeFormatted(getLastBorrowTime());
+        return getTimeMillisFormatted(getLastBorrowTime());
     }
 
     @Override
@@ -83,7 +83,7 @@ public class DefaultPooledObjectInfo implements DefaultPooledObjectInfoMBean {
 
     @Override
     public String getLastReturnTimeFormatted() {
-        return getTimeFormatted(getLastReturnTime());
+        return getTimeMillisFormatted(getLastReturnTime());
     }
 
     @Override
@@ -96,7 +96,7 @@ public class DefaultPooledObjectInfo implements DefaultPooledObjectInfoMBean {
         return pooledObject.getObject().getClass().getName();
     }
 
-    private String getTimeFormatted(final long millis) {
+    private String getTimeMillisFormatted(final long millis) {
         return new SimpleDateFormat(PATTERN).format(Long.valueOf(millis));
     }
 
diff --git a/src/main/java/org/apache/commons/pool2/impl/EvictionTimer.java b/src/main/java/org/apache/commons/pool2/impl/EvictionTimer.java
index 80e53ad..24e162d 100644
--- a/src/main/java/org/apache/commons/pool2/impl/EvictionTimer.java
+++ b/src/main/java/org/apache/commons/pool2/impl/EvictionTimer.java
@@ -74,13 +74,14 @@ class EvictionTimer {
         @Override
         public void run() {
             synchronized (EvictionTimer.class) {
-                for (final Entry<WeakReference<Runnable>, WeakRunner> entry : taskMap.entrySet()) {
+                for (final Entry<WeakReference<BaseGenericObjectPool<?, ?>.Evictor>, WeakRunner<BaseGenericObjectPool<?, ?>.Evictor>> entry : TASK_MAP
+                        .entrySet()) {
                     if (entry.getKey().get() == null) {
                         executor.remove(entry.getValue());
-                        taskMap.remove(entry.getKey());
+                        TASK_MAP.remove(entry.getKey());
                     }
                 }
-                if (taskMap.isEmpty() && executor != null) {
+                if (TASK_MAP.isEmpty() && executor != null) {
                     executor.shutdown();
                     executor.setCorePoolSize(0);
                     executor = null;
@@ -92,17 +93,18 @@ class EvictionTimer {
     /**
      * Runnable that runs the referent of a weak reference. When the referent is no
      * no longer reachable, run is no-op.
+     * @param <R> The kind of Runnable.
      */
-    private static class WeakRunner implements Runnable {
+    private static class WeakRunner<R extends Runnable> implements Runnable {
 
-        private final WeakReference<Runnable> ref;
+        private final WeakReference<R> ref;
 
         /**
          * Constructs a new instance to track the given reference.
          *
          * @param ref the reference to track.
          */
-        private WeakRunner(final WeakReference<Runnable> ref) {
+        private WeakRunner(final WeakReference<R> ref) {
            this.ref = ref;
         }
 
@@ -113,7 +115,7 @@ class EvictionTimer {
                 task.run();
             } else {
                 executor.remove(this);
-                taskMap.remove(ref);
+                TASK_MAP.remove(ref);
             }
         }
     }
@@ -123,7 +125,9 @@ class EvictionTimer {
     private static ScheduledThreadPoolExecutor executor; //@GuardedBy("EvictionTimer.class")
 
     /** Keys are weak references to tasks, values are runners managed by executor. */
-    private static final HashMap<WeakReference<Runnable>, WeakRunner> taskMap = new HashMap<>(); // @GuardedBy("EvictionTimer.class")
+    private static final HashMap<
+        WeakReference<BaseGenericObjectPool<?, ?>.Evictor>, 
+        WeakRunner<BaseGenericObjectPool<?, ?>.Evictor>> TASK_MAP = new HashMap<>(); // @GuardedBy("EvictionTimer.class")
 
     /**
      * Removes the specified eviction task from the timer.
@@ -134,13 +138,13 @@ class EvictionTimer {
      *                  terminate?
      * @param restarting The state of the evictor.
      */
-    static synchronized void cancel(final BaseGenericObjectPool<?>.Evictor evictor, final Duration timeout,
+    static synchronized void cancel(final BaseGenericObjectPool<?, ?>.Evictor evictor, final Duration timeout,
             final boolean restarting) {
         if (evictor != null) {
             evictor.cancel();
             remove(evictor);
         }
-        if (!restarting && executor != null && taskMap.isEmpty()) {
+        if (!restarting && executor != null && TASK_MAP.isEmpty()) {
             executor.shutdown();
             try {
                 executor.awaitTermination(timeout.toMillis(), TimeUnit.MILLISECONDS);
@@ -153,11 +157,29 @@ class EvictionTimer {
         }
     }
 
+    /**
+     * For testing only.
+     * 
+     * @return The executor.
+     */
+    static ScheduledThreadPoolExecutor getExecutor() {
+        return executor;
+    }
+
     /**
      * @return the number of eviction tasks under management.
      */
     static synchronized int getNumTasks() {
-        return taskMap.size();
+        return TASK_MAP.size();
+    }
+
+    /**
+     * Gets the task map. Keys are weak references to tasks, values are runners managed by executor.
+     *
+     * @return the task map.
+     */
+    static HashMap<WeakReference<BaseGenericObjectPool<?, ?>.Evictor>, WeakRunner<BaseGenericObjectPool<?, ?>.Evictor>> getTaskMap() {
+        return TASK_MAP;
     }
 
     /**
@@ -166,11 +188,11 @@ class EvictionTimer {
      *
      * @param evictor Eviction task to remove
      */
-    private static void remove(final BaseGenericObjectPool<?>.Evictor evictor) {
-        for (final Entry<WeakReference<Runnable>, WeakRunner> entry : taskMap.entrySet()) {
+    private static void remove(final BaseGenericObjectPool<?, ?>.Evictor evictor) {
+        for (final Entry<WeakReference<BaseGenericObjectPool<?, ?>.Evictor>, WeakRunner<BaseGenericObjectPool<?, ?>.Evictor>> entry : TASK_MAP.entrySet()) {
             if (entry.getKey().get() == evictor) {
                 executor.remove(entry.getValue());
-                taskMap.remove(entry.getKey());
+                TASK_MAP.remove(entry.getKey());
                 break;
             }
         }
@@ -188,18 +210,18 @@ class EvictionTimer {
      * @param period    Time in milliseconds between executions.
      */
     static synchronized void schedule(
-            final BaseGenericObjectPool<?>.Evictor task, final Duration delay, final Duration period) {
+            final BaseGenericObjectPool<?, ?>.Evictor task, final Duration delay, final Duration period) {
         if (null == executor) {
             executor = new ScheduledThreadPoolExecutor(1, new EvictorThreadFactory());
             executor.setRemoveOnCancelPolicy(true);
             executor.scheduleAtFixedRate(new Reaper(), delay.toMillis(), period.toMillis(), TimeUnit.MILLISECONDS);
         }
-        final WeakReference<Runnable> ref = new WeakReference<>(task);
-        final WeakRunner runner = new WeakRunner(ref);
+        final WeakReference<BaseGenericObjectPool<?, ?>.Evictor> ref = new WeakReference<>(task);
+        final WeakRunner<BaseGenericObjectPool<?, ?>.Evictor> runner = new WeakRunner<>(ref);
         final ScheduledFuture<?> scheduledFuture = executor.scheduleWithFixedDelay(runner, delay.toMillis(),
                 period.toMillis(), TimeUnit.MILLISECONDS);
         task.setScheduledFuture(scheduledFuture);
-        taskMap.put(ref, runner);
+        TASK_MAP.put(ref, runner);
     }
 
     /** Prevents instantiation */
@@ -216,4 +238,5 @@ class EvictionTimer {
         builder.append("EvictionTimer []");
         return builder.toString();
     }
+
 }
diff --git a/src/main/java/org/apache/commons/pool2/impl/GenericKeyedObjectPool.java b/src/main/java/org/apache/commons/pool2/impl/GenericKeyedObjectPool.java
index d9ca745..9dd7fdd 100644
--- a/src/main/java/org/apache/commons/pool2/impl/GenericKeyedObjectPool.java
+++ b/src/main/java/org/apache/commons/pool2/impl/GenericKeyedObjectPool.java
@@ -84,13 +84,12 @@ import org.apache.commons.pool2.UsageTracking;
  *
  * @param <K> The type of keys maintained by this pool.
  * @param <T> Type of element pooled in this pool.
+ * @param <E> Type of exception thrown in this pool.
  *
  * @since 2.0
  */
-public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
-        implements KeyedObjectPool<K, T>, GenericKeyedObjectPoolMXBean<K>, UsageTracking<T> {
-
-    private static final Integer ZERO = Integer.valueOf(0);
+public class GenericKeyedObjectPool<K, T, E extends Exception> extends BaseGenericObjectPool<T, E>
+        implements KeyedObjectPool<K, T, E>, GenericKeyedObjectPoolMXBean<K>, UsageTracking<T> {
 
     /**
      * Maintains information on the per key queue for a given key.
@@ -188,6 +187,8 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
 
     }
 
+    private static final Integer ZERO = Integer.valueOf(0);
+
     // JMX specific attributes
     private static final String ONAME_BASE =
             "org.apache.commons.pool2:type=GenericKeyedObjectPool,name=";
@@ -203,7 +204,7 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
     private volatile int maxTotalPerKey =
             GenericKeyedObjectPoolConfig.DEFAULT_MAX_TOTAL_PER_KEY;
 
-    private final KeyedPooledObjectFactory<K, T> factory;
+    private final KeyedPooledObjectFactory<K, T, E> factory;
 
     private final boolean fairness;
 
@@ -221,7 +222,7 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
      * to ensure any changes to the list of current keys is made in a
      * thread-safe manner.
      */
-    private final List<K> poolKeyList = new ArrayList<>(); // @GuardedBy("keyLock")
+    private final ArrayList<K> poolKeyList = new ArrayList<>(); // @GuardedBy("keyLock")
 
     private final ReadWriteLock keyLock = new ReentrantReadWriteLock(true);
 
@@ -243,7 +244,7 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
      * {@link GenericKeyedObjectPoolConfig}.
      * @param factory the factory to be used to create entries
      */
-    public GenericKeyedObjectPool(final KeyedPooledObjectFactory<K, T> factory) {
+    public GenericKeyedObjectPool(final KeyedPooledObjectFactory<K, T, E> factory) {
         this(factory, new GenericKeyedObjectPoolConfig<>());
     }
 
@@ -257,7 +258,7 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
      *                  the configuration object will not be reflected in the
      *                  pool.
      */
-    public GenericKeyedObjectPool(final KeyedPooledObjectFactory<K, T> factory,
+    public GenericKeyedObjectPool(final KeyedPooledObjectFactory<K, T, E> factory,
             final GenericKeyedObjectPoolConfig<T> config) {
 
         super(config, ONAME_BASE, config.getJmxNamePrefix());
@@ -286,7 +287,7 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
      *                         and removal.  The configuration is used by value.
      * @since 2.10.0
      */
-    public GenericKeyedObjectPool(final KeyedPooledObjectFactory<K, T> factory,
+    public GenericKeyedObjectPool(final KeyedPooledObjectFactory<K, T, E> factory,
             final GenericKeyedObjectPoolConfig<T> config, final AbandonedConfig abandonedConfig) {
         this(factory, config);
         setAbandonedConfig(abandonedConfig);
@@ -298,9 +299,9 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
      * @param key The key to associate with the idle object
      * @param p The wrapped object to add.
      *
-     * @throws Exception If the associated factory fails to passivate the object
+     * @throws E If the associated factory fails to passivate the object
      */
-    private void addIdleObject(final K key, final PooledObject<T> p) throws Exception {
+    private void addIdleObject(final K key, final PooledObject<T> p) throws E {
 
         if (p != null) {
             factory.passivateObject(key, p);
@@ -323,16 +324,15 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
      *
      * @param key the key a new instance should be added to
      *
-     * @throws Exception when {@link KeyedPooledObjectFactory#makeObject}
+     * @throws E when {@link KeyedPooledObjectFactory#makeObject}
      *                   fails.
      */
     @Override
-    public void addObject(final K key) throws Exception {
+    public void addObject(final K key) throws E {
         assertOpen();
         register(key);
         try {
-            final PooledObject<T> p = create(key);
-            addIdleObject(key, p);
+            addIdleObject(key, create(key));
         } finally {
             deregister(key);
         }
@@ -346,7 +346,7 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
      * {@inheritDoc}
      */
     @Override
-    public T borrowObject(final K key) throws Exception {
+    public T borrowObject(final K key) throws E {
         return borrowObject(key, getMaxWaitDuration().toMillis());
     }
 
@@ -409,10 +409,10 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
      * @throws NoSuchElementException if a keyed object instance cannot be
      *                                returned because the pool is exhausted.
      *
-     * @throws Exception if a keyed object instance cannot be returned due to an
+     * @throws E if a keyed object instance cannot be returned due to an
      *                   error
      */
-    public T borrowObject(final K key, final long borrowMaxWaitMillis) throws Exception {
+    public T borrowObject(final K key, final long borrowMaxWaitMillis) throws E {
         assertOpen();
 
         final AbandonedConfig ac = this.abandonedConfig;
@@ -443,10 +443,11 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
                 }
                 if (blockWhenExhausted) {
                     if (p == null) {
-                        if (borrowMaxWaitMillis < 0) {
-                            p = objectDeque.getIdleObjects().takeFirst();
-                        } else {
-                            p = objectDeque.getIdleObjects().pollFirst(borrowMaxWaitMillis, TimeUnit.MILLISECONDS);
+                        try {
+                            p = borrowMaxWaitMillis < 0 ? objectDeque.getIdleObjects().takeFirst() : 
+                                objectDeque.getIdleObjects().pollFirst(borrowMaxWaitMillis, TimeUnit.MILLISECONDS);
+                        } catch (InterruptedException e) {
+                            throw cast(e);
                         }
                     }
                     if (p == null) {
@@ -512,14 +513,6 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
         return p.getObject();
     }
 
-    @Override
-    String getStatsString() {
-        // Simply listed in AB order.
-        return super.getStatsString() +
-                String.format(", fairness=%s, maxIdlePerKey%,d, maxTotalPerKey=%,d, minIdlePerKey=%,d, numTotal=%,d",
-                        fairness, maxIdlePerKey, maxTotalPerKey, minIdlePerKey, numTotal.get());
-    }
-
     /**
      * Calculate the number of objects that need to be created to attempt to
      * maintain the minimum number of idle objects while not exceeded the limits
@@ -557,7 +550,6 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
         return objectDefecit;
     }
 
-
     /**
      * Clears any objects sitting idle in the pool by removing them from the
      * idle instance sub-pools and then invoking the configured
@@ -566,7 +558,6 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
      * method on each idle instance.
      * <p>
      * Implementation notes:
-     * </p>
      * <ul>
      * <li>This method does not destroy or effect in any way instances that are
      * checked out when it is invoked.</li>
@@ -579,23 +570,43 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
      */
     @Override
     public void clear() {
-        poolMap.keySet().forEach(this::clear);
+        poolMap.keySet().forEach(key -> clear(key,false));
     }
 
-
     /**
      * Clears the specified sub-pool, removing all pooled instances
      * corresponding to the given {@code key}. Exceptions encountered
      * destroying idle instances are swallowed but notified via a
      * {@link SwallowedExceptionListener}.
+     * <p>
+     * If there are clients waiting to borrow objects, this method will
+     * attempt to reuse the capacity freed by this operation, adding
+     * instances to the most loaded keyed pools.  To avoid triggering
+     * possible object creation, use {@link #clear(Object, boolean)}.
      *
      * @param key the key to clear
      */
     @Override
     public void clear(final K key) {
+        clear(key, true);
+    }
 
+    /**
+     * Clears the specified sub-pool, removing all pooled instances
+     * corresponding to the given {@code key}. Exceptions encountered
+     * destroying idle instances are swallowed but notified via a
+     * {@link SwallowedExceptionListener}.
+     * <p>
+     * If reuseCapacity is true and there are clients waiting to
+     * borrow objects, this method will attempt to reuse the capacity freed
+     * by this operation, adding instances to the most loaded keyed pools.
+     *
+     * @param key the key to clear
+     * @param reuseCapacity whether or not to reuse freed capacity
+     */
+    public void clear(final K key, boolean reuseCapacity) {
         final ObjectDeque<T> objectDeque = register(key);
-
+        int freedCapacity = 0;
         try {
             final LinkedBlockingDeque<PooledObject<T>> idleObjects =
                     objectDeque.getIdleObjects();
@@ -604,7 +615,9 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
 
             while (p != null) {
                 try {
-                    destroy(key, p, true, DestroyMode.NORMAL);
+                    if (destroy(key, p, true, DestroyMode.NORMAL)) {
+                        freedCapacity++;
+                    }
                 } catch (final Exception e) {
                     swallowException(e);
                 }
@@ -613,8 +626,11 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
         } finally {
             deregister(key);
         }
-    }
+        if (reuseCapacity) {
+            reuseCapacity(freedCapacity);
+        }
 
+    }
 
     /**
      * Clears oldest 15% of objects in pool.  The method sorts the objects into
@@ -701,16 +717,17 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
         }
     }
 
+
     /**
-     * Creates a new pooled object.
+     * Creates a new pooled object or null.
      *
-     * @param key Key associated with new pooled object
+     * @param key Key associated with new pooled object.
      *
-     * @return The new, wrapped pooled object
+     * @return The new, wrapped pooled object. May return null.
      *
-     * @throws Exception If the objection creation fails
+     * @throws E If the objection creation fails.
      */
-    private PooledObject<T> create(final K key) throws Exception {
+    private PooledObject<T> create(final K key) throws E {
         int maxTotalPerKeySave = getMaxTotalPerKey(); // Per key
         if (maxTotalPerKeySave < 0) {
             maxTotalPerKeySave = Integer.MAX_VALUE;
@@ -760,7 +777,11 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
                         // bring the pool to capacity. Those calls might also
                         // fail so wait until they complete and then re-test if
                         // the pool is at capacity or not.
-                        objectDeque.makeObjectCountLock.wait();
+                        try {
+                            objectDeque.makeObjectCountLock.wait();
+                        } catch (InterruptedException e) {
+                            throw cast(e);
+                        }
                     }
                 } else {
                     // The pool is not at capacity. Create a new object.
@@ -838,7 +859,6 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
         }
     }
 
-
     /**
      * Destroy the wrapped, pooled object.
      *
@@ -849,10 +869,9 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
      * @param destroyMode DestroyMode context provided to the factory
      *
      * @return {@code true} if the object was destroyed, otherwise {@code false}
-     * @throws Exception If the object destruction failed
+     * @throws E If the object destruction failed
      */
-    private boolean destroy(final K key, final PooledObject<T> toDestroy, final boolean always, final DestroyMode destroyMode)
-            throws Exception {
+    private boolean destroy(final K key, final PooledObject<T> toDestroy, final boolean always, final DestroyMode destroyMode) throws E {
 
         final ObjectDeque<T> objectDeque = register(key);
 
@@ -886,8 +905,9 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
         }
     }
 
+
     @Override
-    void ensureMinIdle() throws Exception {
+    void ensureMinIdle() throws E {
         final int minIdlePerKeySave = getMinIdlePerKey();
         if (minIdlePerKeySave < 1) {
             return;
@@ -904,9 +924,9 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
      *
      * @param key The key to check for idle objects
      *
-     * @throws Exception If a new object is required and cannot be created
+     * @throws E If a new object is required and cannot be created
      */
-    private void ensureMinIdle(final K key) throws Exception {
+    private void ensureMinIdle(final K key) throws E {
         // Calculate current pool objects
         ObjectDeque<T> objectDeque = poolMap.get(key);
 
@@ -931,7 +951,6 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
         }
     }
 
-
     /**
      * {@inheritDoc}
      * <p>
@@ -941,7 +960,7 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
      * </p>
      */
     @Override
-    public void evict() throws Exception {
+    public void evict() throws E {
         assertOpen();
 
         if (getNumIdle() > 0) {
@@ -1080,16 +1099,29 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
         }
     }
 
+
     /**
      * Gets a reference to the factory used to create, destroy and validate
      * the objects used by this pool.
      *
      * @return the factory
      */
-    public KeyedPooledObjectFactory<K, T> getFactory() {
+    public KeyedPooledObjectFactory<K, T, E> getFactory() {
         return factory;
     }
 
+    /**
+     * Gets a copy of the pool key list.
+     *
+     * @return a copy of the pool key list.
+     * @since 2.12.0
+     */
+    @Override
+    @SuppressWarnings("unchecked")
+    public List<K> getKeys() {
+        return (List<K>) poolKeyList.clone();
+    }
+
     /**
      * Gets the cap on the number of "idle" instances per key in the pool.
      * If maxIdlePerKey is set too low on heavily loaded systems it is possible
@@ -1144,7 +1176,7 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
     @Override
     public int getMinIdlePerKey() {
         final int maxIdlePerKeySave = getMaxIdlePerKey();
-        return this.minIdlePerKey > maxIdlePerKeySave ? maxIdlePerKeySave : minIdlePerKey;
+        return Math.min(this.minIdlePerKey, maxIdlePerKeySave);
     }
 
     @Override
@@ -1248,6 +1280,14 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
         return result;
     }
 
+    @Override
+    String getStatsString() {
+        // Simply listed in AB order.
+        return super.getStatsString() +
+                String.format(", fairness=%s, maxIdlePerKey%,d, maxTotalPerKey=%,d, minIdlePerKey=%,d, numTotal=%,d",
+                        fairness, maxIdlePerKey, maxTotalPerKey, minIdlePerKey, numTotal.get());
+    }
+
     /**
      * Checks to see if there are any threads currently waiting to borrow
      * objects but are blocked waiting for more objects to become available.
@@ -1256,7 +1296,7 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
      *         {@code false}
      */
     private boolean hasBorrowWaiters() {
-        return poolMap.values().stream().anyMatch(deque -> deque != null && deque.getIdleObjects().hasTakeWaiters());
+        return getBlockWhenExhausted() && poolMap.values().stream().anyMatch(deque -> deque != null && deque.getIdleObjects().hasTakeWaiters());
     }
 
     /**
@@ -1269,13 +1309,13 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
      * @param key pool key
      * @param obj instance to invalidate
      *
-     * @throws Exception             if an exception occurs destroying the
+     * @throws E             if an exception occurs destroying the
      *                               object
      * @throws IllegalStateException if obj does not belong to the pool
      *                               under the given key
      */
     @Override
-    public void invalidateObject(final K key, final T obj) throws Exception {
+    public void invalidateObject(final K key, final T obj) throws E {
         invalidateObject(key, obj, DestroyMode.NORMAL);
     }
 
@@ -1290,27 +1330,25 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
      * @param obj instance to invalidate
      * @param destroyMode DestroyMode context provided to factory
      *
-     * @throws Exception             if an exception occurs destroying the
+     * @throws E             if an exception occurs destroying the
      *                               object
      * @throws IllegalStateException if obj does not belong to the pool
      *                               under the given key
      * @since 2.9.0
      */
     @Override
-    public void invalidateObject(final K key, final T obj, final DestroyMode destroyMode) throws Exception {
+    public void invalidateObject(final K key, final T obj, final DestroyMode destroyMode) throws E {
         final ObjectDeque<T> objectDeque = poolMap.get(key);
-        final PooledObject<T> p = objectDeque.getAllObjects().get(new IdentityWrapper<>(obj));
+        final PooledObject<T> p = objectDeque != null ? objectDeque.getAllObjects().get(new IdentityWrapper<>(obj)) : null;
         if (p == null) {
             throw new IllegalStateException(appendStats("Object not currently part of this pool"));
         }
         synchronized (p) {
             if (p.getState() != PooledObjectState.INVALID) {
                 destroy(key, p, true, destroyMode);
+                reuseCapacity();
             }
         }
-        if (objectDeque.idleObjects.hasTakeWaiters()) {
-            addObject(key);
-        }
     }
 
     /**
@@ -1343,9 +1381,9 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
      *
      * @param key - The key to register for pool control.
      *
-     * @throws Exception If the associated factory throws an exception
+     * @throws E If the associated factory throws an exception
      */
-    public void preparePool(final K key) throws Exception {
+    public void preparePool(final K key) throws E {
         final int minIdlePerKeySave = getMinIdlePerKey();
         if (minIdlePerKeySave < 1) {
             return;
@@ -1545,7 +1583,7 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
         int maxQueueLength = 0;
         LinkedBlockingDeque<PooledObject<T>> mostLoaded = null;
         K loadedKey = null;
-        for (final Entry<K, GenericKeyedObjectPool<K, T>.ObjectDeque<T>> entry : poolMap.entrySet()) {
+        for (final Entry<K, GenericKeyedObjectPool<K, T, E>.ObjectDeque<T>> entry : poolMap.entrySet()) {
             final K k = entry.getKey();
             final ObjectDeque<T> deque = entry.getValue();
             if (deque != null) {
@@ -1575,6 +1613,17 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
         }
     }
 
+    /**
+     * Call {@link #reuseCapacity()} repeatedly.
+     *
+     * @param newCapacity number of new instances to attempt to create.
+     */
+    private void reuseCapacity(int newCapacity) {
+        for (int i = 0; i < newCapacity; i++) {
+            reuseCapacity();
+        }
+    }
+
     /**
      * Sets the configuration.
      *
@@ -1675,21 +1724,6 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
         builder.append(abandonedConfig);
     }
 
-    /**
-     * Whether there is at least one thread waiting on this deque, add an pool object.
-     * @param key pool key.
-     * @param idleObjects list of idle pool objects.
-     */
-    private void whenWaitersAddObject(final K key, final LinkedBlockingDeque<PooledObject<T>> idleObjects) {
-        if (idleObjects.hasTakeWaiters()) {
-            try {
-                addObject(key);
-            } catch (final Exception e) {
-                swallowException(e);
-            }
-        }
-    }
-
     /**
      * @since 2.10.0
      */
@@ -1705,4 +1739,19 @@ public class GenericKeyedObjectPool<K, T> extends BaseGenericObjectPool<T>
         }
     }
 
+    /**
+     * Whether there is at least one thread waiting on this deque, add an pool object.
+     * @param key pool key.
+     * @param idleObjects list of idle pool objects.
+     */
+    private void whenWaitersAddObject(final K key, final LinkedBlockingDeque<PooledObject<T>> idleObjects) {
+        if (idleObjects.hasTakeWaiters()) {
+            try {
+                addObject(key);
+            } catch (final Exception e) {
+                swallowException(e);
+            }
+        }
+    }
+
 }
diff --git a/src/main/java/org/apache/commons/pool2/impl/GenericObjectPool.java b/src/main/java/org/apache/commons/pool2/impl/GenericObjectPool.java
index d732fe1..f68e99f 100644
--- a/src/main/java/org/apache/commons/pool2/impl/GenericObjectPool.java
+++ b/src/main/java/org/apache/commons/pool2/impl/GenericObjectPool.java
@@ -76,11 +76,12 @@ import org.apache.commons.pool2.UsageTracking;
  * @see GenericKeyedObjectPool
  *
  * @param <T> Type of element pooled in this pool.
+ * @param <E> Type of exception thrown in this pool.
  *
  * @since 2.0
  */
-public class GenericObjectPool<T> extends BaseGenericObjectPool<T>
-        implements ObjectPool<T>, GenericObjectPoolMXBean, UsageTracking<T> {
+public class GenericObjectPool<T, E extends Exception> extends BaseGenericObjectPool<T, E>
+        implements ObjectPool<T, E>, GenericObjectPoolMXBean, UsageTracking<T> {
 
     // JMX specific attributes
     private static final String ONAME_BASE =
@@ -92,7 +93,7 @@ public class GenericObjectPool<T> extends BaseGenericObjectPool<T>
 
     private volatile int minIdle = GenericObjectPoolConfig.DEFAULT_MIN_IDLE;
 
-    private final PooledObjectFactory<T> factory;
+    private final PooledObjectFactory<T, E> factory;
 
     /*
      * All of the objects currently associated with this pool in any state. It
@@ -126,7 +127,7 @@ public class GenericObjectPool<T> extends BaseGenericObjectPool<T>
      * @param factory The object factory to be used to create object instances
      *                used by this pool
      */
-    public GenericObjectPool(final PooledObjectFactory<T> factory) {
+    public GenericObjectPool(final PooledObjectFactory<T, E> factory) {
         this(factory, new GenericObjectPoolConfig<>());
     }
 
@@ -141,7 +142,7 @@ public class GenericObjectPool<T> extends BaseGenericObjectPool<T>
      *                  the configuration object will not be reflected in the
      *                  pool.
      */
-    public GenericObjectPool(final PooledObjectFactory<T> factory,
+    public GenericObjectPool(final PooledObjectFactory<T, E> factory,
             final GenericObjectPoolConfig<T> config) {
 
         super(config, ONAME_BASE, config.getJmxNamePrefix());
@@ -170,7 +171,7 @@ public class GenericObjectPool<T> extends BaseGenericObjectPool<T>
      * @param abandonedConfig  Configuration for abandoned object identification
      *                         and removal.  The configuration is used by value.
      */
-    public GenericObjectPool(final PooledObjectFactory<T> factory,
+    public GenericObjectPool(final PooledObjectFactory<T, E> factory,
             final GenericObjectPoolConfig<T> config, final AbandonedConfig abandonedConfig) {
         this(factory, config);
         setAbandonedConfig(abandonedConfig);
@@ -183,9 +184,9 @@ public class GenericObjectPool<T> extends BaseGenericObjectPool<T>
      *
      * @param p The object to make idle
      *
-     * @throws Exception If the factory fails to passivate the object
+     * @throws E If the factory fails to passivate the object
      */
-    private void addIdleObject(final PooledObject<T> p) throws Exception {
+    private void addIdleObject(final PooledObject<T> p) throws E {
         if (p != null) {
             factory.passivateObject(p);
             if (getLifo()) {
@@ -204,7 +205,7 @@ public class GenericObjectPool<T> extends BaseGenericObjectPool<T>
      * (no exception, no impact to the pool). </p>
      */
     @Override
-    public void addObject() throws Exception {
+    public void addObject() throws E {
         assertOpen();
         if (factory == null) {
             throw new IllegalStateException("Cannot add objects without a factory.");
@@ -219,7 +220,7 @@ public class GenericObjectPool<T> extends BaseGenericObjectPool<T>
      * {@inheritDoc}
      */
     @Override
-    public T borrowObject() throws Exception {
+    public T borrowObject() throws E {
         return borrowObject(getMaxWaitDuration());
     }
 
@@ -266,14 +267,11 @@ public class GenericObjectPool<T> extends BaseGenericObjectPool<T>
      *                            to become available
      *
      * @return object instance from the pool
-     *
      * @throws NoSuchElementException if an instance cannot be returned
-     *
-     * @throws Exception if an object instance cannot be returned due to an
-     *                   error
+     * @throws E if an object instance cannot be returned due to an error
      * @since 2.10.0
      */
-    public T borrowObject(final Duration borrowMaxWaitDuration) throws Exception {
+    public T borrowObject(final Duration borrowMaxWaitDuration) throws E {
         assertOpen();
 
         final AbandonedConfig ac = this.abandonedConfig;
@@ -302,11 +300,12 @@ public class GenericObjectPool<T> extends BaseGenericObjectPool<T>
             }
             if (blockWhenExhausted) {
                 if (p == null) {
-                    if (borrowMaxWaitDuration.isNegative()) {
-                        p = idleObjects.takeFirst();
-                    } else {
-                        p = idleObjects.pollFirst(borrowMaxWaitDuration);
-                    }
+                    try {
+                        p = borrowMaxWaitDuration.isNegative() ? idleObjects.takeFirst() : idleObjects.pollFirst(borrowMaxWaitDuration);
+                    } catch (final InterruptedException e) {
+                        // Don't surface exception type of internal locking mechanism.
+                        throw cast(e);
+                    }  
                 }
                 if (p == null) {
                     throw new NoSuchElementException(appendStats(
@@ -369,18 +368,6 @@ public class GenericObjectPool<T> extends BaseGenericObjectPool<T>
         return p.getObject();
     }
 
-    PooledObject<T> getPooledObject(final T obj) {
-        return allObjects.get(new IdentityWrapper<>(obj));
-    }
-
-    @Override
-    String getStatsString() {
-        // Simply listed in AB order.
-        return super.getStatsString() +
-                String.format(", createdCount=%,d, makeObjectCount=%,d, maxIdle=%,d, minIdle=%,d",
-                        createdCount.get(), makeObjectCount, maxIdle, minIdle);
-    }
-
     /**
      * Borrows an object from the pool using the specific waiting time which only
      * applies if {@link #getBlockWhenExhausted()} is true.
@@ -427,10 +414,10 @@ public class GenericObjectPool<T> extends BaseGenericObjectPool<T>
      *
      * @throws NoSuchElementException if an instance cannot be returned
      *
-     * @throws Exception if an object instance cannot be returned due to an
+     * @throws E if an object instance cannot be returned due to an
      *                   error
      */
-    public T borrowObject(final long borrowMaxWaitMillis) throws Exception {
+    public T borrowObject(final long borrowMaxWaitMillis) throws E {
         return borrowObject(Duration.ofMillis(borrowMaxWaitMillis));
     }
 
@@ -504,15 +491,14 @@ public class GenericObjectPool<T> extends BaseGenericObjectPool<T>
     /**
      * Attempts to create a new wrapped pooled object.
      * <p>
-     * If there are {@link #getMaxTotal()} objects already in circulation
-     * or in process of being created, this method returns null.
+     * If there are {@link #getMaxTotal()} objects already in circulation or in process of being created, this method
+     * returns null.
      * </p>
      *
      * @return The new wrapped pooled object
-     *
-     * @throws Exception if the object factory's {@code makeObject} fails
+     * @throws E if the object factory's {@code makeObject} fails
      */
-    private PooledObject<T> create() throws Exception {
+    private PooledObject<T> create() throws E {
         int localMaxTotal = getMaxTotal();
         // This simplifies the code later in this method
         if (localMaxTotal < 0) {
@@ -545,7 +531,12 @@ public class GenericObjectPool<T> extends BaseGenericObjectPool<T>
                         // bring the pool to capacity. Those calls might also
                         // fail so wait until they complete and then re-test if
                         // the pool is at capacity or not.
-                        makeObjectCountLock.wait(localMaxWaitTimeMillis);
+                        try {
+                            makeObjectCountLock.wait(localMaxWaitTimeMillis);
+                        } catch (final InterruptedException e) {
+                            // Don't surface exception type of internal locking mechanism.
+                            throw cast(e);
+                        }
                     }
                 } else {
                     // The pool is not at capacity. Create a new object.
@@ -600,10 +591,10 @@ public class GenericObjectPool<T> extends BaseGenericObjectPool<T>
      * @param toDestroy The wrapped pooled object to destroy
      * @param destroyMode DestroyMode context provided to the factory
      *
-     * @throws Exception If the factory fails to destroy the pooled object
+     * @throws E If the factory fails to destroy the pooled object
      *                   cleanly
      */
-    private void destroy(final PooledObject<T> toDestroy, final DestroyMode destroyMode) throws Exception {
+    private void destroy(final PooledObject<T> toDestroy, final DestroyMode destroyMode) throws E {
         toDestroy.invalidate();
         idleObjects.remove(toDestroy);
         allObjects.remove(new IdentityWrapper<>(toDestroy.getObject()));
@@ -626,9 +617,9 @@ public class GenericObjectPool<T> extends BaseGenericObjectPool<T>
      *
      * @param idleCount the number of idle instances desired
      * @param always true means create instances even if the pool has no threads waiting
-     * @throws Exception if the factory's makeObject throws
+     * @throws E if the factory's makeObject throws
      */
-    private void ensureIdle(final int idleCount, final boolean always) throws Exception {
+    private void ensureIdle(final int idleCount, final boolean always) throws E {
         if (idleCount < 1 || isClosed() || (!always && !idleObjects.hasTakeWaiters())) {
             return;
         }
@@ -655,7 +646,7 @@ public class GenericObjectPool<T> extends BaseGenericObjectPool<T>
     }
 
     @Override
-    void ensureMinIdle() throws Exception {
+    void ensureMinIdle() throws E {
         ensureIdle(getMinIdle(), true);
     }
 
@@ -667,7 +658,7 @@ public class GenericObjectPool<T> extends BaseGenericObjectPool<T>
      * </p>
      */
     @Override
-    public void evict() throws Exception {
+    public void evict() throws E {
         assertOpen();
 
         if (!idleObjects.isEmpty()) {
@@ -786,7 +777,7 @@ public class GenericObjectPool<T> extends BaseGenericObjectPool<T>
      *
      * @return the factory
      */
-    public PooledObjectFactory<T> getFactory() {
+    public PooledObjectFactory<T, E> getFactory() {
         return factory;
     }
 
@@ -851,10 +842,7 @@ public class GenericObjectPool<T> extends BaseGenericObjectPool<T>
     @Override
     public int getMinIdle() {
         final int maxIdleSave = getMaxIdle();
-        if (this.minIdle > maxIdleSave) {
-            return maxIdleSave;
-        }
-        return minIdle;
+        return Math.min(this.minIdle, maxIdleSave);
     }
 
     @Override
@@ -898,36 +886,46 @@ public class GenericObjectPool<T> extends BaseGenericObjectPool<T>
         return 0;
     }
 
+    PooledObject<T> getPooledObject(final T obj) {
+        return allObjects.get(new IdentityWrapper<>(obj));
+    }
+
+    @Override
+    String getStatsString() {
+        // Simply listed in AB order.
+        return super.getStatsString() +
+                String.format(", createdCount=%,d, makeObjectCount=%,d, maxIdle=%,d, minIdle=%,d",
+                        createdCount.get(), makeObjectCount, maxIdle, minIdle);
+    }
+
     /**
      * {@inheritDoc}
      * <p>
-     * Activation of this method decrements the active count and attempts to
-     * destroy the instance, using the default (NORMAL) {@link DestroyMode}.
+     * Activation of this method decrements the active count and attempts to destroy the instance, using the default
+     * (NORMAL) {@link DestroyMode}.
      * </p>
      *
-     * @throws Exception             if an exception occurs destroying the
-     *                               object
+     * @throws E if an exception occurs destroying the
      * @throws IllegalStateException if obj does not belong to this pool
      */
     @Override
-    public void invalidateObject(final T obj) throws Exception {
+    public void invalidateObject(final T obj) throws E {
         invalidateObject(obj, DestroyMode.NORMAL);
     }
 
     /**
      * {@inheritDoc}
      * <p>
-     * Activation of this method decrements the active count and attempts to
-     * destroy the instance, using the provided {@link DestroyMode}.
+     * Activation of this method decrements the active count and attempts to destroy the instance, using the provided
+     * {@link DestroyMode}.
      * </p>
-     *
-     * @throws Exception             if an exception occurs destroying the
-     *                               object
+     * 
+     * @throws E if an exception occurs destroying the object
      * @throws IllegalStateException if obj does not belong to this pool
      * @since 2.9.0
      */
     @Override
-    public void invalidateObject(final T obj, final DestroyMode destroyMode) throws Exception {
+    public void invalidateObject(final T obj, final DestroyMode destroyMode) throws E {
         final PooledObject<T> p = getPooledObject(obj);
         if (p == null) {
             if (isAbandonedConfig()) {
@@ -964,10 +962,10 @@ public class GenericObjectPool<T> extends BaseGenericObjectPool<T>
      * Tries to ensure that {@link #getMinIdle()} idle instances are available
      * in the pool.
      *
-     * @throws Exception If the associated factory throws an exception
+     * @throws E If the associated factory throws an exception
      * @since 2.4
      */
-    public void preparePool() throws Exception {
+    public void preparePool() throws E {
         if (getMinIdle() < 1) {
             return;
         }
@@ -1180,5 +1178,5 @@ public class GenericObjectPool<T> extends BaseGenericObjectPool<T>
             getPooledObject(pooledObject).use();
         }
     }
-
+    
 }
diff --git a/src/main/java/org/apache/commons/pool2/impl/LinkedBlockingDeque.java b/src/main/java/org/apache/commons/pool2/impl/LinkedBlockingDeque.java
index 142b3e6..ad48782 100644
--- a/src/main/java/org/apache/commons/pool2/impl/LinkedBlockingDeque.java
+++ b/src/main/java/org/apache/commons/pool2/impl/LinkedBlockingDeque.java
@@ -16,6 +16,8 @@
  */
 package org.apache.commons.pool2.impl;
 
+import java.io.IOException;
+import java.io.ObjectInputStream;
 import java.io.Serializable;
 import java.time.Duration;
 import java.util.AbstractQueue;
@@ -1093,8 +1095,8 @@ class LinkedBlockingDeque<E> extends AbstractQueue<E>
      * deserialize it).
      * @param s the stream
      */
-    private void readObject(final java.io.ObjectInputStream s)
-        throws java.io.IOException, ClassNotFoundException {
+    private void readObject(final ObjectInputStream s)
+        throws IOException, ClassNotFoundException {
         s.defaultReadObject();
         count = 0;
         first = null;
diff --git a/src/main/java/org/apache/commons/pool2/impl/PoolImplUtils.java b/src/main/java/org/apache/commons/pool2/impl/PoolImplUtils.java
index 7765f6c..412c8f2 100644
--- a/src/main/java/org/apache/commons/pool2/impl/PoolImplUtils.java
+++ b/src/main/java/org/apache/commons/pool2/impl/PoolImplUtils.java
@@ -180,6 +180,17 @@ class PoolImplUtils {
         return a.compareTo(b) < 0 ? a : b;
     }
 
+    /**
+     * Returns a non-null duration, value if non-null, otherwise defaultValue.
+     *
+     * @param value May be null.
+     * @param defaultValue May not be null/
+     * @return value if non-null, otherwise defaultValue.
+     */
+    static Duration nonNull(final Duration value, final Duration defaultValue) {
+        return value != null ? value : Objects.requireNonNull(defaultValue, "defaultValue");
+    }
+
     /**
      * Converts a {@link TimeUnit} to a {@link ChronoUnit}.
      *
@@ -208,17 +219,6 @@ class PoolImplUtils {
         }
     }
 
-    /**
-     * Returns a non-null duration, value if non-null, otherwise defaultValue.
-     *
-     * @param value May be null.
-     * @param defaultValue May not be null/
-     * @return value if non-null, otherwise defaultValue.
-     */
-    static Duration nonNull(final Duration value, final Duration defaultValue) {
-        return value != null ? value : Objects.requireNonNull(defaultValue, "defaultValue");
-    }
-
     /**
      * Converts am amount and TimeUnit into a Duration.
      *
diff --git a/src/main/java/org/apache/commons/pool2/impl/SoftReferenceObjectPool.java b/src/main/java/org/apache/commons/pool2/impl/SoftReferenceObjectPool.java
index 868bfcf..2fe90d9 100644
--- a/src/main/java/org/apache/commons/pool2/impl/SoftReferenceObjectPool.java
+++ b/src/main/java/org/apache/commons/pool2/impl/SoftReferenceObjectPool.java
@@ -37,13 +37,15 @@ import org.apache.commons.pool2.PooledObjectFactory;
  *
  * @param <T>
  *            Type of element pooled in this pool.
+ * @param <E>
+ *            Type of exception thrown by this pool.
  *
  * @since 2.0
  */
-public class SoftReferenceObjectPool<T> extends BaseObjectPool<T> {
+public class SoftReferenceObjectPool<T, E extends Exception> extends BaseObjectPool<T, E> {
 
     /** Factory to source pooled objects */
-    private final PooledObjectFactory<T> factory;
+    private final PooledObjectFactory<T, E> factory;
 
     /**
      * Queue of broken references that might be able to be removed from
@@ -75,7 +77,7 @@ public class SoftReferenceObjectPool<T> extends BaseObjectPool<T> {
      *
      * @param factory object factory to use.
      */
-    public SoftReferenceObjectPool(final PooledObjectFactory<T> factory) {
+    public SoftReferenceObjectPool(final PooledObjectFactory<T, E> factory) {
         this.factory = factory;
     }
 
@@ -98,12 +100,12 @@ public class SoftReferenceObjectPool<T> extends BaseObjectPool<T> {
      *
      * @throws IllegalStateException
      *             if invoked on a {@link #close() closed} pool
-     * @throws Exception
+     * @throws E
      *             when the {@link #getFactory() factory} has a problem creating
      *             or passivating an object.
      */
     @Override
-    public synchronized void addObject() throws Exception {
+    public synchronized void addObject() throws E {
         assertOpen();
         if (factory == null) {
             throw new IllegalStateException(
@@ -169,13 +171,13 @@ public class SoftReferenceObjectPool<T> extends BaseObjectPool<T> {
      *             if a valid object cannot be provided
      * @throws IllegalStateException
      *             if invoked on a {@link #close() closed} pool
-     * @throws Exception
+     * @throws E
      *             if an exception occurs creating a new instance
      * @return a valid, activated object instance
      */
     @SuppressWarnings("null") // ref cannot be null
     @Override
-    public synchronized T borrowObject() throws Exception {
+    public synchronized T borrowObject() throws E {
         assertOpen();
         T obj = null;
         boolean newlyCreated = false;
@@ -268,9 +270,9 @@ public class SoftReferenceObjectPool<T> extends BaseObjectPool<T> {
      *
      * @param toDestroy PooledSoftReference to destroy
      *
-     * @throws Exception If an error occurs while trying to destroy the object
+     * @throws E If an error occurs while trying to destroy the object
      */
-    private void destroy(final PooledSoftReference<T> toDestroy) throws Exception {
+    private void destroy(final PooledSoftReference<T> toDestroy) throws E {
         toDestroy.invalidate();
         idleReferences.remove(toDestroy);
         allReferences.remove(toDestroy);
@@ -300,7 +302,7 @@ public class SoftReferenceObjectPool<T> extends BaseObjectPool<T> {
      *
      * @return the factory
      */
-    public synchronized PooledObjectFactory<T> getFactory() {
+    public synchronized PooledObjectFactory<T, E> getFactory() {
         return factory;
     }
 
@@ -330,7 +332,7 @@ public class SoftReferenceObjectPool<T> extends BaseObjectPool<T> {
      * {@inheritDoc}
      */
     @Override
-    public synchronized void invalidateObject(final T obj) throws Exception {
+    public synchronized void invalidateObject(final T obj) throws E {
         final PooledSoftReference<T> ref = findReference(obj);
         if (ref == null) {
             throw new IllegalStateException(
@@ -392,7 +394,7 @@ public class SoftReferenceObjectPool<T> extends BaseObjectPool<T> {
      *            if obj is not currently part of this pool
      */
     @Override
-    public synchronized void returnObject(final T obj) throws Exception {
+    public synchronized void returnObject(final T obj) throws E {
         boolean success = !isClosed();
         final PooledSoftReference<T> ref = findReference(obj);
         if (ref == null) {
diff --git a/src/main/java/org/apache/commons/pool2/proxy/ProxiedKeyedObjectPool.java b/src/main/java/org/apache/commons/pool2/proxy/ProxiedKeyedObjectPool.java
index a2e98e9..38a6c4a 100644
--- a/src/main/java/org/apache/commons/pool2/proxy/ProxiedKeyedObjectPool.java
+++ b/src/main/java/org/apache/commons/pool2/proxy/ProxiedKeyedObjectPool.java
@@ -16,6 +16,7 @@
  */
 package org.apache.commons.pool2.proxy;
 
+import java.util.List;
 import java.util.NoSuchElementException;
 
 import org.apache.commons.pool2.KeyedObjectPool;
@@ -29,12 +30,13 @@ import org.apache.commons.pool2.UsageTracking;
  *
  * @param <K> type of the key
  * @param <V> type of the pooled object
+ * @param <E> type of exception thrown by this pool
  *
  * @since 2.0
  */
-public class ProxiedKeyedObjectPool<K, V> implements KeyedObjectPool<K, V> {
+public class ProxiedKeyedObjectPool<K, V, E extends Exception> implements KeyedObjectPool<K, V, E> {
 
-    private final KeyedObjectPool<K, V> pool;
+    private final KeyedObjectPool<K, V, E> pool;
     private final ProxySource<V> proxySource;
 
 
@@ -44,7 +46,7 @@ public class ProxiedKeyedObjectPool<K, V> implements KeyedObjectPool<K, V> {
      * @param pool  The object pool to wrap
      * @param proxySource The source of the proxy objects
      */
-    public ProxiedKeyedObjectPool(final KeyedObjectPool<K, V> pool,
+    public ProxiedKeyedObjectPool(final KeyedObjectPool<K, V, E> pool,
             final ProxySource<V> proxySource) {
         this.pool = pool;
         this.proxySource = proxySource;
@@ -52,14 +54,14 @@ public class ProxiedKeyedObjectPool<K, V> implements KeyedObjectPool<K, V> {
 
 
     @Override
-    public void addObject(final K key) throws Exception, IllegalStateException,
+    public void addObject(final K key) throws E, IllegalStateException,
             UnsupportedOperationException {
         pool.addObject(key);
     }
 
     @SuppressWarnings("unchecked")
     @Override
-    public V borrowObject(final K key) throws Exception, NoSuchElementException,
+    public V borrowObject(final K key) throws E, NoSuchElementException,
             IllegalStateException {
         UsageTracking<V> usageTracking = null;
         if (pool instanceof UsageTracking) {
@@ -69,12 +71,12 @@ public class ProxiedKeyedObjectPool<K, V> implements KeyedObjectPool<K, V> {
     }
 
     @Override
-    public void clear() throws Exception, UnsupportedOperationException {
+    public void clear() throws E, UnsupportedOperationException {
         pool.clear();
     }
 
     @Override
-    public void clear(final K key) throws Exception, UnsupportedOperationException {
+    public void clear(final K key) throws E, UnsupportedOperationException {
         pool.clear(key);
     }
 
@@ -83,6 +85,11 @@ public class ProxiedKeyedObjectPool<K, V> implements KeyedObjectPool<K, V> {
         pool.close();
     }
 
+    @Override
+    public List<K> getKeys() {
+        return pool.getKeys();
+    }
+
     @Override
     public int getNumActive() {
         return pool.getNumActive();
@@ -104,16 +111,15 @@ public class ProxiedKeyedObjectPool<K, V> implements KeyedObjectPool<K, V> {
     }
 
     @Override
-    public void invalidateObject(final K key, final V proxy) throws Exception {
+    public void invalidateObject(final K key, final V proxy) throws E {
         pool.invalidateObject(key, proxySource.resolveProxy(proxy));
     }
 
     @Override
-    public void returnObject(final K key, final V proxy) throws Exception {
+    public void returnObject(final K key, final V proxy) throws E {
         pool.returnObject(key, proxySource.resolveProxy(proxy));
     }
 
-
     /**
      * @since 2.4.3
      */
diff --git a/src/main/java/org/apache/commons/pool2/proxy/ProxiedObjectPool.java b/src/main/java/org/apache/commons/pool2/proxy/ProxiedObjectPool.java
index e725d22..e0b7cd7 100644
--- a/src/main/java/org/apache/commons/pool2/proxy/ProxiedObjectPool.java
+++ b/src/main/java/org/apache/commons/pool2/proxy/ProxiedObjectPool.java
@@ -28,12 +28,13 @@ import org.apache.commons.pool2.UsageTracking;
  * object to the pool.
  *
  * @param <T> type of the pooled object
+ * @param <E> type of the exception
  *
  * @since 2.0
  */
-public class ProxiedObjectPool<T> implements ObjectPool<T> {
+public class ProxiedObjectPool<T, E extends Exception> implements ObjectPool<T, E> {
 
-    private final ObjectPool<T> pool;
+    private final ObjectPool<T, E> pool;
     private final ProxySource<T> proxySource;
 
 
@@ -43,22 +44,20 @@ public class ProxiedObjectPool<T> implements ObjectPool<T> {
      * @param pool  The object pool to wrap
      * @param proxySource The source of the proxy objects
      */
-    public ProxiedObjectPool(final ObjectPool<T> pool, final ProxySource<T> proxySource) {
+    public ProxiedObjectPool(final ObjectPool<T, E> pool, final ProxySource<T> proxySource) {
         this.pool = pool;
         this.proxySource = proxySource;
     }
 
     @Override
-    public void addObject() throws Exception, IllegalStateException,
-            UnsupportedOperationException {
+    public void addObject() throws E, IllegalStateException, UnsupportedOperationException {
         pool.addObject();
     }
 
 
     @SuppressWarnings("unchecked")
     @Override
-    public T borrowObject() throws Exception, NoSuchElementException,
-            IllegalStateException {
+    public T borrowObject() throws E, NoSuchElementException, IllegalStateException {
         UsageTracking<T> usageTracking = null;
         if (pool instanceof UsageTracking) {
             usageTracking = (UsageTracking<T>) pool;
@@ -68,7 +67,7 @@ public class ProxiedObjectPool<T> implements ObjectPool<T> {
 
 
     @Override
-    public void clear() throws Exception, UnsupportedOperationException {
+    public void clear() throws E, UnsupportedOperationException {
         pool.clear();
     }
 
@@ -92,13 +91,13 @@ public class ProxiedObjectPool<T> implements ObjectPool<T> {
 
 
     @Override
-    public void invalidateObject(final T proxy) throws Exception {
+    public void invalidateObject(final T proxy) throws E {
         pool.invalidateObject(proxySource.resolveProxy(proxy));
     }
 
 
     @Override
-    public void returnObject(final T proxy) throws Exception {
+    public void returnObject(final T proxy) throws E {
         pool.returnObject(proxySource.resolveProxy(proxy));
     }
 
diff --git a/src/site/site.xml b/src/site/site.xml
index 0bcbe3e..6185a10 100644
--- a/src/site/site.xml
+++ b/src/site/site.xml
@@ -29,8 +29,8 @@
             <item name="Javadoc 2.x Archive" href="https://javadoc.io/doc/org.apache.commons/commons-pool2/latest/index.html"/>
             <item name="Javadoc 1.x Archive" href="https://javadoc.io/doc/commons-pool/commons-pool/latest/index.html"/>
             <item name="Examples"            href="/examples.html"/>
-            <item name="Downloads"           href="/download_pool.cgi"/>
-            <item name="Wiki"                href="http://wiki.apache.org/commons/Pool"/>
+            <item name="Downloads"           href="/downloads.html"/>
+            <item name="Wiki"                href="https://cwiki.apache.org/confluence/display/commons/Pool"/>
         </menu>
 
         <menu name="Development">
diff --git a/src/site/xdoc/download_pool.xml b/src/site/xdoc/download_pool.xml
index e2ad8b8..672e53b 100644
--- a/src/site/xdoc/download_pool.xml
+++ b/src/site/xdoc/download_pool.xml
@@ -173,66 +173,6 @@ limitations under the License.
         </table>
       </subsection>
     </section>
-    <section name="Apache Commons Pool 2.4.3 (Java 6)">
-      <subsection name="Binaries">
-        <table>
-          <tr>
-              <td><a href="[preferred]/commons/pool/binaries/commons-pool2-2.4.3-bin.tar.gz">commons-pool2-2.4.3-bin.tar.gz</a></td>
-              <td><a href="https://www.apache.org/dist/commons/pool/binaries/commons-pool2-2.4.3-bin.tar.gz.sha512">sha512</a></td>
-              <td><a href="https://www.apache.org/dist/commons/pool/binaries/commons-pool2-2.4.3-bin.tar.gz.asc">pgp</a></td>
-          </tr>
-          <tr>
-              <td><a href="[preferred]/commons/pool/binaries/commons-pool2-2.4.3-bin.zip">commons-pool2-2.4.3-bin.zip</a></td>
-              <td><a href="https://www.apache.org/dist/commons/pool/binaries/commons-pool2-2.4.3-bin.zip.sha512">sha512</a></td>
-              <td><a href="https://www.apache.org/dist/commons/pool/binaries/commons-pool2-2.4.3-bin.zip.asc">pgp</a></td>
-          </tr>
-        </table>
-      </subsection>
-      <subsection name="Source">
-        <table>
-          <tr>
-              <td><a href="[preferred]/commons/pool/source/commons-pool2-2.4.3-src.tar.gz">commons-pool2-2.4.3-src.tar.gz</a></td>
-              <td><a href="https://www.apache.org/dist/commons/pool/source/commons-pool2-2.4.3-src.tar.gz.sha512">sha512</a></td>
-              <td><a href="https://www.apache.org/dist/commons/pool/source/commons-pool2-2.4.3-src.tar.gz.asc">pgp</a></td>
-          </tr>
-          <tr>
-              <td><a href="[preferred]/commons/pool/source/commons-pool2-2.4.3-src.zip">commons-pool2-2.4.3-src.zip</a></td>
-              <td><a href="https://www.apache.org/dist/commons/pool/source/commons-pool2-2.4.3-src.zip.sha512">sha512</a></td>
-              <td><a href="https://www.apache.org/dist/commons/pool/source/commons-pool2-2.4.3-src.zip.asc">pgp</a></td>
-          </tr>
-        </table>
-      </subsection>
-    </section>
-    <section name="Apache Commons Pool 1.6 (Java 5)">
-      <subsection name="Binaries">
-        <table>
-          <tr>
-              <td><a href="[preferred]/commons/pool/binaries/commons-pool-1.6-bin.tar.gz">commons-pool-1.6-bin.tar.gz</a></td>
-              <td><a href="https://www.apache.org/dist/commons/pool/binaries/commons-pool-1.6-bin.tar.gz.sha512">sha512</a></td>
-              <td><a href="https://www.apache.org/dist/commons/pool/binaries/commons-pool-1.6-bin.tar.gz.asc">pgp</a></td>
-          </tr>
-          <tr>
-              <td><a href="[preferred]/commons/pool/binaries/commons-pool-1.6-bin.zip">commons-pool-1.6-bin.zip</a></td>
-              <td><a href="https://www.apache.org/dist/commons/pool/binaries/commons-pool-1.6-bin.zip.sha512">sha512</a></td>
-              <td><a href="https://www.apache.org/dist/commons/pool/binaries/commons-pool-1.6-bin.zip.asc">pgp</a></td>
-          </tr>
-        </table>
-      </subsection>
-      <subsection name="Source">
-        <table>
-          <tr>
-              <td><a href="[preferred]/commons/pool/source/commons-pool-1.6-src.tar.gz">commons-pool-1.6-src.tar.gz</a></td>
-              <td><a href="https://www.apache.org/dist/commons/pool/source/commons-pool-1.6-src.tar.gz.sha512">sha512</a></td>
-              <td><a href="https://www.apache.org/dist/commons/pool/source/commons-pool-1.6-src.tar.gz.asc">pgp</a></td>
-          </tr>
-          <tr>
-              <td><a href="[preferred]/commons/pool/source/commons-pool-1.6-src.zip">commons-pool-1.6-src.zip</a></td>
-              <td><a href="https://www.apache.org/dist/commons/pool/source/commons-pool-1.6-src.zip.sha512">sha512</a></td>
-              <td><a href="https://www.apache.org/dist/commons/pool/source/commons-pool-1.6-src.zip.asc">pgp</a></td>
-          </tr>
-        </table>
-      </subsection>
-    </section>
     <section name="Archives">
         <p>
           Older releases can be obtained from the archives.
diff --git a/src/site/xdoc/downloads.xml b/src/site/xdoc/downloads.xml
index bb01e06..236d0d9 100644
--- a/src/site/xdoc/downloads.xml
+++ b/src/site/xdoc/downloads.xml
@@ -38,17 +38,6 @@
               with a number of attributes changing name to clarify their
               purpose. 
            </li>
-           <li>
-              Version 1.6 adds generics based on the latest 1.5.x release, 1.5.7
-              at the present time. It requires Java 1.5.
-           </li>
-           <li>
-              Versions 1.3 through 1.5 depend at runtime only on a Java 1.3 or better JVM.
-           </li>
-           <li>
-              Version 1.2 depends at runtime on
-            <a href="https://commons.apache.org/collections/">commons-collections</a>.
-           </li>
          </ul>
          <p>
          </p>
diff --git a/src/test/java/org/apache/commons/pool2/MethodCallPoolableObjectFactory.java b/src/test/java/org/apache/commons/pool2/MethodCallPoolableObjectFactory.java
index 654b80a..7ab6709 100644
--- a/src/test/java/org/apache/commons/pool2/MethodCallPoolableObjectFactory.java
+++ b/src/test/java/org/apache/commons/pool2/MethodCallPoolableObjectFactory.java
@@ -27,7 +27,7 @@ import org.apache.commons.pool2.impl.DefaultPooledObject;
  *
  * @see MethodCall
  */
-public class MethodCallPoolableObjectFactory implements PooledObjectFactory<Object> {
+public class MethodCallPoolableObjectFactory implements PooledObjectFactory<Object, PrivateException> {
     private final List<MethodCall> methodCalls = new ArrayList<>();
     private int count;
     private boolean valid = true;
@@ -38,7 +38,7 @@ public class MethodCallPoolableObjectFactory implements PooledObjectFactory<Obje
     private boolean destroyObjectFail;
 
     @Override
-    public void activateObject(final PooledObject<Object> obj) throws Exception {
+    public void activateObject(final PooledObject<Object> obj) {
         methodCalls.add(new MethodCall("activateObject", obj.getObject()));
         if (activateObjectFail) {
             throw new PrivateException("activateObject");
@@ -46,7 +46,7 @@ public class MethodCallPoolableObjectFactory implements PooledObjectFactory<Obje
     }
 
     @Override
-    public void destroyObject(final PooledObject<Object> obj) throws Exception {
+    public void destroyObject(final PooledObject<Object> obj) {
         methodCalls.add(new MethodCall("destroyObject", obj.getObject()));
         if (destroyObjectFail) {
             throw new PrivateException("destroyObject");
@@ -86,7 +86,7 @@ public class MethodCallPoolableObjectFactory implements PooledObjectFactory<Obje
     }
 
     @Override
-    public PooledObject<Object> makeObject() throws Exception {
+    public PooledObject<Object> makeObject() {
         final MethodCall call = new MethodCall("makeObject");
         methodCalls.add(call);
         final int originalCount = this.count++;
@@ -100,7 +100,7 @@ public class MethodCallPoolableObjectFactory implements PooledObjectFactory<Obje
     }
 
     @Override
-    public void passivateObject(final PooledObject<Object> obj) throws Exception {
+    public void passivateObject(final PooledObject<Object> obj) {
         methodCalls.add(new MethodCall("passivateObject", obj.getObject()));
         if (passivateObjectFail) {
             throw new PrivateException("passivateObject");
diff --git a/src/test/java/org/apache/commons/pool2/ObjectPoolIssue326.java b/src/test/java/org/apache/commons/pool2/ObjectPoolIssue326.java
index 120c2f0..9123eaa 100644
--- a/src/test/java/org/apache/commons/pool2/ObjectPoolIssue326.java
+++ b/src/test/java/org/apache/commons/pool2/ObjectPoolIssue326.java
@@ -39,9 +39,9 @@ import org.apache.commons.pool2.impl.GenericKeyedObjectPoolConfig;
  * negatively since you need to run it for a while.
  */
 public final class ObjectPoolIssue326 {
-    private class ObjectFactory extends BaseKeyedPooledObjectFactory<Integer, Object> {
+    private class ObjectFactory extends BaseKeyedPooledObjectFactory<Integer, Object, RuntimeException> {
         @Override
-        public Object create(final Integer s) throws Exception {
+        public Object create(final Integer s) {
             return new TestObject();
         }
 
@@ -51,11 +51,11 @@ public final class ObjectPoolIssue326 {
         }
     }
 
-    private class Task implements Callable<Object> {
-        private final GenericKeyedObjectPool<Integer, Object> m_pool;
+    private class Task<E extends Exception> implements Callable<Object> {
+        private final GenericKeyedObjectPool<Integer, Object, E> m_pool;
         private final int m_key;
 
-        Task(final GenericKeyedObjectPool<Integer, Object> pool, final int count) {
+        Task(final GenericKeyedObjectPool<Integer, Object, E> pool, final int count) {
             m_pool = pool;
             m_key = count % 20;
         }
@@ -99,10 +99,10 @@ public final class ObjectPoolIssue326 {
         }
     }
 
-    private List<Task> createTasks(final GenericKeyedObjectPool<Integer, Object> pool) {
-        final List<Task> tasks = new ArrayList<>();
+    private <E extends Exception> List<Task<E>> createTasks(final GenericKeyedObjectPool<Integer, Object, E> pool) {
+        final List<Task<E>> tasks = new ArrayList<>();
         for (int i = 0; i < 250; i++) {
-            tasks.add(new Task(pool, i));
+            tasks.add(new Task<>(pool, i));
         }
         return tasks;
     }
@@ -130,7 +130,7 @@ public final class ObjectPoolIssue326 {
         poolConfig.setJmxNameBase(null);
         poolConfig.setJmxNamePrefix(null);
 
-        final GenericKeyedObjectPool<Integer, Object> pool = new GenericKeyedObjectPool<>(new ObjectFactory(), poolConfig);
+        final GenericKeyedObjectPool<Integer, Object, RuntimeException> pool = new GenericKeyedObjectPool<>(new ObjectFactory(), poolConfig);
 
         // number of threads to reproduce is finicky. this count seems to be best for my
         // 4 core box.
@@ -144,7 +144,7 @@ public final class ObjectPoolIssue326 {
                 if (testIter % 1000 == 0) {
                     System.out.println(testIter);
                 }
-                final List<Task> tasks = createTasks(pool);
+                final List<Task<RuntimeException>> tasks = createTasks(pool);
                 final List<Future<Object>> futures = service.invokeAll(tasks);
                 for (final Future<Object> future : futures) {
                     future.get();
diff --git a/src/test/java/org/apache/commons/pool2/PoolTest.java b/src/test/java/org/apache/commons/pool2/PoolTest.java
index 0290a7d..6dac8a1 100644
--- a/src/test/java/org/apache/commons/pool2/PoolTest.java
+++ b/src/test/java/org/apache/commons/pool2/PoolTest.java
@@ -31,26 +31,28 @@ import org.junit.jupiter.api.Test;
 
 @Disabled
 public class PoolTest {
+
     private static class Foo {
     }
-    private static class PooledFooFactory implements PooledObjectFactory<Foo> {
+
+    private static class PooledFooFactory implements PooledObjectFactory<Foo, RuntimeException> {
         private static final long VALIDATION_WAIT_IN_MILLIS = 1000;
 
         @Override
-        public void activateObject(final PooledObject<Foo> pooledObject) throws Exception {
+        public void activateObject(final PooledObject<Foo> pooledObject) {
         }
 
         @Override
-        public void destroyObject(final PooledObject<Foo> pooledObject) throws Exception {
+        public void destroyObject(final PooledObject<Foo> pooledObject) {
         }
 
         @Override
-        public PooledObject<Foo> makeObject() throws Exception {
+        public PooledObject<Foo> makeObject() {
             return new DefaultPooledObject<>(new Foo());
         }
 
         @Override
-        public void passivateObject(final PooledObject<Foo> pooledObject) throws Exception {
+        public void passivateObject(final PooledObject<Foo> pooledObject) {
         }
 
         @Override
@@ -73,7 +75,7 @@ public class PoolTest {
         final GenericObjectPoolConfig<Foo> poolConfig = new GenericObjectPoolConfig<>();
         poolConfig.setTestWhileIdle(true /* testWhileIdle */);
         final PooledFooFactory pooledFooFactory = new PooledFooFactory();
-        try (GenericObjectPool<Foo> pool = new GenericObjectPool<>(pooledFooFactory, poolConfig)) {
+        try (GenericObjectPool<Foo, RuntimeException> pool = new GenericObjectPool<>(pooledFooFactory, poolConfig)) {
             pool.setTimeBetweenEvictionRunsMillis(EVICTION_PERIOD_IN_MILLIS);
             assertEquals(EVICTION_PERIOD_IN_MILLIS, pool.getDurationBetweenEvictionRuns().toMillis());
             assertEquals(EVICTION_PERIOD_IN_MILLIS, pool.getTimeBetweenEvictionRuns().toMillis());
diff --git a/src/test/java/org/apache/commons/pool2/TestBaseKeyedPoolableObjectFactory.java b/src/test/java/org/apache/commons/pool2/TestBaseKeyedPoolableObjectFactory.java
index e939cca..fe8500a 100644
--- a/src/test/java/org/apache/commons/pool2/TestBaseKeyedPoolableObjectFactory.java
+++ b/src/test/java/org/apache/commons/pool2/TestBaseKeyedPoolableObjectFactory.java
@@ -25,10 +25,9 @@ import org.junit.jupiter.api.Test;
  */
 public class TestBaseKeyedPoolableObjectFactory {
 
-    private static class TestFactory
-            extends BaseKeyedPooledObjectFactory<Object,Object> {
+    private static class TestFactory extends BaseKeyedPooledObjectFactory<Object, Object, RuntimeException> {
         @Override
-        public Object create(final Object key) throws Exception {
+        public Object create(final Object key) {
             return null;
         }
         @Override
@@ -38,8 +37,8 @@ public class TestBaseKeyedPoolableObjectFactory {
     }
 
     @Test
-    public void testDefaultMethods() throws Exception {
-        final KeyedPooledObjectFactory<Object,Object> factory = new TestFactory();
+    public void testDefaultMethods() {
+        final KeyedPooledObjectFactory<Object, Object, RuntimeException> factory = new TestFactory();
 
         factory.activateObject("key",null); // a no-op
         factory.passivateObject("key",null); // a no-op
diff --git a/src/test/java/org/apache/commons/pool2/TestBaseObjectPool.java b/src/test/java/org/apache/commons/pool2/TestBaseObjectPool.java
index faa1f70..edb4da2 100644
--- a/src/test/java/org/apache/commons/pool2/TestBaseObjectPool.java
+++ b/src/test/java/org/apache/commons/pool2/TestBaseObjectPool.java
@@ -26,20 +26,23 @@ import org.junit.jupiter.api.Test;
 /**
  */
 public class TestBaseObjectPool extends TestObjectPool {
-    private static class TestObjectPool extends BaseObjectPool<Object> {
+    private static class TestObjectPool extends BaseObjectPool<Object, RuntimeException> {
+        
         @Override
         public Object borrowObject() {
             return null;
         }
+        
         @Override
         public void invalidateObject(final Object obj) {
         }
+        
         @Override
         public void returnObject(final Object obj) {
         }
     }
 
-    private ObjectPool<String> pool;
+    private ObjectPool<String, RuntimeException> pool;
 
     /**
      * @param n Ignored by this implemented. Used by sub-classes.
@@ -72,7 +75,7 @@ public class TestBaseObjectPool extends TestObjectPool {
      *
      * @return A newly created empty pool
      */
-    protected ObjectPool<String> makeEmptyPool(final int minCapacity) {
+    protected <E extends Exception> ObjectPool<String, E> makeEmptyPool(final int minCapacity) {
         if (this.getClass() != TestBaseObjectPool.class) {
             fail("Subclasses of TestBaseObjectPool must reimplement this method.");
         }
@@ -80,7 +83,7 @@ public class TestBaseObjectPool extends TestObjectPool {
     }
 
     @Override
-    protected ObjectPool<Object> makeEmptyPool(final PooledObjectFactory<Object> factory) {
+    protected <E extends Exception> ObjectPool<Object, E> makeEmptyPool(final PooledObjectFactory<Object, E> factory) {
         if (this.getClass() != TestBaseObjectPool.class) {
             fail("Subclasses of TestBaseObjectPool must reimplement this method.");
         }
@@ -254,7 +257,7 @@ public class TestBaseObjectPool extends TestObjectPool {
     @Test
     public void testClose() {
         @SuppressWarnings("resource")
-        final ObjectPool<Object> pool = new TestObjectPool();
+        final ObjectPool<Object, RuntimeException> pool = new TestObjectPool();
 
         pool.close();
         pool.close(); // should not error as of Pool 2.0.
@@ -262,11 +265,11 @@ public class TestBaseObjectPool extends TestObjectPool {
 
     // tests
     @Test
-    public void testUnsupportedOperations() throws Exception {
+    public void testUnsupportedOperations() {
         if (!getClass().equals(TestBaseObjectPool.class)) {
             return; // skip redundant tests
         }
-        try (final ObjectPool<Object> pool = new TestObjectPool()) {
+        try (final ObjectPool<Object,RuntimeException> pool = new TestObjectPool()) {
 
             assertTrue( pool.getNumIdle() < 0,"Negative expected.");
             assertTrue( pool.getNumActive() < 0,"Negative expected.");
diff --git a/src/test/java/org/apache/commons/pool2/TestBasePoolableObjectFactory.java b/src/test/java/org/apache/commons/pool2/TestBasePoolableObjectFactory.java
index 43935d5..ceff13c 100644
--- a/src/test/java/org/apache/commons/pool2/TestBasePoolableObjectFactory.java
+++ b/src/test/java/org/apache/commons/pool2/TestBasePoolableObjectFactory.java
@@ -28,9 +28,9 @@ import org.junit.jupiter.api.Test;
  */
 public class TestBasePoolableObjectFactory {
 
-    private static class TestFactory extends BasePooledObjectFactory<AtomicInteger> {
+    private static class TestFactory extends BasePooledObjectFactory<AtomicInteger, RuntimeException> {
         @Override
-        public AtomicInteger create() throws Exception {
+        public AtomicInteger create() {
             return new AtomicInteger(0);
         }
         @Override
@@ -46,8 +46,8 @@ public class TestBasePoolableObjectFactory {
     }
 
     @Test
-    public void testDefaultMethods() throws Exception {
-        final PooledObjectFactory<AtomicInteger> factory = new TestFactory();
+    public void testDefaultMethods() {
+        final PooledObjectFactory<AtomicInteger, RuntimeException> factory = new TestFactory();
 
         factory.activateObject(null); // a no-op
         factory.passivateObject(null); // a no-op
@@ -60,11 +60,11 @@ public class TestBasePoolableObjectFactory {
      * increments the value.  Verify that destroy with no mode does default,
      * destroy with ABANDONED mode increments.
      *
-     * @throws Exception May occur in some failure modes
+     * @throws RuntimeException May occur in some failure modes
      */
     @Test
-    public void testDestroyModes() throws Exception {
-        final PooledObjectFactory<AtomicInteger> factory = new TestFactory();
+    public void testDestroyModes() {
+        final PooledObjectFactory<AtomicInteger, RuntimeException> factory = new TestFactory();
         final PooledObject<AtomicInteger> pooledObj = factory.makeObject();
         final AtomicInteger obj = pooledObj.getObject();
         factory.destroyObject(pooledObj);
diff --git a/src/test/java/org/apache/commons/pool2/TestKeyedObjectPool.java b/src/test/java/org/apache/commons/pool2/TestKeyedObjectPool.java
index 7fbb573..ef95557 100644
--- a/src/test/java/org/apache/commons/pool2/TestKeyedObjectPool.java
+++ b/src/test/java/org/apache/commons/pool2/TestKeyedObjectPool.java
@@ -34,7 +34,7 @@ import org.junit.jupiter.api.Test;
  */
 public abstract class TestKeyedObjectPool {
 
-    protected static class FailingKeyedPooledObjectFactory implements KeyedPooledObjectFactory<Object,Object> {
+    protected static class FailingKeyedPooledObjectFactory implements KeyedPooledObjectFactory<Object, Object, PrivateException> {
         private final List<MethodCall> methodCalls = new ArrayList<>();
         private int count;
         private boolean makeObjectFail;
@@ -47,7 +47,7 @@ public abstract class TestKeyedObjectPool {
         }
 
         @Override
-        public void activateObject(final Object key, final PooledObject<Object> obj) throws Exception {
+        public void activateObject(final Object key, final PooledObject<Object> obj) {
             methodCalls.add(new MethodCall("activateObject", key, obj.getObject()));
             if (activateObjectFail) {
                 throw new PrivateException("activateObject");
@@ -55,7 +55,7 @@ public abstract class TestKeyedObjectPool {
         }
 
         @Override
-        public void destroyObject(final Object key, final PooledObject<Object> obj) throws Exception {
+        public void destroyObject(final Object key, final PooledObject<Object> obj) {
             methodCalls.add(new MethodCall("destroyObject", key, obj.getObject()));
             if (destroyObjectFail) {
                 throw new PrivateException("destroyObject");
@@ -91,7 +91,7 @@ public abstract class TestKeyedObjectPool {
         }
 
         @Override
-        public PooledObject<Object> makeObject(final Object key) throws Exception {
+        public PooledObject<Object> makeObject(final Object key) {
             final MethodCall call = new MethodCall("makeObject", key);
             methodCalls.add(call);
             final int originalCount = this.count++;
@@ -106,7 +106,7 @@ public abstract class TestKeyedObjectPool {
         }
 
         @Override
-        public void passivateObject(final Object key, final PooledObject<Object> obj) throws Exception {
+        public void passivateObject(final Object key, final PooledObject<Object> obj) {
             methodCalls.add(new MethodCall("passivateObject", key, obj.getObject()));
             if (passivateObjectFail) {
                 throw new PrivateException("passivateObject");
@@ -160,10 +160,9 @@ public abstract class TestKeyedObjectPool {
         }
     }
 
-    private static class TestFactory
-            extends BaseKeyedPooledObjectFactory<Object,Object> {
+    private static class TestFactory extends BaseKeyedPooledObjectFactory<Object, Object, RuntimeException> {
         @Override
-        public Object create(final Object key) throws Exception {
+        public Object create(final Object key) {
             return new Object();
         }
         @Override
@@ -174,7 +173,7 @@ public abstract class TestKeyedObjectPool {
 
     protected static final String KEY = "key";
 
-    private KeyedObjectPool<Object,Object> pool;
+    private KeyedObjectPool<Object, Object, RuntimeException> pool;
 
     // Deliberate choice to create a new object in case future unit tests check
     // for a specific object.
@@ -212,7 +211,7 @@ public abstract class TestKeyedObjectPool {
      *
      * @return the newly created keyed object pool
      */
-    protected abstract KeyedObjectPool<Object,Object> makeEmptyPool(int minCapacity);
+    protected abstract <E extends Exception> KeyedObjectPool<Object, Object, E> makeEmptyPool(int minCapacity);
 
     /**
      * Creates an {@code KeyedObjectPool} with the specified factory.
@@ -220,14 +219,16 @@ public abstract class TestKeyedObjectPool {
      * behaviors described in {@link KeyedObjectPool}.
      * Generally speaking there should be no limits on the various object counts.
      *
+     * @param <E> The type of exception thrown by the pool 
      * @param factory Factory to use to associate with the pool
      * @return The newly created empty pool
      */
-    protected abstract KeyedObjectPool<Object, Object> makeEmptyPool(KeyedPooledObjectFactory<Object, Object> factory);
+    protected abstract <E extends Exception> KeyedObjectPool<Object, Object, E> makeEmptyPool(KeyedPooledObjectFactory<Object, Object, E> factory);
 
     protected abstract Object makeKey(int n);
 
-    private void reset(final KeyedObjectPool<Object,Object> pool, final FailingKeyedPooledObjectFactory factory, final List<MethodCall> expectedMethods) throws Exception {
+    private <E extends Exception> void reset(final KeyedObjectPool<Object, Object, E> pool, final FailingKeyedPooledObjectFactory factory,
+        final List<MethodCall> expectedMethods) throws E {
         pool.clear();
         clear(factory, expectedMethods);
         factory.reset();
@@ -469,8 +470,8 @@ public abstract class TestKeyedObjectPool {
     }
 
     @Test
-    public void testClosedPoolBehavior() throws Exception {
-        final KeyedObjectPool<Object,Object> pool;
+    public void testClosedPoolBehavior() {
+        final KeyedObjectPool<Object, Object, RuntimeException> pool;
         try {
             pool = makeEmptyPool(new TestFactory());
         } catch(final UnsupportedOperationException uoe) {
@@ -503,9 +504,9 @@ public abstract class TestKeyedObjectPool {
     }
 
     @Test
-    public void testKPOFAddObjectUsage() throws Exception {
+    public void testKPOFAddObjectUsage() {
         final FailingKeyedPooledObjectFactory factory = new FailingKeyedPooledObjectFactory();
-        final KeyedObjectPool<Object,Object> pool;
+        final KeyedObjectPool<Object, Object, PrivateException> pool;
         try {
             pool = makeEmptyPool(factory);
         } catch(final UnsupportedOperationException uoe) {
@@ -541,9 +542,9 @@ public abstract class TestKeyedObjectPool {
     }
 
     @Test
-    public void testKPOFBorrowObjectUsages() throws Exception {
+    public void testKPOFBorrowObjectUsages() {
         final FailingKeyedPooledObjectFactory factory = new FailingKeyedPooledObjectFactory();
-        final KeyedObjectPool<Object,Object> pool;
+        final KeyedObjectPool<Object, Object, PrivateException> pool;
         try {
             pool = makeEmptyPool(factory);
         } catch(final UnsupportedOperationException uoe) {
@@ -553,7 +554,7 @@ public abstract class TestKeyedObjectPool {
         Object obj;
 
         if (pool instanceof GenericKeyedObjectPool) {
-            ((GenericKeyedObjectPool<Object,Object>) pool).setTestOnBorrow(true);
+            ((GenericKeyedObjectPool<Object, Object, PrivateException>) pool).setTestOnBorrow(true);
         }
 
         /// Test correct behavior code paths
@@ -619,9 +620,9 @@ public abstract class TestKeyedObjectPool {
     }
 
     @Test
-    public void testKPOFClearUsages() throws Exception {
+    public void testKPOFClearUsages() {
         final FailingKeyedPooledObjectFactory factory = new FailingKeyedPooledObjectFactory();
-        final KeyedObjectPool<Object,Object> pool;
+        final KeyedObjectPool<Object, Object, PrivateException> pool;
         try {
             pool = makeEmptyPool(factory);
         } catch(final UnsupportedOperationException uoe) {
@@ -643,9 +644,9 @@ public abstract class TestKeyedObjectPool {
 
 
     @Test
-    public void testKPOFCloseUsages() throws Exception {
+    public void testKPOFCloseUsages() {
         final FailingKeyedPooledObjectFactory factory = new FailingKeyedPooledObjectFactory();
-        KeyedObjectPool<Object, Object> pool;
+        KeyedObjectPool<Object, Object, PrivateException> pool;
         try {
             pool = makeEmptyPool(factory);
         } catch (final UnsupportedOperationException uoe) {
@@ -658,7 +659,7 @@ public abstract class TestKeyedObjectPool {
         pool.close();
 
         //// Test exception handling close should swallow failures
-        try (final KeyedObjectPool<Object, Object> pool2 = makeEmptyPool(factory)) {
+        try (final KeyedObjectPool<Object, Object, PrivateException> pool2 = makeEmptyPool(factory)) {
             reset(pool2, factory, expectedMethods);
             factory.setDestroyObjectFail(true);
             pool2.addObjects(KEY, 5);
@@ -668,7 +669,7 @@ public abstract class TestKeyedObjectPool {
     @Test
     public void testKPOFInvalidateObjectUsages() throws Exception {
         final FailingKeyedPooledObjectFactory factory = new FailingKeyedPooledObjectFactory();
-        final KeyedObjectPool<Object,Object> pool;
+        final KeyedObjectPool<Object, Object, PrivateException> pool;
         try {
             pool = makeEmptyPool(factory);
         } catch(final UnsupportedOperationException uoe) {
@@ -700,9 +701,9 @@ public abstract class TestKeyedObjectPool {
     }
 
     @Test
-    public void testKPOFReturnObjectUsages() throws Exception {
+    public void testKPOFReturnObjectUsages() {
         final FailingKeyedPooledObjectFactory factory = new FailingKeyedPooledObjectFactory();
-        final KeyedObjectPool<Object,Object> pool;
+        final KeyedObjectPool<Object, Object, PrivateException> pool;
         try {
             pool = makeEmptyPool(factory);
         } catch(final UnsupportedOperationException uoe) {
@@ -759,11 +760,10 @@ public abstract class TestKeyedObjectPool {
 
     @Test
     public void testToString() {
-        final FailingKeyedPooledObjectFactory factory =
-                new FailingKeyedPooledObjectFactory();
-        try (final KeyedObjectPool<Object,Object> pool = makeEmptyPool(factory)) {
+        final FailingKeyedPooledObjectFactory factory = new FailingKeyedPooledObjectFactory();
+        try (final KeyedObjectPool<Object, Object, PrivateException> pool = makeEmptyPool(factory)) {
             pool.toString();
-        } catch(final UnsupportedOperationException uoe) {
+        } catch (final UnsupportedOperationException uoe) {
             return; // test not supported
         }
     }
diff --git a/src/test/java/org/apache/commons/pool2/TestObjectPool.java b/src/test/java/org/apache/commons/pool2/TestObjectPool.java
index 2b1bc89..cc04b00 100644
--- a/src/test/java/org/apache/commons/pool2/TestObjectPool.java
+++ b/src/test/java/org/apache/commons/pool2/TestObjectPool.java
@@ -20,7 +20,6 @@ package org.apache.commons.pool2;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.junit.jupiter.api.Assertions.fail;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -44,7 +43,7 @@ public abstract class TestObjectPool {
         calls.removeIf(call -> "destroyObject".equals(call.getName()));
     }
 
-    private static void reset(final ObjectPool<Object> pool, final MethodCallPoolableObjectFactory factory, final List<MethodCall> expectedMethods) throws Exception {
+    private static void reset(final ObjectPool<Object, ?> pool, final MethodCallPoolableObjectFactory factory, final List<MethodCall> expectedMethods) throws Exception {
         pool.clear();
         clear(factory, expectedMethods);
         factory.reset();
@@ -61,6 +60,7 @@ public abstract class TestObjectPool {
      * behaviors described in {@link ObjectPool}.
      * Generally speaking there should be no limits on the various object counts.
      *
+     * @param <E> The exception type throws by the pool
      * @param factory The factory to be used by the object pool
      *
      * @return the newly created empty pool
@@ -68,11 +68,11 @@ public abstract class TestObjectPool {
      * @throws UnsupportedOperationException if the pool being tested does not
      *                                       follow pool contracts.
      */
-    protected abstract ObjectPool<Object> makeEmptyPool(PooledObjectFactory<Object> factory) throws UnsupportedOperationException;
+    protected abstract <E extends Exception> ObjectPool<Object, E> makeEmptyPool(PooledObjectFactory<Object, E> factory) throws UnsupportedOperationException;
 
     @Test
     public void testClosedPoolBehavior() throws Exception {
-        final ObjectPool<Object> pool;
+        final ObjectPool<Object, PrivateException> pool;
         try {
             pool = makeEmptyPool(new MethodCallPoolableObjectFactory());
         } catch (final UnsupportedOperationException uoe) {
@@ -115,7 +115,7 @@ public abstract class TestObjectPool {
     @Test
     public void testPOFAddObjectUsage() throws Exception {
         final MethodCallPoolableObjectFactory factory = new MethodCallPoolableObjectFactory();
-        final ObjectPool<Object> pool;
+        final ObjectPool<Object, PrivateException> pool;
         try {
             pool = makeEmptyPool(factory);
         } catch(final UnsupportedOperationException uoe) {
@@ -166,14 +166,14 @@ public abstract class TestObjectPool {
     @Test
     public void testPOFBorrowObjectUsages() throws Exception {
         final MethodCallPoolableObjectFactory factory = new MethodCallPoolableObjectFactory();
-        final ObjectPool<Object> pool;
+        final ObjectPool<Object, PrivateException> pool;
         try {
             pool = makeEmptyPool(factory);
         } catch (final UnsupportedOperationException uoe) {
             return; // test not supported
         }
         if (pool instanceof GenericObjectPool) {
-            ((GenericObjectPool<Object>) pool).setTestOnBorrow(true);
+            ((GenericObjectPool<Object, PrivateException>) pool).setTestOnBorrow(true);
         }
         final List<MethodCall> expectedMethods = new ArrayList<>();
         Object obj;
@@ -238,7 +238,7 @@ public abstract class TestObjectPool {
     @Test
     public void testPOFClearUsages() throws Exception {
         final MethodCallPoolableObjectFactory factory = new MethodCallPoolableObjectFactory();
-        final ObjectPool<Object> pool;
+        final ObjectPool<Object, PrivateException> pool;
         try {
             pool = makeEmptyPool(factory);
         } catch (final UnsupportedOperationException uoe) {
@@ -261,7 +261,7 @@ public abstract class TestObjectPool {
     @Test
     public void testPOFCloseUsages() throws Exception {
         final MethodCallPoolableObjectFactory factory = new MethodCallPoolableObjectFactory();
-        ObjectPool<Object> pool;
+        ObjectPool<Object, PrivateException> pool;
         try {
             pool = makeEmptyPool(factory);
         } catch (final UnsupportedOperationException uoe) {
@@ -289,7 +289,7 @@ public abstract class TestObjectPool {
     @Test
     public void testPOFInvalidateObjectUsages() throws Exception {
         final MethodCallPoolableObjectFactory factory = new MethodCallPoolableObjectFactory();
-        final ObjectPool<Object> pool;
+        final ObjectPool<Object, PrivateException> pool;
         try {
             pool = makeEmptyPool(factory);
         } catch (final UnsupportedOperationException uoe) {
@@ -323,7 +323,7 @@ public abstract class TestObjectPool {
     @Test
     public void testPOFReturnObjectUsages() throws Exception {
         final MethodCallPoolableObjectFactory factory = new MethodCallPoolableObjectFactory();
-        final ObjectPool<Object> pool;
+        final ObjectPool<Object, PrivateException> pool;
         try {
             pool = makeEmptyPool(factory);
         } catch (final UnsupportedOperationException uoe) {
@@ -382,7 +382,7 @@ public abstract class TestObjectPool {
 
     @Test
     public void testToString() {
-        final ObjectPool<Object> pool;
+        final ObjectPool<Object, PrivateException> pool;
         try {
             pool = makeEmptyPool(new MethodCallPoolableObjectFactory());
         } catch (final UnsupportedOperationException uoe) {
diff --git a/src/test/java/org/apache/commons/pool2/TestPoolUtils.java b/src/test/java/org/apache/commons/pool2/TestPoolUtils.java
index de0efb2..ccbcb54 100644
--- a/src/test/java/org/apache/commons/pool2/TestPoolUtils.java
+++ b/src/test/java/org/apache/commons/pool2/TestPoolUtils.java
@@ -100,12 +100,13 @@ public class TestPoolUtils {
         return createProxy(clazz, new MethodCallLogger(logger));
     }
 
-    private static List<String> invokeEveryMethod(final KeyedObjectPool<Object,Object> kop) throws Exception {
+    private static List<String> invokeEveryMethod(final KeyedObjectPool<Object, Object, RuntimeException> kop) {
         kop.addObject(null);
         kop.borrowObject(null);
         kop.clear();
         kop.clear(null);
         kop.close();
+        kop.getKeys();
         kop.getNumActive();
         kop.getNumActive(null);
         kop.getNumIdle();
@@ -114,11 +115,11 @@ public class TestPoolUtils {
         kop.returnObject(null, new Object());
         kop.toString();
 
-        return Arrays.asList("addObject", "borrowObject", "clear", "clear", "close", "getNumActive", "getNumActive",
-                "getNumIdle", "getNumIdle", "invalidateObject", "returnObject", "toString");
+        return Arrays.asList("addObject", "borrowObject", "clear", "clear", "close", "getKeys", "getNumActive", "getNumActive", "getNumIdle", "getNumIdle",
+                "invalidateObject", "returnObject", "toString");
     }
 
-    private static <K, V> List<String> invokeEveryMethod(final KeyedPooledObjectFactory<K, V> kpof) throws Exception {
+    private static <K, V> List<String> invokeEveryMethod(final KeyedPooledObjectFactory<K, V, RuntimeException> kpof) {
         kpof.activateObject(null, null);
         kpof.destroyObject(null, null);
         kpof.makeObject(null);
@@ -129,7 +130,7 @@ public class TestPoolUtils {
         return Arrays.asList("activateObject", "destroyObject", "makeObject", "passivateObject", "validateObject", "toString");
     }
 
-    private static List<String> invokeEveryMethod(final ObjectPool<Object> op) throws Exception {
+    private static List<String> invokeEveryMethod(final ObjectPool<Object, RuntimeException> op) {
         op.addObject();
         op.borrowObject();
         op.clear();
@@ -144,7 +145,7 @@ public class TestPoolUtils {
                 "returnObject", "toString");
     }
 
-    private static <T> List<String> invokeEveryMethod(final PooledObjectFactory<T> pof) throws Exception {
+    private static <T, E extends Exception> List<String> invokeEveryMethod(final PooledObjectFactory<T, E> pof) throws E {
         pof.activateObject(null);
         pof.destroyObject(null);
         pof.makeObject();
@@ -160,12 +161,12 @@ public class TestPoolUtils {
         assertThrows(IllegalArgumentException.class, () -> PoolUtils.checkMinIdle(null, new Object(), 1, 1),
                 "PoolUtils.checkMinIdle(KeyedObjectPool,Object,int,long) must not allow null pool.");
         try (@SuppressWarnings("unchecked")
-        final KeyedObjectPool<Object, Object> pool = createProxy(KeyedObjectPool.class, (List<String>) null)) {
+        final KeyedObjectPool<Object, Object, RuntimeException> pool = createProxy(KeyedObjectPool.class, (List<String>) null)) {
             assertThrows(IllegalArgumentException.class, () -> PoolUtils.checkMinIdle(pool, (Object) null, 1, 1),
                     "PoolUtils.checkMinIdle(KeyedObjectPool,Object,int,long) must not accept null keys.");
         }
         try (@SuppressWarnings("unchecked")
-        final KeyedObjectPool<Object, Object> pool = createProxy(KeyedObjectPool.class, (List<String>) null)) {
+        final KeyedObjectPool<Object, Object, RuntimeException> pool = createProxy(KeyedObjectPool.class, (List<String>) null)) {
             assertThrows(IllegalArgumentException.class, () -> PoolUtils.checkMinIdle(pool, new Object(), -1, 1),
                     "PoolUtils.checkMinIdle(KeyedObjectPool,Object,int,long) must not accept negative min idle values.");
         }
@@ -175,8 +176,8 @@ public class TestPoolUtils {
 
         // Test that the minIdle check doesn't add too many idle objects
         @SuppressWarnings("unchecked")
-        final KeyedPooledObjectFactory<Object, Object> kpof = createProxy(KeyedPooledObjectFactory.class, calledMethods);
-        try (final KeyedObjectPool<Object, Object> kop = new GenericKeyedObjectPool<>(kpof)) {
+        final KeyedPooledObjectFactory<Object, Object, RuntimeException> kpof = createProxy(KeyedPooledObjectFactory.class, calledMethods);
+        try (final KeyedObjectPool<Object, Object, RuntimeException> kop = new GenericKeyedObjectPool<>(kpof)) {
             PoolUtils.checkMinIdle(kop, key, 2, 100);
             Thread.sleep(400);
             assertEquals(2, kop.getNumIdle(key));
@@ -200,7 +201,7 @@ public class TestPoolUtils {
             try {
                 calledMethods.clear();
                 try (@SuppressWarnings("unchecked")
-                final KeyedObjectPool<Object, Object> pool = createProxy(KeyedObjectPool.class, calledMethods)) {
+                final KeyedObjectPool<Object, Object, RuntimeException> pool = createProxy(KeyedObjectPool.class, calledMethods)) {
                     // checks minIdle immediately
                     final TimerTask task = PoolUtils.checkMinIdle(pool, key, 1, CHECK_PERIOD);
 
@@ -226,7 +227,7 @@ public class TestPoolUtils {
     }
 
     @Test
-    public void testCheckMinIdleKeyedObjectPoolKeys() throws Exception {
+    public void testCheckMinIdleKeyedObjectPoolKeys() throws InterruptedException {
         // Because this isn't deterministic and you can get false failures, try more than once.
         AssertionFailedError afe = null;
         int triesLeft = 3;
@@ -234,7 +235,7 @@ public class TestPoolUtils {
             afe = null;
             final List<String> calledMethods = new ArrayList<>();
             try (@SuppressWarnings("unchecked")
-            final KeyedObjectPool<String, Object> pool = createProxy(KeyedObjectPool.class, calledMethods)) {
+            final KeyedObjectPool<String, Object, RuntimeException> pool = createProxy(KeyedObjectPool.class, calledMethods)) {
                 final Collection<String> keys = new ArrayList<>(2);
                 keys.add("one");
                 keys.add("two");
@@ -264,13 +265,13 @@ public class TestPoolUtils {
     @Test
     public void testCheckMinIdleKeyedObjectPoolKeysNulls() {
         try (@SuppressWarnings("unchecked")
-        final KeyedObjectPool<Object, Object> pool = createProxy(KeyedObjectPool.class, (List<String>) null)) {
+        final KeyedObjectPool<Object, Object, RuntimeException> pool = createProxy(KeyedObjectPool.class, (List<String>) null)) {
             assertThrows(IllegalArgumentException.class, () -> PoolUtils.checkMinIdle(pool, (Collection<?>) null, 1, 1),
                     "PoolUtils.checkMinIdle(KeyedObjectPool,Collection,int,long) must not accept null keys.");
         }
 
         try (@SuppressWarnings("unchecked")
-        final KeyedObjectPool<Object, Object> pool = createProxy(KeyedObjectPool.class, (List<String>) null)) {
+        final KeyedObjectPool<Object, Object, RuntimeException> pool = createProxy(KeyedObjectPool.class, (List<String>) null)) {
             PoolUtils.checkMinIdle(pool, (Collection<?>) Collections.emptyList(), 1, 1);
         } catch (final IllegalArgumentException iae) {
             fail("PoolUtils.checkMinIdle(KeyedObjectPool,Collection,int,long) must accept empty lists.");
@@ -278,11 +279,11 @@ public class TestPoolUtils {
     }
 
     @Test
-    public void testCheckMinIdleObjectPool() throws Exception {
+    public void testCheckMinIdleObjectPool() throws InterruptedException {
         assertThrows(IllegalArgumentException.class, () -> PoolUtils.checkMinIdle(null, 1, 1),
                 "PoolUtils.checkMinIdle(ObjectPool,,) must not allow null pool.");
         try (@SuppressWarnings("unchecked")
-        final ObjectPool<Object> pool = createProxy(ObjectPool.class, (List<String>) null)) {
+        final ObjectPool<Object, RuntimeException> pool = createProxy(ObjectPool.class, (List<String>) null)) {
             assertThrows(IllegalArgumentException.class, () -> PoolUtils.checkMinIdle(pool, -1, 1),
                     "PoolUtils.checkMinIdle(ObjectPool,,) must not accept negative min idle values.");
         }
@@ -291,8 +292,8 @@ public class TestPoolUtils {
 
         // Test that the minIdle check doesn't add too many idle objects
         @SuppressWarnings("unchecked")
-        final PooledObjectFactory<Object> pof = createProxy(PooledObjectFactory.class, calledMethods);
-        try (final ObjectPool<Object> op = new GenericObjectPool<>(pof)) {
+        final PooledObjectFactory<Object, RuntimeException> pof = createProxy(PooledObjectFactory.class, calledMethods);
+        try (final ObjectPool<Object, RuntimeException> op = new GenericObjectPool<>(pof)) {
             PoolUtils.checkMinIdle(op, 2, 100);
             Thread.sleep(1000);
             assertEquals(2, op.getNumIdle());
@@ -315,7 +316,7 @@ public class TestPoolUtils {
             try {
                 calledMethods.clear();
                 try (@SuppressWarnings("unchecked")
-                final ObjectPool<Object> pool = createProxy(ObjectPool.class, calledMethods)) {
+                final ObjectPool<Object, RuntimeException> pool = createProxy(ObjectPool.class, calledMethods)) {
                     final TimerTask task = PoolUtils.checkMinIdle(pool, 1, CHECK_PERIOD); // checks minIdle immediately
 
                     Thread.sleep(CHECK_SLEEP_PERIOD); // will check CHECK_COUNT more times.
@@ -367,8 +368,8 @@ public class TestPoolUtils {
     @Test
     public void testErodingObjectPoolDefaultFactor() {
         try (@SuppressWarnings("unchecked")
-        final ObjectPool<Object> internalPool = createProxy(ObjectPool.class, (arg0, arg1, arg2) -> null);
-                final ObjectPool<Object> pool = PoolUtils.erodingPool(internalPool)) {
+        final ObjectPool<Object, RuntimeException> internalPool = createProxy(ObjectPool.class, (arg0, arg1, arg2) -> null);
+                final ObjectPool<Object, RuntimeException> pool = PoolUtils.erodingPool(internalPool)) {
             final String expectedToString = "ErodingObjectPool{factor=ErodingFactor{factor=1.0, idleHighWaterMark=1}, pool=" +
                     internalPool + "}";
             // The factor is not exposed, but will be printed in the toString() method
@@ -378,14 +379,14 @@ public class TestPoolUtils {
     }
 
     @Test
-    public void testErodingPerKeyKeyedObjectPool() throws Exception {
-        assertThrows(IllegalArgumentException.class, () -> PoolUtils.erodingPool((KeyedObjectPool<Object, Object>) null, 1f, true),
+    public void testErodingPerKeyKeyedObjectPool() throws InterruptedException {
+        assertThrows(IllegalArgumentException.class, () -> PoolUtils.erodingPool((KeyedObjectPool<Object, Object, RuntimeException>) null, 1f, true),
                 "PoolUtils.erodingPool(KeyedObjectPool) must not allow a null pool.");
 
-        assertThrows(IllegalArgumentException.class, () -> PoolUtils.erodingPool((KeyedObjectPool<Object, Object>) null, 0f, true),
+        assertThrows(IllegalArgumentException.class, () -> PoolUtils.erodingPool((KeyedObjectPool<Object, Object, RuntimeException>) null, 0f, true),
                 "PoolUtils.erodingPool(ObjectPool, float, boolean) must not allow a non-positive factor.");
 
-        assertThrows(IllegalArgumentException.class, () -> PoolUtils.erodingPool((KeyedObjectPool<Object, Object>) null, 1f, true),
+        assertThrows(IllegalArgumentException.class, () -> PoolUtils.erodingPool((KeyedObjectPool<Object, Object, RuntimeException>) null, 1f, true),
                 "PoolUtils.erodingPool(KeyedObjectPool, float, boolean) must not allow a null pool.");
 
         final List<String> calledMethods = new ArrayList<>();
@@ -404,7 +405,7 @@ public class TestPoolUtils {
         // If the logic behind PoolUtils.erodingPool changes then this will need to be tweaked.
         final float factor = 0.01f; // about ~9 seconds until first discard
         try (@SuppressWarnings("unchecked")
-        final KeyedObjectPool<Object, Object> pool = PoolUtils.erodingPool(createProxy(KeyedObjectPool.class, handler), factor, true)) {
+        final KeyedObjectPool<Object, Object, RuntimeException> pool = PoolUtils.erodingPool(createProxy(KeyedObjectPool.class, handler), factor, true)) {
 
             final List<String> expectedMethods = new ArrayList<>();
             assertEquals(expectedMethods, calledMethods);
@@ -450,14 +451,14 @@ public class TestPoolUtils {
     }
 
     @Test
-    public void testErodingPoolKeyedObjectPool() throws Exception {
-        assertThrows(IllegalArgumentException.class, () -> PoolUtils.erodingPool((KeyedObjectPool<Object, Object>) null),
+    public void testErodingPoolKeyedObjectPool() throws InterruptedException {
+        assertThrows(IllegalArgumentException.class, () -> PoolUtils.erodingPool((KeyedObjectPool<Object, Object, RuntimeException>) null),
                 "PoolUtils.erodingPool(KeyedObjectPool) must not allow a null pool.");
 
-        assertThrows(IllegalArgumentException.class, () -> PoolUtils.erodingPool((KeyedObjectPool<Object, Object>) null, 1f),
+        assertThrows(IllegalArgumentException.class, () -> PoolUtils.erodingPool((KeyedObjectPool<Object, Object, RuntimeException>) null, 1f),
                 "PoolUtils.erodingPool(KeyedObjectPool, float) must not allow a null pool.");
 
-        assertThrows(IllegalArgumentException.class, () -> PoolUtils.erodingPool((KeyedObjectPool<Object, Object>) null, 1f, true),
+        assertThrows(IllegalArgumentException.class, () -> PoolUtils.erodingPool((KeyedObjectPool<Object, Object, RuntimeException>) null, 1f, true),
                 "PoolUtils.erodingPool(KeyedObjectPool, float, boolean) must not allow a null pool.");
 
         final List<String> calledMethods = new ArrayList<>();
@@ -483,7 +484,7 @@ public class TestPoolUtils {
         final float factor = 0.01f; // about ~9 seconds until first discard
         final List<String> expectedMethods = new ArrayList<>();
         try (@SuppressWarnings("unchecked")
-        final KeyedObjectPool<Object, Object> pool = PoolUtils.erodingPool(createProxy(KeyedObjectPool.class, handler), factor)) {
+        final KeyedObjectPool<Object, Object, RuntimeException> pool = PoolUtils.erodingPool(createProxy(KeyedObjectPool.class, handler), factor)) {
 
             assertEquals(expectedMethods, calledMethods);
 
@@ -539,9 +540,9 @@ public class TestPoolUtils {
     @Test
     public void testErodingPoolKeyedObjectPoolDefaultFactor() {
         try (@SuppressWarnings("unchecked")
-        final KeyedObjectPool<Object, Object> internalPool = createProxy(KeyedObjectPool.class,
+        final KeyedObjectPool<Object, Object, RuntimeException> internalPool = createProxy(KeyedObjectPool.class,
                 (arg0, arg1, arg2) -> null);
-                final KeyedObjectPool<Object, Object> pool = PoolUtils.erodingPool(internalPool)) {
+                final KeyedObjectPool<Object, Object, RuntimeException> pool = PoolUtils.erodingPool(internalPool)) {
             final String expectedToString = "ErodingKeyedObjectPool{factor=ErodingFactor{factor=1.0, idleHighWaterMark=1}, keyedPool=" +
                     internalPool + "}";
             // The factor is not exposed, but will be printed in the toString() method
@@ -552,10 +553,10 @@ public class TestPoolUtils {
 
     @Test
     public void testErodingPoolObjectPool() throws Exception {
-        assertThrows(IllegalArgumentException.class, () -> PoolUtils.erodingPool((ObjectPool<Object>) null),
+        assertThrows(IllegalArgumentException.class, () -> PoolUtils.erodingPool((ObjectPool<Object, RuntimeException>) null),
                 "PoolUtils.erodingPool(ObjectPool) must not allow a null pool.");
 
-        assertThrows(IllegalArgumentException.class, () -> PoolUtils.erodingPool((ObjectPool<Object>) null, 1f),
+        assertThrows(IllegalArgumentException.class, () -> PoolUtils.erodingPool((ObjectPool<Object, RuntimeException>) null, 1f),
                 "PoolUtils.erodingPool(ObjectPool, float) must not allow a null pool.");
 
         final List<String> calledMethods = new ArrayList<>();
@@ -578,7 +579,7 @@ public class TestPoolUtils {
         final float factor = 0.01f; // about ~9 seconds until first discard
         final List<String> expectedMethods = new ArrayList<>();
         try (@SuppressWarnings("unchecked")
-        final ObjectPool<Object> pool = PoolUtils.erodingPool(createProxy(ObjectPool.class, handler), factor)) {
+        final ObjectPool<Object, RuntimeException> pool = PoolUtils.erodingPool(createProxy(ObjectPool.class, handler), factor)) {
 
             assertEquals(expectedMethods, calledMethods);
 
@@ -640,14 +641,14 @@ public class TestPoolUtils {
         assertThrows(IllegalArgumentException.class, () -> PoolUtils.prefill(null, new Object(), 1),
                 "PoolUtils.prefill(KeyedObjectPool,Object,int) must not accept null pool.");
 
-        try (final KeyedObjectPool<Object, String> pool = new GenericKeyedObjectPool<>(new TestGenericKeyedObjectPool.SimpleFactory<>())) {
+        try (final KeyedObjectPool<Object, String, Exception> pool = new GenericKeyedObjectPool<>(new TestGenericKeyedObjectPool.SimpleFactory<>())) {
             assertThrows(IllegalArgumentException.class, () -> PoolUtils.prefill(pool, (Object) null, 1),
                     "PoolUtils.prefill(KeyedObjectPool,Object,int) must not accept null key.");
         }
 
         final List<String> calledMethods = new ArrayList<>();
         try (@SuppressWarnings("unchecked")
-        final KeyedObjectPool<Object, Object> pool = createProxy(KeyedObjectPool.class, calledMethods)) {
+        final KeyedObjectPool<Object, Object, RuntimeException> pool = createProxy(KeyedObjectPool.class, calledMethods)) {
 
             PoolUtils.prefill(pool, new Object(), 0);
             final List<String> expectedMethods = new ArrayList<>();
@@ -662,16 +663,16 @@ public class TestPoolUtils {
 
     @SuppressWarnings("deprecation")
     @Test
-    public void testPrefillKeyedObjectPoolCollection() throws Exception {
+    public void testPrefillKeyedObjectPoolCollection() {
         try (@SuppressWarnings("unchecked")
-        final KeyedObjectPool<String, String> pool = createProxy(KeyedObjectPool.class, (List<String>) null)) {
+        final KeyedObjectPool<String, String, RuntimeException> pool = createProxy(KeyedObjectPool.class, (List<String>) null)) {
             assertThrows(IllegalArgumentException.class, () -> PoolUtils.prefill(pool, (Collection<String>) null, 1),
                     "PoolUtils.prefill(KeyedObjectPool,Collection,int) must not accept null keys.");
         }
 
         final List<String> calledMethods = new ArrayList<>();
         try (@SuppressWarnings("unchecked")
-            final KeyedObjectPool<String, Object> pool = createProxy(KeyedObjectPool.class, calledMethods)) {
+            final KeyedObjectPool<String, Object, RuntimeException> pool = createProxy(KeyedObjectPool.class, calledMethods)) {
 
             final Set<String> keys = new HashSet<>();
             PoolUtils.prefill(pool, keys, 0);
@@ -691,12 +692,12 @@ public class TestPoolUtils {
 
     @SuppressWarnings("deprecation")
     @Test
-    public void testPrefillObjectPool() throws Exception {
+    public void testPrefillObjectPool() {
         assertThrows(IllegalArgumentException.class, () -> PoolUtils.prefill(null, 1), "PoolUtils.prefill(ObjectPool,int) must not allow null pool.");
 
         final List<String> calledMethods = new ArrayList<>();
         try (@SuppressWarnings("unchecked")
-        final ObjectPool<Object> pool = createProxy(ObjectPool.class, calledMethods)) {
+        final ObjectPool<Object, RuntimeException> pool = createProxy(ObjectPool.class, calledMethods)) {
 
             PoolUtils.prefill(pool, 0);
             final List<String> expectedMethods = new ArrayList<>();
@@ -710,15 +711,16 @@ public class TestPoolUtils {
     }
 
     @Test
-    public void testSynchronizedPoolableFactoryKeyedPoolableObjectFactory() throws Exception {
-        assertThrows(IllegalArgumentException.class, () -> PoolUtils.synchronizedKeyedPooledFactory((KeyedPooledObjectFactory<Object, Object>) null),
-                "PoolUtils.synchronizedPoolableFactory(KeyedPoolableObjectFactory) must not allow a null factory.");
+    public void testSynchronizedPoolableFactoryKeyedPoolableObjectFactory() {
+        assertThrows(IllegalArgumentException.class,
+            () -> PoolUtils.synchronizedKeyedPooledFactory((KeyedPooledObjectFactory<Object, Object, RuntimeException>) null),
+            "PoolUtils.synchronizedPoolableFactory(KeyedPoolableObjectFactory) must not allow a null factory.");
 
         final List<String> calledMethods = new ArrayList<>();
         @SuppressWarnings("unchecked")
-        final KeyedPooledObjectFactory<Object, Object> kpof = createProxy(KeyedPooledObjectFactory.class, calledMethods);
+        final KeyedPooledObjectFactory<Object, Object, RuntimeException> kpof = createProxy(KeyedPooledObjectFactory.class, calledMethods);
 
-        final KeyedPooledObjectFactory<Object, Object> skpof = PoolUtils.synchronizedKeyedPooledFactory(kpof);
+        final KeyedPooledObjectFactory<Object, Object, RuntimeException> skpof = PoolUtils.synchronizedKeyedPooledFactory(kpof);
         final List<String> expectedMethods = invokeEveryMethod(skpof);
         assertEquals(expectedMethods, calledMethods);
 
@@ -727,14 +729,14 @@ public class TestPoolUtils {
 
     @Test
     public void testSynchronizedPoolableFactoryPoolableObjectFactory() throws Exception {
-        assertThrows(IllegalArgumentException.class, () -> PoolUtils.synchronizedPooledFactory((PooledObjectFactory<Object>) null),
+        assertThrows(IllegalArgumentException.class, () -> PoolUtils.synchronizedPooledFactory((PooledObjectFactory<Object, Exception>) null),
                 "PoolUtils.synchronizedPoolableFactory(PoolableObjectFactory) must not allow a null factory.");
 
         final List<String> calledMethods = new ArrayList<>();
         @SuppressWarnings("unchecked")
-        final PooledObjectFactory<Object> pof = createProxy(PooledObjectFactory.class, calledMethods);
+        final PooledObjectFactory<Object, Exception> pof = createProxy(PooledObjectFactory.class, calledMethods);
 
-        final PooledObjectFactory<Object> spof = PoolUtils.synchronizedPooledFactory(pof);
+        final PooledObjectFactory<Object, Exception> spof = PoolUtils.synchronizedPooledFactory(pof);
         final List<String> expectedMethods = invokeEveryMethod(spof);
         assertEquals(expectedMethods, calledMethods);
 
@@ -742,14 +744,14 @@ public class TestPoolUtils {
     }
 
     @Test
-    public void testSynchronizedPoolKeyedObjectPool() throws Exception {
-        assertThrows(IllegalArgumentException.class, () -> PoolUtils.synchronizedPool((KeyedObjectPool<Object, Object>) null),
+    public void testSynchronizedPoolKeyedObjectPool() {
+        assertThrows(IllegalArgumentException.class, () -> PoolUtils.synchronizedPool((KeyedObjectPool<Object, Object, RuntimeException>) null),
                 "PoolUtils.synchronizedPool(KeyedObjectPool) must not allow a null pool.");
 
         final List<String> calledMethods = new ArrayList<>();
         try (@SuppressWarnings("unchecked")
-        final KeyedObjectPool<Object, Object> kop = createProxy(KeyedObjectPool.class, calledMethods);
-                final KeyedObjectPool<Object, Object> skop = PoolUtils.synchronizedPool(kop)) {
+            final KeyedObjectPool<Object, Object, RuntimeException> kop = createProxy(KeyedObjectPool.class, calledMethods);
+            final KeyedObjectPool<Object, Object, RuntimeException> skop = PoolUtils.synchronizedPool(kop)) {
             final List<String> expectedMethods = invokeEveryMethod(skop);
             assertEquals(expectedMethods, calledMethods);
         }
@@ -758,13 +760,13 @@ public class TestPoolUtils {
     }
 
     @Test
-    public void testSynchronizedPoolObjectPool() throws Exception {
-        assertThrows(IllegalArgumentException.class, () -> PoolUtils.synchronizedPool((ObjectPool<Object>) null),
+    public void testSynchronizedPoolObjectPool() {
+        assertThrows(IllegalArgumentException.class, () -> PoolUtils.synchronizedPool((ObjectPool<Object, RuntimeException>) null),
                 "PoolUtils.synchronizedPool(ObjectPool) must not allow a null pool.");
 
         final List<String> calledMethods = new ArrayList<>();
         try (@SuppressWarnings("unchecked")
-        final ObjectPool<Object> op = createProxy(ObjectPool.class, calledMethods); final ObjectPool<Object> sop = PoolUtils.synchronizedPool(op)) {
+        final ObjectPool<Object, RuntimeException> op = createProxy(ObjectPool.class, calledMethods); final ObjectPool<Object, RuntimeException> sop = PoolUtils.synchronizedPool(op)) {
             final List<String> expectedMethods = invokeEveryMethod(sop);
             assertEquals(expectedMethods, calledMethods);
 
diff --git a/src/test/java/org/apache/commons/pool2/VisitTracker.java b/src/test/java/org/apache/commons/pool2/VisitTracker.java
index 2fbed31..2dbd4b7 100644
--- a/src/test/java/org/apache/commons/pool2/VisitTracker.java
+++ b/src/test/java/org/apache/commons/pool2/VisitTracker.java
@@ -17,11 +17,12 @@
 package org.apache.commons.pool2;
 
 /**
- * Test pooled object class.  Keeps track of how many times it has been
- * validated, activated, passivated.
+ * Test pooled object class. Keeps track of how many times it has been validated, activated, passivated.
  *
+ * @param <K> The key type.
  */
 public class VisitTracker<K> {
+
     private int validateCount;
     private int activateCount;
     private int passivateCount;
@@ -50,42 +51,53 @@ public class VisitTracker<K> {
         }
         activateCount++;
     }
+
     public void destroy() {
         destroyed = true;
     }
+
     private void fail(final String message) {
         throw new IllegalStateException(message);
     }
+
     public int getActivateCount() {
         return activateCount;
     }
+
     public int getId() {
         return id;
     }
+
     public K getKey() {
         return key;
     }
+
     public int getPassivateCount() {
         return passivateCount;
     }
+
     public int getValidateCount() {
         return validateCount;
     }
+
     public boolean isDestroyed() {
         return destroyed;
     }
+
     public void passivate() {
         if (destroyed) {
             fail("attempted to passivate a destroyed object");
         }
         passivateCount++;
     }
+
     public void reset() {
         validateCount = 0;
         activateCount = 0;
         passivateCount = 0;
         destroyed = false;
     }
+
     @Override
     public String toString() {
         return "Key: " + key + " id: " + id;
diff --git a/src/test/java/org/apache/commons/pool2/VisitTrackerFactory.java b/src/test/java/org/apache/commons/pool2/VisitTrackerFactory.java
index fd2844b..78121ef 100644
--- a/src/test/java/org/apache/commons/pool2/VisitTrackerFactory.java
+++ b/src/test/java/org/apache/commons/pool2/VisitTrackerFactory.java
@@ -22,21 +22,20 @@ import org.apache.commons.pool2.impl.DefaultPooledObject;
 /**
  * Factory that creates VisitTracker instances. Used to test Evictor runs.
  *
+ * @param <K> The VisitTracker key type.
  */
 public class VisitTrackerFactory<K>
-        implements PooledObjectFactory<VisitTracker<K>>, KeyedPooledObjectFactory<K, VisitTracker<K>> {
-    private int nextId;
+        implements PooledObjectFactory<VisitTracker<K>, RuntimeException>, KeyedPooledObjectFactory<K, VisitTracker<K>, RuntimeException> {
 
-    public VisitTrackerFactory() {
-    }
+    private int nextId;
 
     @Override
-    public void activateObject(final K key, final PooledObject<VisitTracker<K>> ref) throws Exception {
+    public void activateObject(final K key, final PooledObject<VisitTracker<K>> ref) {
         ref.getObject().activate();
     }
 
     @Override
-    public void activateObject(final PooledObject<VisitTracker<K>> ref) throws Exception {
+    public void activateObject(final PooledObject<VisitTracker<K>> ref) {
         ref.getObject().activate();
     }
 
@@ -61,12 +60,12 @@ public class VisitTrackerFactory<K>
     }
 
     @Override
-    public void passivateObject(final K key, final PooledObject<VisitTracker<K>> ref) throws Exception {
+    public void passivateObject(final K key, final PooledObject<VisitTracker<K>> ref) {
         ref.getObject().passivate();
     }
 
     @Override
-    public void passivateObject(final PooledObject<VisitTracker<K>> ref) throws Exception {
+    public void passivateObject(final PooledObject<VisitTracker<K>> ref) {
         ref.getObject().passivate();
     }
 
diff --git a/src/test/java/org/apache/commons/pool2/Waiter.java b/src/test/java/org/apache/commons/pool2/Waiter.java
index 78924ae..0a3166a 100644
--- a/src/test/java/org/apache/commons/pool2/Waiter.java
+++ b/src/test/java/org/apache/commons/pool2/Waiter.java
@@ -28,6 +28,14 @@ import java.util.concurrent.atomic.AtomicInteger;
  */
 public class Waiter {
     private static final AtomicInteger instanceCount = new AtomicInteger();
+    /** TODO Reuse Apache Commons Lang ThreadUtils */
+    public static void sleepQuietly(final long millis) {
+        try {
+            Thread.sleep(millis);
+        } catch (final InterruptedException e) {
+            // be quiet
+        }
+    }
     private boolean active;
     private boolean valid;
     private long latency;
@@ -35,6 +43,7 @@ public class Waiter {
     private long lastIdleTimeMillis;
     private long passivationCount;
     private long validationCount;
+
     private final int id = instanceCount.getAndIncrement();
 
     public Waiter(final boolean active, final boolean valid, final long latency) {
@@ -171,13 +180,4 @@ public class Waiter {
         buff.append("latency = " + latency + '\n');
         return buff.toString();
     }
-
-    /** TODO Reuse Apache Commons Lang ThreadUtils */
-    public static void sleepQuietly(final long millis) {
-        try {
-            Thread.sleep(millis);
-        } catch (final InterruptedException e) {
-            // be quiet
-        }
-    }
 }
diff --git a/src/test/java/org/apache/commons/pool2/WaiterFactory.java b/src/test/java/org/apache/commons/pool2/WaiterFactory.java
index 05d7661..3a19039 100644
--- a/src/test/java/org/apache/commons/pool2/WaiterFactory.java
+++ b/src/test/java/org/apache/commons/pool2/WaiterFactory.java
@@ -29,10 +29,10 @@ import org.apache.commons.pool2.impl.DefaultPooledObject;
  * If the factory's maxActive / maxActivePerKey are set to match those of the
  * pool, makeObject will throw IllegalStateException if the number of makes - destroys
  * (per key) exceeds the configured max.
- *
+ * 
+ * @param <K> The type of keys managed by this factory.
  */
-public class WaiterFactory<K> implements PooledObjectFactory<Waiter>,
-KeyedPooledObjectFactory<K,Waiter> {
+public class WaiterFactory<K> implements PooledObjectFactory<Waiter, IllegalStateException>, KeyedPooledObjectFactory<K, Waiter, RuntimeException> {
 
     /** Latency of activateObject */
     private final long activateLatency;
@@ -97,18 +97,18 @@ KeyedPooledObjectFactory<K,Waiter> {
     }
 
     @Override
-    public void activateObject(final K key, final PooledObject<Waiter> obj) throws Exception {
+    public void activateObject(final K key, final PooledObject<Waiter> obj) {
         activateObject(obj);
     }
 
     @Override
-    public void activateObject(final PooledObject<Waiter> obj) throws Exception {
+    public void activateObject(final PooledObject<Waiter> obj) {
         doWait(activateLatency);
         obj.getObject().setActive(true);
     }
 
     @Override
-    public void destroyObject(final K key,final PooledObject<Waiter> obj) throws Exception {
+    public void destroyObject(final K key,final PooledObject<Waiter> obj) {
         destroyObject(obj);
         synchronized (this) {
             final Integer count = activeCounts.get(key);
@@ -117,7 +117,7 @@ KeyedPooledObjectFactory<K,Waiter> {
     }
 
     @Override
-    public void destroyObject(final PooledObject<Waiter> obj) throws Exception {
+    public void destroyObject(final PooledObject<Waiter> obj) {
         doWait(destroyLatency);
         obj.getObject().setValid(false);
         obj.getObject().setActive(false);
@@ -142,7 +142,7 @@ KeyedPooledObjectFactory<K,Waiter> {
     }
 
     @Override
-    public PooledObject<Waiter> makeObject() throws Exception {
+    public PooledObject<Waiter> makeObject() {
         // Increment and test *before* make
         synchronized (this) {
             if (activeCount >= maxActive) {
@@ -156,7 +156,7 @@ KeyedPooledObjectFactory<K,Waiter> {
     }
 
     @Override
-    public PooledObject<Waiter> makeObject(final K key) throws Exception {
+    public PooledObject<Waiter> makeObject(final K key) {
         synchronized (this) {
             Integer count = activeCounts.get(key);
             if (count == null) {
@@ -178,12 +178,12 @@ KeyedPooledObjectFactory<K,Waiter> {
     // KeyedPoolableObjectFactory methods
 
     @Override
-    public void passivateObject(final K key, final PooledObject<Waiter> obj) throws Exception {
+    public void passivateObject(final K key, final PooledObject<Waiter> obj) {
         passivateObject(obj);
     }
 
     @Override
-    public void passivateObject(final PooledObject<Waiter> obj) throws Exception {
+    public void passivateObject(final PooledObject<Waiter> obj) {
         obj.getObject().setActive(false);
         doWait(passivateLatency);
         if (Math.random() < passivateInvalidationProbability) {
diff --git a/src/test/java/org/apache/commons/pool2/impl/AtomicIntegerFactory.java b/src/test/java/org/apache/commons/pool2/impl/AtomicIntegerFactory.java
index 2f66560..e970765 100644
--- a/src/test/java/org/apache/commons/pool2/impl/AtomicIntegerFactory.java
+++ b/src/test/java/org/apache/commons/pool2/impl/AtomicIntegerFactory.java
@@ -30,7 +30,7 @@ import org.apache.commons.pool2.Waiter;
  *
  */
 public class AtomicIntegerFactory
-    extends BasePooledObjectFactory<AtomicInteger> {
+    extends BasePooledObjectFactory<AtomicInteger, RuntimeException> {
 
     private long activateLatency;
     private long passivateLatency;
diff --git a/src/test/java/org/apache/commons/pool2/impl/TestAbandonedKeyedObjectPool.java b/src/test/java/org/apache/commons/pool2/impl/TestAbandonedKeyedObjectPool.java
index 7e93c5b..3c2f23d 100644
--- a/src/test/java/org/apache/commons/pool2/impl/TestAbandonedKeyedObjectPool.java
+++ b/src/test/java/org/apache/commons/pool2/impl/TestAbandonedKeyedObjectPool.java
@@ -78,7 +78,7 @@ public class TestAbandonedKeyedObjectPool {
         }
     }
 
-    private static class SimpleFactory implements KeyedPooledObjectFactory<Integer,PooledTestObject> {
+    private static class SimpleFactory implements KeyedPooledObjectFactory<Integer, PooledTestObject, InterruptedException> {
 
         private final long destroyLatency;
         private final long validateLatency;
@@ -99,12 +99,12 @@ public class TestAbandonedKeyedObjectPool {
         }
 
         @Override
-        public void destroyObject(final Integer key, final PooledObject<PooledTestObject> obj) throws Exception {
+        public void destroyObject(final Integer key, final PooledObject<PooledTestObject> obj) throws InterruptedException {
             destroyObject(key, obj, DestroyMode.NORMAL);
         }
 
         @Override
-        public void destroyObject(final Integer key, final PooledObject<PooledTestObject> obj, final DestroyMode destroyMode) throws Exception {
+        public void destroyObject(final Integer key, final PooledObject<PooledTestObject> obj, final DestroyMode destroyMode) throws InterruptedException {
             obj.getObject().setActive(false);
             // while destroying instances, yield control to other threads
             // helps simulate threading errors
@@ -132,7 +132,7 @@ public class TestAbandonedKeyedObjectPool {
         }
     }
 
-    private GenericKeyedObjectPool<Integer,PooledTestObject> pool;
+    private GenericKeyedObjectPool<Integer, PooledTestObject, InterruptedException> pool;
 
     private AbandonedConfig abandonedConfig;
 
@@ -211,6 +211,9 @@ public class TestAbandonedKeyedObjectPool {
             obj = pool.borrowObject(0);
         }
         Thread.sleep(1000);          // abandon checked out instances and let evictor start
+        if (!pool.getKeys().contains(0)) {
+            Thread.sleep(1000); // Wait a little more.
+        }
         pool.invalidateObject(0, obj);  // Should not trigger another destroy / decrement
         Thread.sleep(2000);          // give evictor time to finish destroys
         assertEquals(0, pool.getNumActive());
diff --git a/src/test/java/org/apache/commons/pool2/impl/TestAbandonedObjectPool.java b/src/test/java/org/apache/commons/pool2/impl/TestAbandonedObjectPool.java
index dd5e164..b403f90 100644
--- a/src/test/java/org/apache/commons/pool2/impl/TestAbandonedObjectPool.java
+++ b/src/test/java/org/apache/commons/pool2/impl/TestAbandonedObjectPool.java
@@ -158,7 +158,7 @@ public class TestAbandonedObjectPool {
         }
     }
 
-    private static class SimpleFactory implements PooledObjectFactory<PooledTestObject> {
+    private static class SimpleFactory implements PooledObjectFactory<PooledTestObject, InterruptedException> {
 
         private final long destroyLatency;
         private final long validateLatency;
@@ -179,12 +179,12 @@ public class TestAbandonedObjectPool {
         }
 
         @Override
-        public void destroyObject(final PooledObject<PooledTestObject> obj) throws Exception {
+        public void destroyObject(final PooledObject<PooledTestObject> obj) throws InterruptedException {
             destroyObject(obj, DestroyMode.NORMAL);
         }
 
         @Override
-        public void destroyObject(final PooledObject<PooledTestObject> obj, final DestroyMode destroyMode) throws Exception {
+        public void destroyObject(final PooledObject<PooledTestObject> obj, final DestroyMode destroyMode) throws InterruptedException {
             obj.getObject().setActive(false);
             // while destroying instances, yield control to other threads
             // helps simulate threading errors
@@ -212,13 +212,13 @@ public class TestAbandonedObjectPool {
         }
     }
 
-    private GenericObjectPool<PooledTestObject> pool;
+    private GenericObjectPool<PooledTestObject, InterruptedException> pool;
 
     private AbandonedConfig abandonedConfig;
 
     @SuppressWarnings("deprecation")
     @BeforeEach
-    public void setUp() throws Exception {
+    public void setUp() {
         abandonedConfig = new AbandonedConfig();
 
         // Uncomment the following line to enable logging:
diff --git a/src/test/java/org/apache/commons/pool2/impl/TestBaseGenericObjectPool.java b/src/test/java/org/apache/commons/pool2/impl/TestBaseGenericObjectPool.java
index d36fd17..8bd820a 100644
--- a/src/test/java/org/apache/commons/pool2/impl/TestBaseGenericObjectPool.java
+++ b/src/test/java/org/apache/commons/pool2/impl/TestBaseGenericObjectPool.java
@@ -31,7 +31,7 @@ import org.junit.jupiter.api.Test;
  */
 public class TestBaseGenericObjectPool {
 
-    BaseGenericObjectPool<String> pool;
+    BaseGenericObjectPool<String, Exception> pool;
     SimpleFactory factory;
 
     @BeforeEach
@@ -83,7 +83,7 @@ public class TestBaseGenericObjectPool {
     public void testEvictionTimerMultiplePools() throws InterruptedException {
         final AtomicIntegerFactory factory = new AtomicIntegerFactory();
         factory.setValidateLatency(50);
-        try (final GenericObjectPool<AtomicInteger> evictingPool = new GenericObjectPool<>(factory)) {
+        try (final GenericObjectPool<AtomicInteger, RuntimeException> evictingPool = new GenericObjectPool<>(factory)) {
             evictingPool.setTimeBetweenEvictionRuns(Duration.ofMillis(100));
             evictingPool.setNumTestsPerEvictionRun(5);
             evictingPool.setTestWhileIdle(true);
@@ -97,7 +97,7 @@ public class TestBaseGenericObjectPool {
             }
 
             for (int i = 0; i < 1000; i++) {
-                try (final GenericObjectPool<AtomicInteger> nonEvictingPool = new GenericObjectPool<>(factory)) {
+                try (final GenericObjectPool<AtomicInteger, RuntimeException> nonEvictingPool = new GenericObjectPool<>(factory)) {
                     // empty
                 }
             }
diff --git a/src/test/java/org/apache/commons/pool2/impl/TestDefaultPooledObject.java b/src/test/java/org/apache/commons/pool2/impl/TestDefaultPooledObject.java
index 4bf779c..1057516 100644
--- a/src/test/java/org/apache/commons/pool2/impl/TestDefaultPooledObject.java
+++ b/src/test/java/org/apache/commons/pool2/impl/TestDefaultPooledObject.java
@@ -17,9 +17,11 @@
 package org.apache.commons.pool2.impl;
 
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.lessThan;
 import static org.hamcrest.Matchers.lessThanOrEqualTo;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
 
 import java.time.Duration;
 import java.util.ArrayList;
@@ -37,67 +39,6 @@ import org.junit.jupiter.api.Test;
  */
 public class TestDefaultPooledObject {
 
-    @Test
-    public void testInitialStateActiveDuration() throws InterruptedException {
-        final PooledObject<Object> dpo = new DefaultPooledObject<>(new Object());
-        // Sleep MUST be "long enough" to test that we are not returning a negative time.
-        // Need an API in Java 8 to get the clock granularity.
-        Thread.sleep(200);
-        // In the initial state, all instants are the creation instant: last borrow, last use, last return.
-        // In the initial state, the active duration is the time between "now" and the creation time.
-        // In the initial state, the idle duration is the time between "now" and the last return, which is the creation time.
-        assertFalse(dpo.getActiveDuration().isNegative());
-        assertFalse(dpo.getActiveDuration().isZero());
-        // We use greaterThanOrEqualTo instead of equal because "now" many be different when each argument is evaluated.
-        assertThat(1L, lessThanOrEqualTo(2L)); // sanity check
-        assertThat(Duration.ZERO, lessThanOrEqualTo(Duration.ZERO.plusNanos(1))); // sanity check
-        assertThat(dpo.getActiveDuration(), lessThanOrEqualTo(dpo.getIdleDuration()));
-        // Deprecated
-        assertThat(dpo.getActiveDuration().toMillis(), lessThanOrEqualTo(dpo.getActiveTimeMillis()));
-        assertThat(dpo.getActiveDuration(), lessThanOrEqualTo(dpo.getActiveTime()));
-        assertThat(dpo.getActiveDuration(), lessThanOrEqualTo(dpo.getIdleTime()));
-        assertThat(dpo.getActiveDuration().toMillis(), lessThanOrEqualTo(dpo.getIdleTimeMillis()));
-    }
-
-    @Test
-    public void testInitialStateIdleDuration() throws InterruptedException {
-        final PooledObject<Object> dpo = new DefaultPooledObject<>(new Object());
-        // Sleep MUST be "long enough" to test that we are not returning a negative time.
-        Thread.sleep(200);
-        // In the initial state, all instants are the creation instant: last borrow, last use, last return.
-        // In the initial state, the active duration is the time between "now" and the creation time.
-        // In the initial state, the idle duration is the time between "now" and the last return, which is the creation time.
-        assertFalse(dpo.getIdleDuration().isNegative());
-        assertFalse(dpo.getIdleDuration().isZero());
-        // We use greaterThanOrEqualTo instead of equal because "now" many be different when each argument is evaluated.
-        assertThat(dpo.getIdleDuration(), lessThanOrEqualTo(dpo.getActiveDuration()));
-        // Deprecated
-        // assertThat(dpo.getIdleDuration().toMillis(), lessThanOrEqualTo(dpo.getIdleTimeMillis()));
-        // assertThat(dpo.getIdleDuration(), lessThanOrEqualTo(dpo.getIdleTime()));
-        assertThat(dpo.getIdleDuration(), lessThanOrEqualTo(dpo.getActiveTime()));
-        assertThat(dpo.getIdleDuration().toMillis(), lessThanOrEqualTo(dpo.getActiveTimeMillis()));
-    }
-
-    @Test
-    public void testInitialStateCreateInstant() throws InterruptedException {
-        final PooledObject<Object> dpo = new DefaultPooledObject<>(new Object());
-
-        // In the initial state, all instants are the creation instant: last borrow, last use, last return.
-
-        // Instant vs. Instant
-        assertEquals(dpo.getCreateInstant(), dpo.getLastBorrowInstant());
-        assertEquals(dpo.getCreateInstant(), dpo.getLastReturnInstant());
-        assertEquals(dpo.getCreateInstant(), dpo.getLastUsedInstant());
-
-        // Instant vs. long (deprecated)
-        assertEquals(dpo.getCreateInstant().toEpochMilli(), dpo.getCreateTime());
-
-        // long vs. long (deprecated)
-        assertEquals(dpo.getCreateTime(), dpo.getLastBorrowTime());
-        assertEquals(dpo.getCreateTime(), dpo.getLastReturnTime());
-        assertEquals(dpo.getCreateTime(), dpo.getLastUsedTime());
-    }
-
     /**
      * JIRA: POOL-279
      *
@@ -147,4 +88,78 @@ public class TestDefaultPooledObject {
         }
         assertFalse(negativeIdleTimeReturned.get(), "DefaultPooledObject.getIdleTimeMillis() returned a negative value");
     }
+
+    @Test
+    public void testInitialStateActiveDuration() throws InterruptedException {
+        final PooledObject<Object> dpo = new DefaultPooledObject<>(new Object());
+        // Sleep MUST be "long enough" to test that we are not returning a negative time.
+        // Need an API in Java 8 to get the clock granularity.
+        Thread.sleep(200);
+        // In the initial state, all instants are the creation instant: last borrow, last use, last return.
+        // In the initial state, the active duration is the time between "now" and the creation time.
+        // In the initial state, the idle duration is the time between "now" and the last return, which is the creation time.
+        assertFalse(dpo.getActiveDuration().isNegative());
+        assertFalse(dpo.getActiveDuration().isZero());
+        // We use greaterThanOrEqualTo instead of equal because "now" many be different when each argument is evaluated.
+        assertThat(1L, lessThanOrEqualTo(2L)); // sanity check
+        assertThat(Duration.ZERO, lessThanOrEqualTo(Duration.ZERO.plusNanos(1))); // sanity check
+        assertThat(dpo.getActiveDuration(), lessThanOrEqualTo(dpo.getIdleDuration()));
+        // Deprecated
+        assertThat(dpo.getActiveDuration().toMillis(), lessThanOrEqualTo(dpo.getActiveTimeMillis()));
+        assertThat(dpo.getActiveDuration(), lessThanOrEqualTo(dpo.getActiveTime()));
+        assertThat(dpo.getActiveDuration(), lessThanOrEqualTo(dpo.getIdleTime()));
+        assertThat(dpo.getActiveDuration().toMillis(), lessThanOrEqualTo(dpo.getIdleTimeMillis()));
+    }
+
+    @Test
+    public void testInitialStateCreateInstant() {
+        final PooledObject<Object> dpo = new DefaultPooledObject<>(new Object());
+
+        // In the initial state, all instants are the creation instant: last borrow, last use, last return.
+
+        // Instant vs. Instant
+        assertEquals(dpo.getCreateInstant(), dpo.getLastBorrowInstant());
+        assertEquals(dpo.getCreateInstant(), dpo.getLastReturnInstant());
+        assertEquals(dpo.getCreateInstant(), dpo.getLastUsedInstant());
+
+        // Instant vs. long (deprecated)
+        assertEquals(dpo.getCreateInstant().toEpochMilli(), dpo.getCreateTime());
+
+        // long vs. long (deprecated)
+        assertEquals(dpo.getCreateTime(), dpo.getLastBorrowTime());
+        assertEquals(dpo.getCreateTime(), dpo.getLastReturnTime());
+        assertEquals(dpo.getCreateTime(), dpo.getLastUsedTime());
+    }
+
+    @Test
+    public void testInitialStateDuration() throws InterruptedException {
+        final PooledObject<Object> dpo = new DefaultPooledObject<>(new Object());
+        final Duration duration1 = dpo.getFullDuration();
+        assertNotNull(duration1);
+        assertFalse(duration1.isNegative());
+        Thread.sleep(100);
+        final Duration duration2 = dpo.getFullDuration();
+        assertNotNull(duration2);
+        assertFalse(duration2.isNegative());
+        assertThat(duration1, lessThan(duration2));
+    }
+
+    @Test
+    public void testInitialStateIdleDuration() throws InterruptedException {
+        final PooledObject<Object> dpo = new DefaultPooledObject<>(new Object());
+        // Sleep MUST be "long enough" to test that we are not returning a negative time.
+        Thread.sleep(200);
+        // In the initial state, all instants are the creation instant: last borrow, last use, last return.
+        // In the initial state, the active duration is the time between "now" and the creation time.
+        // In the initial state, the idle duration is the time between "now" and the last return, which is the creation time.
+        assertFalse(dpo.getIdleDuration().isNegative());
+        assertFalse(dpo.getIdleDuration().isZero());
+        // We use greaterThanOrEqualTo instead of equal because "now" many be different when each argument is evaluated.
+        assertThat(dpo.getIdleDuration(), lessThanOrEqualTo(dpo.getActiveDuration()));
+        // Deprecated
+        // assertThat(dpo.getIdleDuration().toMillis(), lessThanOrEqualTo(dpo.getIdleTimeMillis()));
+        // assertThat(dpo.getIdleDuration(), lessThanOrEqualTo(dpo.getIdleTime()));
+        assertThat(dpo.getIdleDuration(), lessThanOrEqualTo(dpo.getActiveTime()));
+        assertThat(dpo.getIdleDuration().toMillis(), lessThanOrEqualTo(dpo.getActiveTimeMillis()));
+    }
 }
\ No newline at end of file
diff --git a/src/test/java/org/apache/commons/pool2/impl/TestDefaultPooledObjectInfo.java b/src/test/java/org/apache/commons/pool2/impl/TestDefaultPooledObjectInfo.java
index fd05ad4..897765f 100644
--- a/src/test/java/org/apache/commons/pool2/impl/TestDefaultPooledObjectInfo.java
+++ b/src/test/java/org/apache/commons/pool2/impl/TestDefaultPooledObjectInfo.java
@@ -34,7 +34,7 @@ public class TestDefaultPooledObjectInfo {
         abandonedConfig.setRemoveAbandonedOnBorrow(true);
         abandonedConfig.setRemoveAbandonedTimeout(TestConstants.ONE_SECOND_DURATION);
         abandonedConfig.setLogAbandoned(true);
-        try (final GenericObjectPool<String> pool = new GenericObjectPool<>(new SimpleFactory(),
+        try (final GenericObjectPool<String, Exception> pool = new GenericObjectPool<>(new SimpleFactory(),
                 new GenericObjectPoolConfig<>(), abandonedConfig)) {
 
             pool.borrowObject();
@@ -50,7 +50,7 @@ public class TestDefaultPooledObjectInfo {
 
     @Test
     public void testGetPooledObjectToString() throws Exception {
-        try (final GenericObjectPool<String> pool = new GenericObjectPool<>(new SimpleFactory())) {
+        try (final GenericObjectPool<String, Exception> pool = new GenericObjectPool<>(new SimpleFactory())) {
 
             final String s1 = pool.borrowObject();
 
@@ -66,7 +66,7 @@ public class TestDefaultPooledObjectInfo {
 
     @Test
     public void testGetPooledObjectType() throws Exception {
-        try (final GenericObjectPool<String> pool = new GenericObjectPool<>(new SimpleFactory())) {
+        try (final GenericObjectPool<String, Exception> pool = new GenericObjectPool<>(new SimpleFactory())) {
 
             pool.borrowObject();
 
@@ -82,7 +82,7 @@ public class TestDefaultPooledObjectInfo {
 
     @Test
     public void testTiming() throws Exception {
-        try (final GenericObjectPool<String> pool = new GenericObjectPool<>(new SimpleFactory())) {
+        try (final GenericObjectPool<String, Exception> pool = new GenericObjectPool<>(new SimpleFactory())) {
 
             final long t1Millis = System.currentTimeMillis();
 
diff --git a/src/test/java/org/apache/commons/pool2/impl/TestEvictionTimer.java b/src/test/java/org/apache/commons/pool2/impl/TestEvictionTimer.java
index a45e189..f904ff5 100644
--- a/src/test/java/org/apache/commons/pool2/impl/TestEvictionTimer.java
+++ b/src/test/java/org/apache/commons/pool2/impl/TestEvictionTimer.java
@@ -38,10 +38,10 @@ public class TestEvictionTimer {
     @Test
     public void testStartStopEvictionTimer() throws Exception {
 
-        try (final GenericObjectPool<String> pool = new GenericObjectPool<>(new BasePooledObjectFactory<String>() {
+        try (final GenericObjectPool<String, RuntimeException> pool = new GenericObjectPool<>(new BasePooledObjectFactory<String, RuntimeException>() {
 
             @Override
-            public String create() throws Exception {
+            public String create() {
                 return null;
             }
 
@@ -52,7 +52,7 @@ public class TestEvictionTimer {
         })) {
 
             // Start evictor #1
-            final BaseGenericObjectPool<String>.Evictor evictor1 = pool.new Evictor();
+            final BaseGenericObjectPool<String, RuntimeException>.Evictor evictor1 = pool.new Evictor();
             EvictionTimer.schedule(evictor1, TestConstants.ONE_MINUTE_DURATION, TestConstants.ONE_MINUTE_DURATION);
 
             // Assert that eviction objects are correctly allocated
@@ -70,7 +70,7 @@ public class TestEvictionTimer {
             assertEquals(1, EvictionTimer.getNumTasks());
 
             // Start evictor #2
-            final BaseGenericObjectPool<String>.Evictor evictor2 = pool.new Evictor();
+            final BaseGenericObjectPool<String, RuntimeException>.Evictor evictor2 = pool.new Evictor();
             EvictionTimer.schedule(evictor2, TestConstants.ONE_MINUTE_DURATION, TestConstants.ONE_MINUTE_DURATION);
 
             // Assert that eviction objects are correctly allocated
diff --git a/src/test/java/org/apache/commons/pool2/impl/TestGenericKeyedObjectPool.java b/src/test/java/org/apache/commons/pool2/impl/TestGenericKeyedObjectPool.java
index 13626dc..156874a 100644
--- a/src/test/java/org/apache/commons/pool2/impl/TestGenericKeyedObjectPool.java
+++ b/src/test/java/org/apache/commons/pool2/impl/TestGenericKeyedObjectPool.java
@@ -53,6 +53,7 @@ import javax.management.MBeanServer;
 import javax.management.ObjectName;
 
 import org.apache.commons.pool2.BaseKeyedPooledObjectFactory;
+import org.apache.commons.pool2.DestroyMode;
 import org.apache.commons.pool2.KeyedObjectPool;
 import org.apache.commons.pool2.KeyedPooledObjectFactory;
 import org.apache.commons.pool2.PooledObject;
@@ -65,6 +66,8 @@ import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Timeout;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
 
 /**
  */
@@ -79,10 +82,9 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
         }
     }
 
-    private static class DummyFactory
-            extends BaseKeyedPooledObjectFactory<Object,Object> {
+    private static class DummyFactory extends BaseKeyedPooledObjectFactory<Object, Object, RuntimeException> {
         @Override
-        public Object create(final Object key) throws Exception {
+        public Object create(final Object key) {
             return null;
         }
         @Override
@@ -97,9 +99,9 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
      *  1) Instances are mutable and mutation can cause change in identity / hashcode.
      */
     private static final class HashSetFactory
-            extends BaseKeyedPooledObjectFactory<String, HashSet<String>> {
+            extends BaseKeyedPooledObjectFactory<String, HashSet<String>, RuntimeException> {
         @Override
-        public HashSet<String> create(final String key) throws Exception {
+        public HashSet<String> create(final String key) {
             return new HashSet<>();
         }
         @Override
@@ -112,11 +114,13 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
      * Attempts to invalidate an object, swallowing IllegalStateException.
      */
     static class InvalidateThread implements Runnable {
+
         private final String obj;
-        private final KeyedObjectPool<String, String> pool;
+        private final KeyedObjectPool<String, String, Exception> pool;
         private final String key;
         private boolean done;
-        public InvalidateThread(final KeyedObjectPool<String, String> pool, final String key, final String obj) {
+
+        public InvalidateThread(final KeyedObjectPool<String, String, Exception> pool, final String key, final String obj) {
             this.obj = obj;
             this.pool = pool;
             this.key = key;
@@ -139,11 +143,10 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
     }
 
     private static class ObjectFactory
-        extends BaseKeyedPooledObjectFactory<Integer, Object> {
+        extends BaseKeyedPooledObjectFactory<Integer, Object, RuntimeException> {
 
         @Override
-        public Object create(final Integer key)
-            throws Exception {
+        public Object create(final Integer key) {
             return new Object();
         }
 
@@ -152,7 +155,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
             return new DefaultPooledObject<>(value);
         }
     }
-    public static class SimpleFactory<K> implements KeyedPooledObjectFactory<K,String> {
+    public static class SimpleFactory<K> implements KeyedPooledObjectFactory<K, String, Exception> {
         volatile int counter;
         final boolean valid;
         int activeCount;
@@ -182,10 +185,11 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
 
         @Override
         public void activateObject(final K key, final PooledObject<String> obj) throws Exception {
-            if (exceptionOnActivate && !(validateCounter++%2 == 0 ? evenValid : oddValid)) {
+            if (exceptionOnActivate && !(validateCounter++ % 2 == 0 ? evenValid : oddValid)) {
                 throw new Exception();
             }
         }
+
         @Override
         public void destroyObject(final K key, final PooledObject<String> obj) throws Exception {
             doWait(destroyLatency);
@@ -266,17 +270,16 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
                 throw new RuntimeException("validation failed");
             }
             if (enableValidation) {
-                return validateCounter++%2 == 0 ? evenValid : oddValid;
+                return validateCounter++ % 2 == 0 ? evenValid : oddValid;
             }
             return valid;
         }
     }
-    private static class SimplePerKeyFactory
-            extends BaseKeyedPooledObjectFactory<Object,Object> {
-        final ConcurrentHashMap<Object,AtomicInteger> map =
-                new ConcurrentHashMap<>();
+    private static class SimplePerKeyFactory extends BaseKeyedPooledObjectFactory<Object, Object, RuntimeException> {
+        final ConcurrentHashMap<Object, AtomicInteger> map = new ConcurrentHashMap<>();
+
         @Override
-        public Object create(final Object key) throws Exception {
+        public Object create(final Object key) {
             int counter = 0;
             final AtomicInteger Counter = map.get(key);
             if(null != Counter) {
@@ -287,6 +290,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
             }
             return String.valueOf(key) + String.valueOf(counter);
         }
+
         @Override
         public PooledObject<Object> wrap(final Object value) {
             return new DefaultPooledObject<>(value);
@@ -296,11 +300,11 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
      * Very simple test thread that just tries to borrow an object from
      * the provided pool with the specified key and returns it
      */
-    static class SimpleTestThread<T> implements Runnable {
-        private final KeyedObjectPool<String,T> pool;
+    static class SimpleTestThread<T, E extends Exception> implements Runnable {
+        private final KeyedObjectPool<String, T, E> pool;
         private final String key;
 
-        public SimpleTestThread(final KeyedObjectPool<String,T> pool, final String key) {
+        public SimpleTestThread(final KeyedObjectPool<String, T, E> pool, final String key) {
             this.pool = pool;
             this.key = key;
         }
@@ -337,11 +341,11 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
         }
     }
 
-    static class TestThread<T> implements Runnable {
+    static class TestThread<T, E extends Exception> implements Runnable {
         private final java.util.Random random = new java.util.Random();
 
         /** GKOP to hit */
-        private final KeyedObjectPool<String,T> pool;
+        private final KeyedObjectPool<String, T, E> pool;
         /** number of borrow/return iterations */
         private final int iter;
         /** delay before borrow */
@@ -359,19 +363,19 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
         private volatile boolean failed;
         private volatile Exception exception;
 
-        public TestThread(final KeyedObjectPool<String,T> pool) {
+        public TestThread(final KeyedObjectPool<String, T, E> pool) {
             this(pool, 100, 50, 50, true, null, null);
         }
 
-        public TestThread(final KeyedObjectPool<String,T> pool, final int iter) {
+        public TestThread(final KeyedObjectPool<String, T, E> pool, final int iter) {
             this(pool, iter, 50, 50, true, null, null);
         }
 
-        public TestThread(final KeyedObjectPool<String,T> pool, final int iter, final int delay) {
+        public TestThread(final KeyedObjectPool<String, T, E> pool, final int iter, final int delay) {
             this(pool, iter, delay, delay, true, null, null);
         }
 
-        public TestThread(final KeyedObjectPool<String,T> pool, final int iter, final int startDelay,
+        public TestThread(final KeyedObjectPool<String, T, E> pool, final int iter, final int startDelay,
             final int holdTime, final boolean randomDelay, final T expectedObject, final String key) {
             this.pool = pool;
             this.iter = iter;
@@ -393,13 +397,13 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
 
         @Override
         public void run() {
-            for(int i=0;i<iter;i++) {
+            for (int i = 0; i < iter; i++) {
                 final String actualKey = key == null ? String.valueOf(random.nextInt(3)) : key;
                 Waiter.sleepQuietly(randomDelay ? random.nextInt(startDelay) : startDelay);
                 T obj = null;
                 try {
                     obj = pool.borrowObject(actualKey);
-                } catch(final Exception e) {
+                } catch (final Exception e) {
                     exception = e;
                     failed = true;
                     complete = true;
@@ -407,7 +411,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
                 }
 
                 if (expectedObject != null && !expectedObject.equals(obj)) {
-                    exception = new Exception("Expected: "+expectedObject+ " found: "+obj);
+                    exception = new Exception("Expected: " + expectedObject + " found: " + obj);
                     failed = true;
                     complete = true;
                     break;
@@ -415,8 +419,8 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
 
                 Waiter.sleepQuietly(randomDelay ? random.nextInt(holdTime) : holdTime);
                 try {
-                    pool.returnObject(actualKey,obj);
-                } catch(final Exception e) {
+                    pool.returnObject(actualKey, obj);
+                } catch (final Exception e) {
                     exception = e;
                     failed = true;
                     complete = true;
@@ -431,8 +435,8 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
      * Very simple test thread that just tries to borrow an object from
      * the provided pool with the specified key and returns it after a wait
      */
-    static class WaitingTestThread extends Thread {
-        private final KeyedObjectPool<String,String> pool;
+    static class WaitingTestThread<E extends Exception> extends Thread {
+        private final KeyedObjectPool<String, String, E> pool;
         private final String key;
         private final long pause;
         private Throwable thrown;
@@ -443,7 +447,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
         private long endedMillis;
         private String objectId;
 
-        public WaitingTestThread(final KeyedObjectPool<String,String> pool, final String key, final long pause) {
+        public WaitingTestThread(final KeyedObjectPool<String, String, E> pool, final String key, final long pause) {
             this.pool = pool;
             this.key = key;
             this.pause = pause;
@@ -481,14 +485,14 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
     // @see https://issues.apache.org/jira/browse/SUREFIRE-121
 
     /** setUp(): {@code new GenericKeyedObjectPool<String,String>(factory)} */
-    private GenericKeyedObjectPool<String,String> gkoPool;
+    private GenericKeyedObjectPool<String, String, Exception> gkoPool;
 
     /** setUp(): {@code new SimpleFactory<String>()} */
     private SimpleFactory<String> simpleFactory;
 
     private void checkEvictionOrder(final boolean lifo) throws Exception {
         final SimpleFactory<Integer> intFactory = new SimpleFactory<>();
-        try (final GenericKeyedObjectPool<Integer, String> intPool = new GenericKeyedObjectPool<>(intFactory)) {
+        try (final GenericKeyedObjectPool<Integer, String, Exception> intPool = new GenericKeyedObjectPool<>(intFactory)) {
             intPool.setNumTestsPerEvictionRun(2);
             intPool.setMinEvictableIdleTime(Duration.ofMillis(100));
             intPool.setLifo(lifo);
@@ -599,7 +603,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
 
     private void checkEvictorVisiting(final boolean lifo) throws Exception {
         VisitTrackerFactory<Integer> trackerFactory = new VisitTrackerFactory<>();
-        try (GenericKeyedObjectPool<Integer, VisitTracker<Integer>> intPool = new GenericKeyedObjectPool<>(
+        try (GenericKeyedObjectPool<Integer, VisitTracker<Integer>, RuntimeException> intPool = new GenericKeyedObjectPool<>(
                 trackerFactory)) {
             intPool.setNumTestsPerEvictionRun(2);
             intPool.setMinEvictableIdleTime(Duration.ofMillis(-1));
@@ -683,7 +687,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
                 // Can't use clear as some objects are still active so create
                 // a new pool
                 trackerFactory = new VisitTrackerFactory<>();
-                try (GenericKeyedObjectPool<Integer, VisitTracker<Integer>> intPool = new GenericKeyedObjectPool<>(
+                try (GenericKeyedObjectPool<Integer, VisitTracker<Integer>, RuntimeException> intPool = new GenericKeyedObjectPool<>(
                         trackerFactory)) {
                     intPool.setMaxIdlePerKey(-1);
                     intPool.setMaxTotalPerKey(-1);
@@ -799,19 +803,18 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
         return true;
     }
 
+    @SuppressWarnings("unchecked")
     @Override
-    protected KeyedObjectPool<Object,Object> makeEmptyPool(final int minCapacity) {
-        final KeyedPooledObjectFactory<Object,Object> perKeyFactory =
-                new SimplePerKeyFactory();
-        final GenericKeyedObjectPool<Object,Object> perKeyPool =
-            new GenericKeyedObjectPool<>(perKeyFactory);
+    protected <E extends Exception> KeyedObjectPool<Object, Object, E> makeEmptyPool(final int minCapacity) {
+        final KeyedPooledObjectFactory<Object, Object, RuntimeException> perKeyFactory = new SimplePerKeyFactory();
+        final GenericKeyedObjectPool<Object, Object, RuntimeException> perKeyPool = new GenericKeyedObjectPool<>(perKeyFactory);
         perKeyPool.setMaxTotalPerKey(minCapacity);
         perKeyPool.setMaxIdlePerKey(minCapacity);
-        return perKeyPool;
+        return (KeyedObjectPool<Object, Object, E>) perKeyPool;
     }
 
     @Override
-    protected KeyedObjectPool<Object,Object> makeEmptyPool(final KeyedPooledObjectFactory<Object,Object> fac) {
+    protected <E extends Exception> KeyedObjectPool<Object, Object, E> makeEmptyPool(final KeyedPooledObjectFactory<Object, Object, E> fac) {
         return new GenericKeyedObjectPool<>(fac);
     }
 
@@ -831,27 +834,27 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
      * @param delay         Maximum delay between iterations
      * @param gkopPool      The keyed object pool to use
      */
-    public <T> void runTestThreads(final int numThreads, final int iterations, final int delay, final GenericKeyedObjectPool<String,T> gkopPool) {
-        final ArrayList<TestThread<T>> threads = new ArrayList<>();
-        for(int i=0;i<numThreads;i++) {
-            final TestThread<T> testThread = new TestThread<>(gkopPool, iterations, delay);
+    public <T, E extends Exception> void runTestThreads(final int numThreads, final int iterations, final int delay,
+            final GenericKeyedObjectPool<String, T, E> gkopPool) {
+        final ArrayList<TestThread<T, E>> threads = new ArrayList<>();
+        for (int i = 0; i < numThreads; i++) {
+            final TestThread<T, E> testThread = new TestThread<>(gkopPool, iterations, delay);
             threads.add(testThread);
             final Thread t = new Thread(testThread);
             t.start();
         }
-        for (final TestThread<T> testThread : threads) {
-            while(!(testThread.complete())) {
+        for (final TestThread<T, E> testThread : threads) {
+            while (!(testThread.complete())) {
                 Waiter.sleepQuietly(500L);
             }
-            if(testThread.failed()) {
-                fail("Thread failed: " + threads.indexOf(testThread) + "\n" +
-                        getExceptionTrace(testThread.exception));
+            if (testThread.failed()) {
+                fail("Thread failed: " + threads.indexOf(testThread) + "\n" + getExceptionTrace(testThread.exception));
             }
         }
     }
 
     @BeforeEach
-    public void setUp() throws Exception {
+    public void setUp() {
         simpleFactory = new SimpleFactory<>();
         gkoPool = new GenericKeyedObjectPool<>(simpleFactory);
     }
@@ -890,7 +893,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
     public void testAppendStats() {
         assertFalse(gkoPool.getMessageStatistics());
         assertEquals("foo", (gkoPool.appendStats("foo")));
-        try (final GenericKeyedObjectPool<?, ?> pool = new GenericKeyedObjectPool<>(new SimpleFactory<>())) {
+        try (final GenericKeyedObjectPool<?, ?, Exception> pool = new GenericKeyedObjectPool<>(new SimpleFactory<>())) {
             pool.setMessagesStatistics(true);
             assertNotEquals("foo", (pool.appendStats("foo")));
             pool.setMessagesStatistics(false);
@@ -899,7 +902,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
     }
 
     @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    @Timeout(value = 60_000, unit = TimeUnit.MILLISECONDS)
     public void testBlockedKeyDoesNotBlockPool() throws Exception {
         gkoPool.setBlockWhenExhausted(true);
         gkoPool.setMaxWaitMillis(5000);
@@ -934,7 +937,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
         "rawtypes"
     })
     @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    @Timeout(value = 60_000, unit = TimeUnit.MILLISECONDS)
     public void testBorrowObjectFairness() throws Exception {
 
         final int numThreads = 40;
@@ -991,7 +994,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
      * @throws Exception May occur in some failure modes
      */
     @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    @Timeout(value = 60_000, unit = TimeUnit.MILLISECONDS)
     public void testClear() throws Exception {
         gkoPool.setMaxTotal(2);
         gkoPool.setMaxTotalPerKey(2);
@@ -1014,18 +1017,17 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
         gkoPool.close();
     }
 
-
     /**
      * Test to make sure that clearOldest does not destroy instances that have been checked out.
      *
      * @throws Exception May occur in some failure modes
      */
     @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    @Timeout(value = 60_000, unit = TimeUnit.MILLISECONDS)
     public void testClearOldest() throws Exception {
         // Make destroy have some latency so clearOldest takes some time
         final WaiterFactory<String> waiterFactory = new WaiterFactory<>(0, 20, 0, 0, 0, 0, 50, 5, 0);
-        try (final GenericKeyedObjectPool<String, Waiter> waiterPool = new GenericKeyedObjectPool<>(waiterFactory)) {
+        try (final GenericKeyedObjectPool<String, Waiter, RuntimeException> waiterPool = new GenericKeyedObjectPool<>(waiterFactory)) {
             waiterPool.setMaxTotalPerKey(5);
             waiterPool.setMaxTotal(50);
             waiterPool.setLifo(false);
@@ -1041,7 +1043,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
             // Now set up a race - one thread wants a new instance, triggering clearOldest
             // Other goes after an element on death row
             // See if we end up with dead man walking
-            final SimpleTestThread<Waiter> t2 = new SimpleTestThread<>(waiterPool, "51");
+            final SimpleTestThread<Waiter, RuntimeException> t2 = new SimpleTestThread<>(waiterPool, "51");
             final Thread thread2 = new Thread(t2);
             thread2.start(); // Triggers clearOldest, killing all of the 0's and the 2 oldest 1's
             Thread.sleep(50); // Wait for clearOldest to kick off, but not long enough to reach the 1's
@@ -1051,13 +1053,135 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
         }
     }
 
+
+    /**
+       * POOL-391 Verify that when clear(key) is called with reuseCapacity true,
+       * capacity freed is reused and allocated to most loaded pools.
+       *
+       * @throws Exception May occur in some failure modes
+       */
+      @Test
+      public void testClearReuseCapacity() throws Exception {
+          gkoPool.setMaxTotalPerKey(6);
+          gkoPool.setMaxTotal(6);
+          gkoPool.setMaxWait(Duration.ofSeconds(5));
+          // Create one thread to wait on "one", two on "two", three on "three"
+          final ArrayList<Thread> testThreads = new ArrayList<>();
+          testThreads.add(new Thread(new SimpleTestThread<>(gkoPool, "one")));
+          testThreads.add(new Thread(new SimpleTestThread<>(gkoPool, "two")));
+          testThreads.add(new Thread(new SimpleTestThread<>(gkoPool, "two")));
+          testThreads.add(new Thread(new SimpleTestThread<>(gkoPool, "three")));
+          testThreads.add(new Thread(new SimpleTestThread<>(gkoPool, "three")));
+          testThreads.add(new Thread(new SimpleTestThread<>(gkoPool, "three")));
+          // Borrow two each from "four", "five", "six" - using all capacity
+          final String four = gkoPool.borrowObject("four");
+          final String four2 = gkoPool.borrowObject("four");
+          final String five = gkoPool.borrowObject("five");
+          final String five2 = gkoPool.borrowObject("five");
+          final String six = gkoPool.borrowObject("six");
+          final String six2 = gkoPool.borrowObject("six");
+          Thread.sleep(100);
+          // Launch the waiters - all will be blocked waiting
+          for (Thread t : testThreads) {
+              t.start();
+          }
+          Thread.sleep(100);
+          // Return and clear the fours - at least one "three" should get served
+          // Other should be a two or a three (three was most loaded)
+          gkoPool.returnObject("four", four);
+          gkoPool.returnObject("four", four2);
+          gkoPool.clear("four");
+          Thread.sleep(20);
+          assertTrue(!testThreads.get(3).isAlive() || !testThreads.get(4).isAlive() || !testThreads.get(5).isAlive());
+          // Now clear the fives
+          gkoPool.returnObject("five", five);
+          gkoPool.returnObject("five", five2);
+          gkoPool.clear("five");
+          Thread.sleep(20);
+          // Clear the sixes
+          gkoPool.returnObject("six", six);
+          gkoPool.returnObject("six", six2);
+          gkoPool.clear("six");
+          Thread.sleep(20);
+          for (Thread t : testThreads) {
+              assertFalse(t.isAlive());
+          }
+      }
+
+    /**
+     * POOL-391 Adapted from code in the JIRA ticket.
+     *
+     * @throws Exception May occur in some failure modes
+     */
+    @Test
+    @Timeout(value = 2000, unit = TimeUnit.MILLISECONDS)
+    public void testClearUnblocksWaiters() {
+        final GenericKeyedObjectPoolConfig<Integer> config = new GenericKeyedObjectPoolConfig<>();
+        config.setMaxTotalPerKey(1);
+        config.setMinIdlePerKey(0);
+        config.setMaxIdlePerKey(-1);
+        config.setMaxTotal(-1);
+        config.setMaxWait(Duration.ofMillis(5));
+        GenericKeyedObjectPool<Integer, Integer, InterruptedException> testPool = new GenericKeyedObjectPool<>(
+                new KeyedPooledObjectFactory<Integer, Integer, InterruptedException>() {
+                    @Override
+                    public void activateObject(Integer key, PooledObject<Integer> p) {
+                        // do nothing
+                    }
+
+                    @Override
+                    public void destroyObject(Integer key, PooledObject<Integer> p) throws InterruptedException {
+                        Thread.sleep(500);
+                    }
+
+                    @Override
+                    public PooledObject<Integer> makeObject(Integer key) {
+                        return new DefaultPooledObject<>(10);
+                    }
+
+                    @Override
+                    public void passivateObject(Integer key, PooledObject<Integer> p) {
+                        // do nothing
+                    }
+
+                    @Override
+                    public boolean validateObject(Integer key, PooledObject<Integer> p) {
+                        return true;
+                    }
+                }, config);
+        final int borrowKey = 10;
+        Thread t = new Thread(() -> {
+            try {
+                while (true) {
+                    Integer integer = testPool.borrowObject(borrowKey);
+                    testPool.returnObject(borrowKey, integer);
+                    Thread.sleep(10);
+                }
+            } catch (Exception e) {
+                fail();
+            }
+        });
+        Thread t2 = new Thread(() -> {
+            try {
+                while (true) {
+                    testPool.clear(borrowKey);
+                    Thread.sleep(10);
+                }
+            } catch (Exception e) {
+                fail();
+            }
+        });
+        t.start();
+        t2.start();
+    }
+
     // POOL-259
     @Test
     public void testClientWaitStats() throws Exception {
         final SimpleFactory<String> factory = new SimpleFactory<>();
         // Give makeObject a little latency
         factory.setMakeLatency(200);
-        try (final GenericKeyedObjectPool<String, String> pool = new GenericKeyedObjectPool<>(factory,
+        try (final GenericKeyedObjectPool<String, String, Exception> pool = new GenericKeyedObjectPool<>(factory,
                 new GenericKeyedObjectPoolConfig<>())) {
             final String s = pool.borrowObject("one");
             // First borrow waits on create, so wait time should be at least 200 ms
@@ -1112,7 +1236,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
             // Launch nThreads threads all trying to invalidate the target
             for (int i = 0; i < nThreads; i++) {
                 threads[i] =
-                        new InvalidateThread(gkoPool,key, obj[targ.intValue()]);
+                        new InvalidateThread(gkoPool, key, obj[targ.intValue()]);
             }
             for (int i = 0; i < nThreads; i++) {
                 new Thread(threads[i]).start();
@@ -1138,7 +1262,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
 
     @SuppressWarnings("deprecation")
     @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    @Timeout(value = 60_000, unit = TimeUnit.MILLISECONDS)
     public void testConstructors() {
 
         // Make constructor arguments all different from defaults
@@ -1156,9 +1280,9 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
         final long timeBetweenEvictionRunsMillis = 8;
         final boolean blockWhenExhausted = false;
         final boolean lifo = false;
-        final KeyedPooledObjectFactory<Object, Object> dummyFactory = new DummyFactory();
+        final KeyedPooledObjectFactory<Object, Object, RuntimeException> dummyFactory = new DummyFactory();
 
-        try (GenericKeyedObjectPool<Object, Object> objPool = new GenericKeyedObjectPool<>(dummyFactory)) {
+        try (GenericKeyedObjectPool<Object, Object, RuntimeException> objPool = new GenericKeyedObjectPool<>(dummyFactory)) {
             assertEquals(GenericKeyedObjectPoolConfig.DEFAULT_MAX_TOTAL_PER_KEY, objPool.getMaxTotalPerKey());
             assertEquals(GenericKeyedObjectPoolConfig.DEFAULT_MAX_IDLE_PER_KEY, objPool.getMaxIdlePerKey());
             assertEquals(BaseObjectPoolConfig.DEFAULT_MAX_WAIT_MILLIS, objPool.getMaxWaitMillis());
@@ -1206,7 +1330,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
         config.setTestWhileIdle(testWhileIdle);
         config.setTimeBetweenEvictionRuns(Duration.ofMillis(timeBetweenEvictionRunsMillis));
         config.setBlockWhenExhausted(blockWhenExhausted);
-        try (GenericKeyedObjectPool<Object, Object> objPool = new GenericKeyedObjectPool<>(dummyFactory, config)) {
+        try (GenericKeyedObjectPool<Object, Object, RuntimeException> objPool = new GenericKeyedObjectPool<>(dummyFactory, config)) {
             assertEquals(maxTotalPerKey, objPool.getMaxTotalPerKey());
             assertEquals(maxIdle, objPool.getMaxIdlePerKey());
             assertEquals(maxWaitDuration, objPool.getMaxWaitDuration());
@@ -1238,7 +1362,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
         config.setTimeBetweenEvictionRuns(Duration.ofMillis(500));
         config.setMinEvictableIdleTime(Duration.ofMillis(50));
         config.setNumTestsPerEvictionRun(5);
-        try (final GenericKeyedObjectPool<String, String> p = new GenericKeyedObjectPool<>(simpleFactory, config)) {
+        try (final GenericKeyedObjectPool<String, String, Exception> p = new GenericKeyedObjectPool<>(simpleFactory, config)) {
             for (int i = 0; i < 5; i++) {
                 p.addObject("one");
             }
@@ -1258,7 +1382,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
     @Test
     public void testEqualsIndiscernible() throws Exception {
         final HashSetFactory factory = new HashSetFactory();
-        try (final GenericKeyedObjectPool<String, HashSet<String>> pool = new GenericKeyedObjectPool<>(factory,
+        try (final GenericKeyedObjectPool<String, HashSet<String>, RuntimeException> pool = new GenericKeyedObjectPool<>(factory,
                 new GenericKeyedObjectPoolConfig<>())) {
             final HashSet<String> s1 = pool.borrowObject("a");
             final HashSet<String> s2 = pool.borrowObject("a");
@@ -1268,7 +1392,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
     }
 
     @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    @Timeout(value = 60_000, unit = TimeUnit.MILLISECONDS)
     public void testEviction() throws Exception {
         gkoPool.setMaxIdlePerKey(500);
         gkoPool.setMaxTotalPerKey(500);
@@ -1319,7 +1443,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
     }
 
     @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    @Timeout(value = 60_000, unit = TimeUnit.MILLISECONDS)
     public void testEviction2() throws Exception {
         gkoPool.setMaxIdlePerKey(500);
         gkoPool.setMaxTotalPerKey(500);
@@ -1372,7 +1496,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
      * @throws Exception May occur in some failure modes
      */
     @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    @Timeout(value = 60_000, unit = TimeUnit.MILLISECONDS)
     public void testEvictionOrder() throws Exception {
         checkEvictionOrder(false);
         checkEvictionOrder(true);
@@ -1418,14 +1542,14 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
      * @throws Exception May occur in some failure modes
      */
     @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    @Timeout(value = 60_000, unit = TimeUnit.MILLISECONDS)
     public void testEvictorVisiting() throws Exception {
         checkEvictorVisiting(true);
         checkEvictorVisiting(false);
     }
 
     @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    @Timeout(value = 60_000, unit = TimeUnit.MILLISECONDS)
     public void testExceptionInValidationDuringEviction() throws Exception {
         gkoPool.setMaxIdlePerKey(1);
         gkoPool.setMinEvictableIdleTime(Duration.ZERO);
@@ -1440,8 +1564,9 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
         assertEquals(0, gkoPool.getNumIdle());
     }
 
+
     @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    @Timeout(value = 60_000, unit = TimeUnit.MILLISECONDS)
     public void testExceptionOnActivateDuringBorrow() throws Exception {
         final String obj1 = gkoPool.borrowObject("one");
         final String obj2 = gkoPool.borrowObject("one");
@@ -1468,9 +1593,8 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
         assertEquals(0, gkoPool.getNumIdle());
     }
 
-
     @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    @Timeout(value = 60_000, unit = TimeUnit.MILLISECONDS)
     public void testExceptionOnDestroyDuringBorrow() throws Exception {
         simpleFactory.setThrowExceptionOnDestroy(true);
         simpleFactory.setValidationEnabled(true);
@@ -1485,7 +1609,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
     }
 
     @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    @Timeout(value = 60_000, unit = TimeUnit.MILLISECONDS)
     public void testExceptionOnDestroyDuringReturn() throws Exception {
         simpleFactory.setThrowExceptionOnDestroy(true);
         simpleFactory.setValidationEnabled(true);
@@ -1501,7 +1625,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
     }
 
     @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    @Timeout(value = 60_000, unit = TimeUnit.MILLISECONDS)
     public void testExceptionOnPassivateDuringReturn() throws Exception {
         final String obj = gkoPool.borrowObject("one");
         simpleFactory.setThrowExceptionOnPassivate(true);
@@ -1511,7 +1635,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
     }
 
     @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    @Timeout(value = 60_000, unit = TimeUnit.MILLISECONDS)
     public void testFIFO() throws Exception {
         gkoPool.setLifo(false);
         final String key = "key";
@@ -1528,6 +1652,19 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
         assertEquals( "key4", gkoPool.borrowObject(key),"new-4");
     }
 
+    @Test
+    @Timeout(value = 60, unit = TimeUnit.SECONDS)
+    public void testGetKeys() throws Exception {
+        gkoPool.addObject("one");
+        assertEquals(1, gkoPool.getKeys().size());
+        gkoPool.addObject("two");
+        assertEquals(2, gkoPool.getKeys().size());
+        gkoPool.clear("one");
+        assertEquals(1, gkoPool.getKeys().size());
+        assertEquals("two", (String) gkoPool.getKeys().get(0));
+        gkoPool.clear();
+    }
+
     @Test
     public void testGetStatsString() {
         assertNotNull((gkoPool.getStatsString()));
@@ -1544,7 +1681,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
     @Test
     public void testInvalidateFreesCapacity() throws Exception {
         final SimpleFactory<String> factory = new SimpleFactory<>();
-        try (final GenericKeyedObjectPool<String, String> pool = new GenericKeyedObjectPool<>(factory)) {
+        try (final GenericKeyedObjectPool<String, String, Exception> pool = new GenericKeyedObjectPool<>(factory)) {
             pool.setMaxTotalPerKey(2);
             pool.setMaxWaitMillis(500);
             // Borrow an instance and hold if for 5 seconds
@@ -1565,6 +1702,30 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
         }
     }
 
+    @Test
+    public void testInvalidateFreesCapacityForOtherKeys() throws Exception {
+        gkoPool.setMaxTotal(1);
+        gkoPool.setMaxWait(Duration.ofMillis(500));
+        Thread borrower = new Thread(new SimpleTestThread<>(gkoPool, "two"));
+        String obj = gkoPool.borrowObject("one");
+        borrower.start();  // Will block
+        Thread.sleep(100);  // Make sure borrower has started
+        gkoPool.invalidateObject("one", obj);  // Should free capacity to serve the other key
+        Thread.sleep(20);  // Should have been served by now
+        assertFalse(borrower.isAlive());
+    }
+
+    @Test
+    public void testInvalidateMissingKey() throws Exception {
+        assertThrows(IllegalStateException.class, () -> gkoPool.invalidateObject("UnknownKey", "Ignored"));
+    }
+
+    @ParameterizedTest
+    @EnumSource(DestroyMode.class)
+    public void testInvalidateMissingKeyForDestroyMode(final DestroyMode destroyMode) throws Exception {
+        assertThrows(IllegalStateException.class, () -> gkoPool.invalidateObject("UnknownKey", "Ignored", destroyMode));
+    }
+
     /**
      * Verify that threads blocked waiting on a depleted pool get served when a checked out instance
      * is invalidated.
@@ -1588,8 +1749,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
         config.setTestWhileIdle(true);
         config.setTimeBetweenEvictionRuns(Duration.ofMillis(-1));
 
-        try (final GenericKeyedObjectPool<Integer, Object> pool = new GenericKeyedObjectPool<>(new ObjectFactory(),
-                config)) {
+        try (final GenericKeyedObjectPool<Integer, Object, RuntimeException> pool = new GenericKeyedObjectPool<>(new ObjectFactory(), config)) {
 
             // Allocate both objects with this thread
             pool.borrowObject(Integer.valueOf(1)); // object1
@@ -1633,7 +1793,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
      * Ensure the pool is registered.
      */
     @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    @Timeout(value = 60_000, unit = TimeUnit.MILLISECONDS)
     public void testJmxRegistration() {
         final ObjectName oname = gkoPool.getJmxName();
         final MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
@@ -1642,7 +1802,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
     }
 
     @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    @Timeout(value = 60_000, unit = TimeUnit.MILLISECONDS)
     public void testLIFO() throws Exception {
         gkoPool.setLifo(true);
         final String key = "key";
@@ -1666,7 +1826,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
      * @throws Exception May occur in some failure modes
      */
     @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    @Timeout(value = 60_000, unit = TimeUnit.MILLISECONDS)
     public void testLivenessPerKey() throws Exception {
         gkoPool.setMaxIdlePerKey(3);
         gkoPool.setMaxTotal(3);
@@ -1696,7 +1856,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
     @Test
     public void testMakeObjectException() throws Exception {
         final SimpleFactory<String> factory = new SimpleFactory<>();
-        try (final GenericKeyedObjectPool<String, String> pool = new GenericKeyedObjectPool<>(factory)) {
+        try (final GenericKeyedObjectPool<String, String, Exception> pool = new GenericKeyedObjectPool<>(factory)) {
             pool.setMaxTotalPerKey(1);
             pool.setBlockWhenExhausted(false);
             factory.exceptionOnCreate = true;
@@ -1715,7 +1875,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
         final WaiterFactory<String> waiterFactory = new WaiterFactory<>(0, 20, 0, 0, 0, 0, 8, 5, 0);
         // TODO Fix this. Can't use local pool since runTestThreads uses the
         // protected pool field
-        try (final GenericKeyedObjectPool<String, Waiter> waiterPool = new GenericKeyedObjectPool<>(waiterFactory)) {
+        try (final GenericKeyedObjectPool<String, Waiter, RuntimeException> waiterPool = new GenericKeyedObjectPool<>(waiterFactory)) {
             waiterPool.setMaxTotalPerKey(5);
             waiterPool.setMaxTotal(8);
             waiterPool.setTestOnBorrow(true);
@@ -1726,7 +1886,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
     }
 
     @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    @Timeout(value = 60_000, unit = TimeUnit.MILLISECONDS)
     public void testMaxIdle() throws Exception {
         gkoPool.setMaxTotalPerKey(100);
         gkoPool.setMaxIdlePerKey(8);
@@ -1762,7 +1922,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
     }
 
     @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    @Timeout(value = 60_000, unit = TimeUnit.MILLISECONDS)
     public void testMaxTotal() throws Exception {
         gkoPool.setMaxTotalPerKey(2);
         gkoPool.setMaxTotal(3);
@@ -1805,7 +1965,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
      * validation failures.
      */
     @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    @Timeout(value = 60_000, unit = TimeUnit.MILLISECONDS)
     public void testMaxTotalInvariant() {
         final int maxTotal = 15;
         simpleFactory.setEvenValid(false);     // Every other validation fails
@@ -1820,7 +1980,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
     }
 
     @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    @Timeout(value = 60_000, unit = TimeUnit.MILLISECONDS)
     public void testMaxTotalLRU() throws Exception {
         gkoPool.setMaxTotalPerKey(2);
         gkoPool.setMaxTotal(3);
@@ -1871,7 +2031,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
     }
 
     @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    @Timeout(value = 60_000, unit = TimeUnit.MILLISECONDS)
     public void testMaxTotalPerKey() throws Exception {
         gkoPool.setMaxTotalPerKey(3);
         gkoPool.setBlockWhenExhausted(false);
@@ -1882,15 +2042,17 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
         assertThrows(NoSuchElementException.class, () -> gkoPool.borrowObject(""));
     }
 
+
     @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testMaxTotalPerKeyZero() throws Exception {
+    @Timeout(value = 60_000, unit = TimeUnit.MILLISECONDS)
+    public void testMaxTotalPerKeyZero() {
         gkoPool.setMaxTotalPerKey(0);
         gkoPool.setBlockWhenExhausted(false);
 
         assertThrows(NoSuchElementException.class, () -> gkoPool.borrowObject("a"));
     }
 
+
     /**
      * Verifies that if a borrow of a new key is blocked because maxTotal has
      * been reached, that borrow continues once another object is returned.
@@ -1905,8 +2067,8 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
 
         final int holdTime = 2000;
 
-        final TestThread<String> testA = new TestThread<>(gkoPool, 1, 0, holdTime, false, null, "a");
-        final TestThread<String> testB = new TestThread<>(gkoPool, 1, 0, holdTime, false, null, "b");
+        final TestThread<String, Exception> testA = new TestThread<>(gkoPool, 1, 0, holdTime, false, null, "a");
+        final TestThread<String, Exception> testB = new TestThread<>(gkoPool, 1, 0, holdTime, false, null, "b");
 
         final Thread threadA = new Thread(testA);
         final Thread threadB = new Thread(testB);
@@ -1933,15 +2095,14 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
     }
 
     @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
-    public void testMaxTotalZero() throws Exception {
+    @Timeout(value = 60_000, unit = TimeUnit.MILLISECONDS)
+    public void testMaxTotalZero() {
         gkoPool.setMaxTotal(0);
         gkoPool.setBlockWhenExhausted(false);
 
         assertThrows(NoSuchElementException.class, () -> gkoPool.borrowObject("a"));
     }
 
-
     /*
      * Test multi-threaded pool access.
      * Multiple keys, multiple threads, but maxActive only allows half the threads to succeed.
@@ -1951,7 +2112,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
      * Let's see if the this fails on Continuum too!
      */
     @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    @Timeout(value = 60_000, unit = TimeUnit.MILLISECONDS)
     public void testMaxWaitMultiThreaded() throws Exception {
         final long maxWait = 500; // wait for connection
         final long holdTime = 4 * maxWait; // how long to hold connection
@@ -2000,9 +2161,8 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
         assertEquals(wtt.length/2,failed,"Expected half the threads to fail");
     }
 
-
     @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    @Timeout(value = 60_000, unit = TimeUnit.MILLISECONDS)
     public void testMinIdle() throws Exception {
         gkoPool.setMaxIdlePerKey(500);
         gkoPool.setMinIdlePerKey(5);
@@ -2042,7 +2202,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
     }
 
     @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    @Timeout(value = 60_000, unit = TimeUnit.MILLISECONDS)
     public void testMinIdleMaxTotalPerKey() throws Exception {
         gkoPool.setMaxIdlePerKey(500);
         gkoPool.setMinIdlePerKey(5);
@@ -2096,7 +2256,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
     }
 
     @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    @Timeout(value = 60_000, unit = TimeUnit.MILLISECONDS)
     public void testMinIdleNoPreparePool() throws Exception {
         gkoPool.setMaxIdlePerKey(500);
         gkoPool.setMinIdlePerKey(5);
@@ -2129,7 +2289,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
     @Test
     public void testMultipleReturn() throws Exception {
         final WaiterFactory<String> factory = new WaiterFactory<>(0, 0, 0, 0, 0, 0);
-        try (final GenericKeyedObjectPool<String, Waiter> pool = new GenericKeyedObjectPool<>(factory)) {
+        try (final GenericKeyedObjectPool<String, Waiter, RuntimeException> pool = new GenericKeyedObjectPool<>(factory)) {
             pool.setTestOnReturn(true);
             final Waiter waiter = pool.borrowObject("a");
             pool.returnObject("a", waiter);
@@ -2155,7 +2315,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
     @Test
     public void testMutable() throws Exception {
         final HashSetFactory factory = new HashSetFactory();
-        try (final GenericKeyedObjectPool<String, HashSet<String>> pool = new GenericKeyedObjectPool<>(factory,
+        try (final GenericKeyedObjectPool<String, HashSet<String>, RuntimeException> pool = new GenericKeyedObjectPool<>(factory,
                 new GenericKeyedObjectPoolConfig<>())) {
             final HashSet<String> s1 = pool.borrowObject("a");
             final HashSet<String> s2 = pool.borrowObject("a");
@@ -2167,7 +2327,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
     }
 
     @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    @Timeout(value = 60_000, unit = TimeUnit.MILLISECONDS)
     public void testNegativeMaxTotalPerKey() throws Exception {
         gkoPool.setMaxTotalPerKey(-1);
         gkoPool.setBlockWhenExhausted(false);
@@ -2177,7 +2337,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
     }
 
     @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    @Timeout(value = 60_000, unit = TimeUnit.MILLISECONDS)
     public void testNumActiveNumIdle2() throws Exception {
         assertEquals(0,gkoPool.getNumActive());
         assertEquals(0,gkoPool.getNumIdle());
@@ -2229,12 +2389,42 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
 
     @Test
     public void testReturnObjectThrowsIllegalStateException() {
-        try (final GenericKeyedObjectPool<String, String> pool = new GenericKeyedObjectPool<>(new SimpleFactory<>())) {
+        try (final GenericKeyedObjectPool<String, String, Exception> pool = new GenericKeyedObjectPool<>(new SimpleFactory<>())) {
             assertThrows(IllegalStateException.class,
                     () ->  pool.returnObject("Foo", "Bar"));
         }
     }
 
+
+    @Test
+    public void testReturnObjectWithBlockWhenExhausted() throws Exception {
+        gkoPool.setBlockWhenExhausted(true);
+        gkoPool.setMaxTotal(1);
+
+        // Test return object with no take waiters
+        String obj = gkoPool.borrowObject("0");
+        gkoPool.returnObject("0", obj);
+
+        // Test return object with a take waiter
+        final TestThread<String, Exception> testA = new TestThread<>(gkoPool, 1, 0, 500, false, null, "0");
+        final TestThread<String, Exception> testB = new TestThread<>(gkoPool, 1, 0, 0, false, null, "1");
+        final Thread threadA = new Thread(testA);
+        final Thread threadB = new Thread(testB);
+        threadA.start();
+        threadB.start();
+        threadA.join();
+        threadB.join();
+    }
+
+    @Test
+    public void testReturnObjectWithoutBlockWhenExhausted() throws Exception {
+        gkoPool.setBlockWhenExhausted(false);
+
+        // Test return object with no take waiters
+        String obj = gkoPool.borrowObject("0");
+        gkoPool.returnObject("0", obj);
+    }
+
     /**
      * JIRA: POOL-287
      *
@@ -2248,7 +2438,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
         final SimpleFactory<String> factory = new SimpleFactory<>();
         factory.setValidateLatency(100);
         factory.setValid(true); // Validation always succeeds
-        try (final GenericKeyedObjectPool<String, String> pool = new GenericKeyedObjectPool<>(factory)) {
+        try (final GenericKeyedObjectPool<String, String, Exception> pool = new GenericKeyedObjectPool<>(factory)) {
             pool.setMaxWaitMillis(1000);
             pool.setTestWhileIdle(true);
             pool.setMaxTotalPerKey(2);
@@ -2274,7 +2464,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
     }
 
     @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    @Timeout(value = 60_000, unit = TimeUnit.MILLISECONDS)
     public void testSettersAndGetters() {
         {
             gkoPool.setMaxTotalPerKey(123);
@@ -2343,7 +2533,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
     }
 
     @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    @Timeout(value = 60_000, unit = TimeUnit.MILLISECONDS)
     public void testThreaded1() {
         gkoPool.setMaxTotalPerKey(15);
         gkoPool.setMaxIdlePerKey(15);
@@ -2351,7 +2541,6 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
         runTestThreads(20, 100, 50, gkoPool);
     }
 
-
     // Pool-361
     @Test
     public void testValidateOnCreate() throws Exception {
@@ -2361,26 +2550,26 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
         assertEquals(1, simpleFactory.validateCounter);
     }
 
-    @Test
-    public void testValidateOnCreateFailure() throws Exception {
-        gkoPool.setTestOnCreate(true);
-        gkoPool.setTestOnBorrow(false);
-        gkoPool.setMaxTotal(2);
-        simpleFactory.setValidationEnabled(true);
-        simpleFactory.setValid(false);
-        // Make sure failed validations do not leak capacity
-        gkoPool.addObject("one");
-        gkoPool.addObject("one");
-        assertEquals(0, gkoPool.getNumIdle());
-        assertEquals(0, gkoPool.getNumActive());
-        simpleFactory.setValid(true);
-        final String obj = gkoPool.borrowObject("one");
-        assertNotNull(obj);
-        gkoPool.addObject("one");
-        // Should have one idle, one out now
-        assertEquals(1, gkoPool.getNumIdle());
-        assertEquals(1, gkoPool.getNumActive());
-    }
+   @Test
+public void testValidateOnCreateFailure() throws Exception {
+    gkoPool.setTestOnCreate(true);
+    gkoPool.setTestOnBorrow(false);
+    gkoPool.setMaxTotal(2);
+    simpleFactory.setValidationEnabled(true);
+    simpleFactory.setValid(false);
+    // Make sure failed validations do not leak capacity
+    gkoPool.addObject("one");
+    gkoPool.addObject("one");
+    assertEquals(0, gkoPool.getNumIdle());
+    assertEquals(0, gkoPool.getNumActive());
+    simpleFactory.setValid(true);
+    final String obj = gkoPool.borrowObject("one");
+    assertNotNull(obj);
+    gkoPool.addObject("one");
+    // Should have one idle, one out now
+    assertEquals(1, gkoPool.getNumIdle());
+    assertEquals(1, gkoPool.getNumActive());
+}
 
     /**
      * Verify that threads waiting on a depleted pool get served when a returning object fails
@@ -2396,7 +2585,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
         final SimpleFactory<String> factory = new SimpleFactory<>();
         factory.setValid(false); // Validate will always fail
         factory.setValidationEnabled(true);
-        try (final GenericKeyedObjectPool<String, String> pool = new GenericKeyedObjectPool<>(factory)) {
+        try (final GenericKeyedObjectPool<String, String, Exception> pool = new GenericKeyedObjectPool<>(factory)) {
             pool.setMaxTotalPerKey(2);
             pool.setMaxWaitMillis(1500);
             pool.setTestOnReturn(true);
@@ -2448,7 +2637,7 @@ public class TestGenericKeyedObjectPool extends TestKeyedObjectPool {
      * @throws Exception May occur in some failure modes
      */
     @Test
-    @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
+    @Timeout(value = 60_000, unit = TimeUnit.MILLISECONDS)
     public void testWhenExhaustedBlockClosePool() throws Exception {
         gkoPool.setMaxTotalPerKey(1);
         gkoPool.setBlockWhenExhausted(true);
diff --git a/src/test/java/org/apache/commons/pool2/impl/TestGenericObjectPool.java b/src/test/java/org/apache/commons/pool2/impl/TestGenericObjectPool.java
index 8e38303..47081ce 100644
--- a/src/test/java/org/apache/commons/pool2/impl/TestGenericObjectPool.java
+++ b/src/test/java/org/apache/commons/pool2/impl/TestGenericObjectPool.java
@@ -31,7 +31,6 @@ import static org.junit.jupiter.api.Assertions.fail;
 
 import java.lang.management.ManagementFactory;
 import java.lang.ref.WeakReference;
-import java.lang.reflect.Field;
 import java.nio.charset.UnsupportedCharsetException;
 import java.time.Duration;
 import java.time.Instant;
@@ -93,12 +92,12 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
         }
     }
 
-    private static class CreateErrorFactory extends BasePooledObjectFactory<String> {
+    private static class CreateErrorFactory extends BasePooledObjectFactory<String, InterruptedException> {
 
         private final Semaphore semaphore = new Semaphore(0);
 
         @Override
-        public String create() throws Exception {
+        public String create() throws InterruptedException {
             semaphore.acquire();
             throw new UnknownError("wiggle");
         }
@@ -117,12 +116,12 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
         }
     }
 
-    private static class CreateFailFactory extends BasePooledObjectFactory<String> {
+    private static class CreateFailFactory extends BasePooledObjectFactory<String, InterruptedException> {
 
         private final Semaphore semaphore = new Semaphore(0);
 
         @Override
-        public String create() throws Exception {
+        public String create() throws InterruptedException {
             semaphore.acquire();
             throw new UnsupportedCharsetException("wibble");
         }
@@ -142,9 +141,9 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
     }
 
     private static final class DummyFactory
-            extends BasePooledObjectFactory<Object> {
+            extends BasePooledObjectFactory<Object, RuntimeException> {
         @Override
-        public Object create() throws Exception {
+        public Object create() {
             return null;
         }
         @Override
@@ -153,11 +152,11 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
         }
     }
 
-    private static class EvictionThread<T> extends Thread {
+    private static class EvictionThread<T, E extends Exception> extends Thread {
 
-        private final GenericObjectPool<T> pool;
+        private final GenericObjectPool<T, E> pool;
 
-        public EvictionThread(final GenericObjectPool<T> pool) {
+        public EvictionThread(final GenericObjectPool<T, E> pool) {
             this.pool = pool;
         }
 
@@ -177,9 +176,9 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
      *  1) Instances are mutable and mutation can cause change in identity / hashcode.
      */
     private static final class HashSetFactory
-            extends BasePooledObjectFactory<HashSet<String>> {
+            extends BasePooledObjectFactory<HashSet<String>, RuntimeException> {
         @Override
-        public HashSet<String> create() throws Exception {
+        public HashSet<String> create() {
             return new HashSet<>();
         }
         @Override
@@ -193,9 +192,9 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
      */
     static class InvalidateThread implements Runnable {
         private final String obj;
-        private final ObjectPool<String> pool;
+        private final ObjectPool<String, Exception> pool;
         private boolean done;
-        public InvalidateThread(final ObjectPool<String> pool, final String obj) {
+        public InvalidateThread(final ObjectPool<String, Exception> pool, final String obj) {
             this.obj = obj;
             this.pool = pool;
         }
@@ -217,10 +216,10 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
     }
 
     private static class InvalidFactory
-            extends BasePooledObjectFactory<Object> {
+            extends BasePooledObjectFactory<Object, RuntimeException> {
 
         @Override
-        public Object create() throws Exception {
+        public Object create() {
             return new Object();
         }
         @Override
@@ -235,7 +234,7 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
         }
     }
 
-    public static class SimpleFactory implements PooledObjectFactory<String> {
+    public static class SimpleFactory implements PooledObjectFactory<String, Exception> {
         int makeCounter;
 
         int activationCounter;
@@ -273,10 +272,12 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
         public SimpleFactory(final boolean valid) {
             this(valid,valid);
         }
+        
         public SimpleFactory(final boolean evalid, final boolean ovalid) {
             evenValid = evalid;
             oddValid = ovalid;
         }
+        
         @Override
         public void activateObject(final PooledObject<String> obj) throws Exception {
             final boolean hurl;
@@ -293,6 +294,7 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
                 throw new Exception();
             }
         }
+        
         @Override
         public void destroyObject(final PooledObject<String> obj) throws Exception {
             final long waitLatency;
@@ -311,18 +313,23 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
                 throw new Exception();
             }
         }
+        
         private void doWait(final long latency) {
             Waiter.sleepQuietly(latency);
         }
+        
         public synchronized int getMakeCounter() {
             return makeCounter;
         }
+        
         public synchronized boolean isThrowExceptionOnActivate() {
             return exceptionOnActivate;
         }
+        
         public synchronized boolean isValidationEnabled() {
             return enableValidation;
         }
+        
         @Override
         public PooledObject<String> makeObject() {
             final long waitLatency;
@@ -343,6 +350,7 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
             }
             return new DefaultPooledObject<>(String.valueOf(counter));
         }
+        
         @Override
         public void passivateObject(final PooledObject<String> obj) throws Exception {
             final boolean hurl;
@@ -353,18 +361,23 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
                 throw new Exception();
             }
         }
+        
         public synchronized void setDestroyLatency(final long destroyLatency) {
             this.destroyLatency = destroyLatency;
         }
+        
         public synchronized void setEvenValid(final boolean valid) {
             evenValid = valid;
         }
+        
         public synchronized void setMakeLatency(final long makeLatency) {
             this.makeLatency = makeLatency;
         }
+        
         public synchronized void setMaxTotal(final int maxTotal) {
             this.maxTotal = maxTotal;
         }
+        
         public synchronized void setOddValid(final boolean valid) {
             oddValid = valid;
         }
@@ -438,13 +451,13 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
         }
     }
 
-    static class TestThread<T> implements Runnable {
+    static class TestThread<T, E extends Exception> implements Runnable {
 
         /** source of random delay times */
         private final java.util.Random random;
 
         /** pool to borrow from */
-        private final ObjectPool<T> pool;
+        private final ObjectPool<T, E> pool;
 
         /** number of borrow attempts */
         private final int iter;
@@ -465,29 +478,29 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
         private volatile boolean failed;
         private volatile Throwable error;
 
-        public TestThread(final ObjectPool<T> pool) {
+        public TestThread(final ObjectPool<T, E> pool) {
             this(pool, 100, 50, true, null);
         }
 
-        public TestThread(final ObjectPool<T> pool, final int iter) {
+        public TestThread(final ObjectPool<T, E> pool, final int iter) {
             this(pool, iter, 50, true, null);
         }
 
-        public TestThread(final ObjectPool<T> pool, final int iter, final int delay) {
+        public TestThread(final ObjectPool<T, E> pool, final int iter, final int delay) {
             this(pool, iter, delay, true, null);
         }
 
-        public TestThread(final ObjectPool<T> pool, final int iter, final int delay,
+        public TestThread(final ObjectPool<T, E> pool, final int iter, final int delay,
                 final boolean randomDelay) {
             this(pool, iter, delay, randomDelay, null);
         }
 
-        public TestThread(final ObjectPool<T> pool, final int iter, final int delay,
+        public TestThread(final ObjectPool<T, E> pool, final int iter, final int delay,
                 final boolean randomDelay, final Object obj) {
             this(pool, iter, delay, delay, randomDelay, obj);
         }
 
-        public TestThread(final ObjectPool<T> pool, final int iter, final int startDelay,
+        public TestThread(final ObjectPool<T, E> pool, final int iter, final int startDelay,
             final int holdTime, final boolean randomDelay, final Object obj) {
        this.pool = pool;
        this.iter = iter;
@@ -547,8 +560,8 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
      * Very simple test thread that just tries to borrow an object from
      * the provided pool returns it after a wait
      */
-    static class WaitingTestThread extends Thread {
-        private final GenericObjectPool<String> pool;
+    static class WaitingTestThread<E extends Exception> extends Thread {
+        private final GenericObjectPool<String, E> pool;
         private final long pause;
         private Throwable thrown;
 
@@ -558,7 +571,7 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
         private long endedMillis;
         private String objectId;
 
-        public WaitingTestThread(final GenericObjectPool<String> pool, final long pause) {
+        public WaitingTestThread(final GenericObjectPool<String, E> pool, final long pause) {
             this.pool = pool;
             this.pause = pause;
             this.thrown = null;
@@ -588,12 +601,12 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
     // mvn test -DargLine="-DTestGenericObjectPool.display.thread.details=true"
     // @see https://issues.apache.org/jira/browse/SUREFIRE-121
 
-    protected GenericObjectPool<String> genericObjectPool;
+    protected GenericObjectPool<String, Exception> genericObjectPool;
 
     private SimpleFactory simpleFactory;
 
     @SuppressWarnings("deprecation")
-    private void assertConfiguration(final GenericObjectPoolConfig<?> expected, final GenericObjectPool<?> actual) {
+    private void assertConfiguration(final GenericObjectPoolConfig<?> expected, final GenericObjectPool<?, ?> actual) {
         assertEquals(Boolean.valueOf(expected.getTestOnCreate()), Boolean.valueOf(actual.getTestOnCreate()),
                 "testOnCreate");
         assertEquals(Boolean.valueOf(expected.getTestOnBorrow()), Boolean.valueOf(actual.getTestOnBorrow()),
@@ -673,8 +686,8 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
         // Order, oldest to youngest, is "0", "1", ...,"4"
         genericObjectPool.evict(); // Should evict "0" and "1"
         final Object obj = genericObjectPool.borrowObject();
-        assertFalse(obj.equals("0"), "oldest not evicted");
-        assertFalse(obj.equals("1"), "second oldest not evicted");
+        assertNotEquals("0", obj, "oldest not evicted");
+        assertNotEquals("1", obj, "second oldest not evicted");
         // 2 should be next out for FIFO, 4 for LIFO
         assertEquals(lifo ? "4" : "2" , obj,"Wrong instance returned");
     }
@@ -697,7 +710,7 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
     private void checkEvictorVisiting(final boolean lifo) throws Exception {
         VisitTracker<Object> obj;
         VisitTrackerFactory<Object> trackerFactory = new VisitTrackerFactory<>();
-        try (GenericObjectPool<VisitTracker<Object>> trackerPool = new GenericObjectPool<>(trackerFactory)) {
+        try (GenericObjectPool<VisitTracker<Object>,RuntimeException> trackerPool = new GenericObjectPool<>(trackerFactory)) {
             trackerPool.setNumTestsPerEvictionRun(2);
             trackerPool.setMinEvictableIdleTime(Duration.ofMillis(-1));
             trackerPool.setTestWhileIdle(true);
@@ -728,7 +741,7 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
         }
 
         trackerFactory = new VisitTrackerFactory<>();
-        try (GenericObjectPool<VisitTracker<Object>> trackerPool = new GenericObjectPool<>(trackerFactory)) {
+        try (GenericObjectPool<VisitTracker<Object>, RuntimeException> trackerPool = new GenericObjectPool<>(trackerFactory)) {
             trackerPool.setNumTestsPerEvictionRun(3);
             trackerPool.setMinEvictableIdleTime(Duration.ofMillis(-1));
             trackerPool.setTestWhileIdle(true);
@@ -771,7 +784,7 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
         random.setSeed(System.currentTimeMillis());
         for (int i = 0; i < 4; i++) {
             for (int j = 0; j < 5; j++) {
-                try (GenericObjectPool<VisitTracker<Object>> trackerPool = new GenericObjectPool<>(trackerFactory)) {
+                try (GenericObjectPool<VisitTracker<Object>, RuntimeException> trackerPool = new GenericObjectPool<>(trackerFactory)) {
                     trackerPool.setNumTestsPerEvictionRun(smallPrimes[i]);
                     trackerPool.setMinEvictableIdleTime(Duration.ofMillis(-1));
                     trackerPool.setTestWhileIdle(true);
@@ -809,8 +822,8 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
         }
     }
 
-    private BasePooledObjectFactory<String> createDefaultPooledObjectFactory() {
-        return new BasePooledObjectFactory<String>() {
+    private BasePooledObjectFactory<String, RuntimeException> createDefaultPooledObjectFactory() {
+        return new BasePooledObjectFactory<String, RuntimeException>() {
             @Override
             public String create() {
                 // fake
@@ -825,8 +838,8 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
         };
     }
 
-    private BasePooledObjectFactory<String> createNullPooledObjectFactory() {
-        return new BasePooledObjectFactory<String>() {
+    private BasePooledObjectFactory<String, RuntimeException> createNullPooledObjectFactory() {
+        return new BasePooledObjectFactory<String, RuntimeException>() {
             @Override
             public String create() {
                 // fake
@@ -841,10 +854,10 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
         };
     }
 
-    private BasePooledObjectFactory<String> createSlowObjectFactory(final long elapsedTimeMillis) {
-        return new BasePooledObjectFactory<String>() {
+    private BasePooledObjectFactory<String, InterruptedException> createSlowObjectFactory(final long elapsedTimeMillis) {
+        return new BasePooledObjectFactory<String, InterruptedException>() {
             @Override
-            public String create() throws Exception {
+            public String create() throws InterruptedException {
                 Thread.sleep(elapsedTimeMillis);
                 return "created";
             }
@@ -873,17 +886,15 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
     }
 
     @Override
-    protected ObjectPool<String> makeEmptyPool(final int minCap) {
-       final GenericObjectPool<String> mtPool =
-               new GenericObjectPool<>(new SimpleFactory());
-       mtPool.setMaxTotal(minCap);
+    protected ObjectPool<String, Exception> makeEmptyPool(final int minCap) {
+        final GenericObjectPool<String, Exception> mtPool = new GenericObjectPool<>(new SimpleFactory());
+        mtPool.setMaxTotal(minCap);
        mtPool.setMaxIdle(minCap);
        return mtPool;
     }
 
     @Override
-    protected ObjectPool<Object> makeEmptyPool(
-            final PooledObjectFactory<Object> fac) {
+    protected <E extends Exception> ObjectPool<Object, E> makeEmptyPool(final PooledObjectFactory<Object, E> fac) {
         return new GenericObjectPool<>(fac);
     }
 
@@ -892,13 +903,10 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
      * <iterations> borrow-return cycles with random delay times <= delay
      * in between.
      */
-    @SuppressWarnings({
-        "rawtypes", "unchecked"
-    })
-    private void runTestThreads(final int numThreads, final int iterations, final int delay, final GenericObjectPool testPool) {
-        final TestThread[] threads = new TestThread[numThreads];
+    private <T, E extends Exception> void runTestThreads(final int numThreads, final int iterations, final int delay, final GenericObjectPool<T, E> testPool) {
+        final TestThread<T, E>[] threads = new TestThread[numThreads];
         for (int i = 0; i < numThreads; i++) {
-            threads[i] = new TestThread<String>(testPool, iterations, delay);
+            threads[i] = new TestThread<>(testPool, iterations, delay);
             final Thread t = new Thread(threads[i]);
             t.start();
         }
@@ -929,8 +937,7 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
         simpleFactory = null;
 
         final MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
-        final Set<ObjectName> result = mbs.queryNames(new ObjectName(
-                "org.apache.commoms.pool2:type=GenericObjectPool,*"), null);
+        final Set<ObjectName> result = mbs.queryNames(new ObjectName("org.apache.commoms.pool2:type=GenericObjectPool,*"), null);
         // There should be no registered pools at this point
         final int registeredPoolCount = result.size();
         final StringBuilder msg = new StringBuilder("Current pool is: ");
@@ -944,12 +951,14 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
             msg.append('\n');
             mbs.unregisterMBean(name);
         }
-        assertEquals( 0, registeredPoolCount,msg.toString());
+        assertEquals(0, registeredPoolCount, msg.toString());
 
-        // Make sure that EvictionTimer executor is shut down
-        final Field evictorExecutorField = EvictionTimer.class.getDeclaredField("executor");
-        evictorExecutorField.setAccessible(true);
-        assertNull(evictorExecutorField.get(null));
+        // Make sure that EvictionTimer executor is shut down.
+        Thread.yield();
+        if (EvictionTimer.getExecutor() != null) {
+            Thread.sleep(1000);
+        }
+        assertNull(EvictionTimer.getExecutor(), "EvictionTimer.getExecutor()");
     }
 
     /**
@@ -961,12 +970,12 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
     public void testAbandonedPool() throws Exception {
         final GenericObjectPoolConfig<String> config = new GenericObjectPoolConfig<>();
         config.setJmxEnabled(false);
-        GenericObjectPool<String> abandoned = new GenericObjectPool<>(simpleFactory, config);
+        GenericObjectPool<String, Exception> abandoned = new GenericObjectPool<>(simpleFactory, config);
         abandoned.setTimeBetweenEvictionRuns(Duration.ofMillis(100)); // Starts evictor
         assertEquals(abandoned.getRemoveAbandonedTimeout(), abandoned.getRemoveAbandonedTimeoutDuration().getSeconds());
 
         // This is ugly, but forces GC to hit the pool
-        final WeakReference<GenericObjectPool<String>> ref = new WeakReference<>(abandoned);
+        final WeakReference<GenericObjectPool<String, Exception>> ref = new WeakReference<>(abandoned);
         abandoned = null;
         while (ref.get() != null) {
             System.gc();
@@ -994,7 +1003,7 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
     public void testAppendStats() {
         assertFalse(genericObjectPool.getMessageStatistics());
         assertEquals("foo", (genericObjectPool.appendStats("foo")));
-        try (final GenericObjectPool<?> pool = new GenericObjectPool<>(new SimpleFactory())) {
+        try (final GenericObjectPool<?, Exception> pool = new GenericObjectPool<>(new SimpleFactory())) {
             pool.setMessagesStatistics(true);
             assertNotEquals("foo", (pool.appendStats("foo")));
             pool.setMessagesStatistics(false);
@@ -1002,65 +1011,6 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
         }
     }
 
-    @Test
-    public void testBorrowTimings() throws Exception {
-        // Borrow
-        final String object = genericObjectPool.borrowObject();
-        final PooledObject<String> po = genericObjectPool.getPooledObject(object);
-        // In the initial state, all instants are the creation instant: last borrow, last use, last return.
-        // In the initial state, the active duration is the time between "now" and the creation time.
-        // In the initial state, the idle duration is the time between "now" and the last return, which is the creation time.
-        // But... this PO might have already been used in other tests in this class.
-
-        final Instant lastBorrowInstant1 = po.getLastBorrowInstant();
-        final Instant lastReturnInstant1 = po.getLastReturnInstant();
-        final Instant lastUsedInstant1 = po.getLastUsedInstant();
-
-        assertThat(po.getCreateInstant(), lessThanOrEqualTo(lastBorrowInstant1));
-        assertThat(po.getCreateInstant(), lessThanOrEqualTo(lastReturnInstant1));
-        assertThat(po.getCreateInstant(), lessThanOrEqualTo(lastUsedInstant1));
-        assertThat(po.getCreateTime(), lessThanOrEqualTo(lastBorrowInstant1.toEpochMilli()));
-        assertThat(po.getCreateTime(), lessThanOrEqualTo(lastReturnInstant1.toEpochMilli()));
-        assertThat(po.getCreateTime(), lessThanOrEqualTo(lastUsedInstant1.toEpochMilli()));
-
-        // Sleep MUST be "long enough" to detect that more than 0 milliseconds have elapsed.
-        // Need an API in Java 8 to get the clock granularity.
-        Thread.sleep(200);
-
-        assertFalse(po.getActiveDuration().isNegative());
-        assertFalse(po.getActiveDuration().isZero());
-        // We use greaterThanOrEqualTo instead of equal because "now" many be different when each argument is evaluated.
-        assertThat(1L, lessThanOrEqualTo(2L)); // sanity check
-        assertThat(Duration.ZERO, lessThanOrEqualTo(Duration.ZERO.plusNanos(1))); // sanity check
-        assertThat(po.getActiveDuration(), lessThanOrEqualTo(po.getIdleDuration()));
-        // Deprecated
-        assertThat(po.getActiveDuration().toMillis(), lessThanOrEqualTo(po.getActiveTimeMillis()));
-        assertThat(po.getActiveDuration(), lessThanOrEqualTo(po.getActiveTime()));
-        //
-        // TODO How to compare ID with AD since other tests may have touched the PO?
-        assertThat(po.getActiveDuration(), lessThanOrEqualTo(po.getIdleTime()));
-        assertThat(po.getActiveDuration().toMillis(), lessThanOrEqualTo(po.getIdleTimeMillis()));
-        //
-        assertThat(po.getCreateInstant(), lessThanOrEqualTo(po.getLastBorrowInstant()));
-        assertThat(po.getCreateInstant(), lessThanOrEqualTo(po.getLastReturnInstant()));
-        assertThat(po.getCreateInstant(), lessThanOrEqualTo(po.getLastUsedInstant()));
-
-        assertThat(lastBorrowInstant1, lessThanOrEqualTo(po.getLastBorrowInstant()));
-        assertThat(lastReturnInstant1, lessThanOrEqualTo(po.getLastReturnInstant()));
-        assertThat(lastUsedInstant1, lessThanOrEqualTo(po.getLastUsedInstant()));
-
-        genericObjectPool.returnObject(object);
-
-        assertFalse(po.getActiveDuration().isNegative());
-        assertFalse(po.getActiveDuration().isZero());
-        assertThat(po.getActiveDuration().toMillis(), lessThanOrEqualTo(po.getActiveTimeMillis()));
-        assertThat(po.getActiveDuration(), lessThanOrEqualTo(po.getActiveTime()));
-
-        assertThat(lastBorrowInstant1, lessThanOrEqualTo(po.getLastBorrowInstant()));
-        assertThat(lastReturnInstant1, lessThanOrEqualTo(po.getLastReturnInstant()));
-        assertThat(lastUsedInstant1, lessThanOrEqualTo(po.getLastUsedInstant()));
-    }
-
     /*
      * Note: This test relies on timing for correct execution. There *should* be
      * enough margin for this to work correctly on most (all?) systems but be
@@ -1120,6 +1070,65 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
         }
     }
 
+    @Test
+    public void testBorrowTimings() throws Exception {
+        // Borrow
+        final String object = genericObjectPool.borrowObject();
+        final PooledObject<String> po = genericObjectPool.getPooledObject(object);
+        // In the initial state, all instants are the creation instant: last borrow, last use, last return.
+        // In the initial state, the active duration is the time between "now" and the creation time.
+        // In the initial state, the idle duration is the time between "now" and the last return, which is the creation time.
+        // But... this PO might have already been used in other tests in this class.
+
+        final Instant lastBorrowInstant1 = po.getLastBorrowInstant();
+        final Instant lastReturnInstant1 = po.getLastReturnInstant();
+        final Instant lastUsedInstant1 = po.getLastUsedInstant();
+
+        assertThat(po.getCreateInstant(), lessThanOrEqualTo(lastBorrowInstant1));
+        assertThat(po.getCreateInstant(), lessThanOrEqualTo(lastReturnInstant1));
+        assertThat(po.getCreateInstant(), lessThanOrEqualTo(lastUsedInstant1));
+        assertThat(po.getCreateTime(), lessThanOrEqualTo(lastBorrowInstant1.toEpochMilli()));
+        assertThat(po.getCreateTime(), lessThanOrEqualTo(lastReturnInstant1.toEpochMilli()));
+        assertThat(po.getCreateTime(), lessThanOrEqualTo(lastUsedInstant1.toEpochMilli()));
+
+        // Sleep MUST be "long enough" to detect that more than 0 milliseconds have elapsed.
+        // Need an API in Java 8 to get the clock granularity.
+        Thread.sleep(200);
+
+        assertFalse(po.getActiveDuration().isNegative());
+        assertFalse(po.getActiveDuration().isZero());
+        // We use greaterThanOrEqualTo instead of equal because "now" many be different when each argument is evaluated.
+        assertThat(1L, lessThanOrEqualTo(2L)); // sanity check
+        assertThat(Duration.ZERO, lessThanOrEqualTo(Duration.ZERO.plusNanos(1))); // sanity check
+        assertThat(po.getActiveDuration(), lessThanOrEqualTo(po.getIdleDuration()));
+        // Deprecated
+        assertThat(po.getActiveDuration().toMillis(), lessThanOrEqualTo(po.getActiveTimeMillis()));
+        assertThat(po.getActiveDuration(), lessThanOrEqualTo(po.getActiveTime()));
+        //
+        // TODO How to compare ID with AD since other tests may have touched the PO?
+        assertThat(po.getActiveDuration(), lessThanOrEqualTo(po.getIdleTime()));
+        assertThat(po.getActiveDuration().toMillis(), lessThanOrEqualTo(po.getIdleTimeMillis()));
+        //
+        assertThat(po.getCreateInstant(), lessThanOrEqualTo(po.getLastBorrowInstant()));
+        assertThat(po.getCreateInstant(), lessThanOrEqualTo(po.getLastReturnInstant()));
+        assertThat(po.getCreateInstant(), lessThanOrEqualTo(po.getLastUsedInstant()));
+
+        assertThat(lastBorrowInstant1, lessThanOrEqualTo(po.getLastBorrowInstant()));
+        assertThat(lastReturnInstant1, lessThanOrEqualTo(po.getLastReturnInstant()));
+        assertThat(lastUsedInstant1, lessThanOrEqualTo(po.getLastUsedInstant()));
+
+        genericObjectPool.returnObject(object);
+
+        assertFalse(po.getActiveDuration().isNegative());
+        assertFalse(po.getActiveDuration().isZero());
+        assertThat(po.getActiveDuration().toMillis(), lessThanOrEqualTo(po.getActiveTimeMillis()));
+        assertThat(po.getActiveDuration(), lessThanOrEqualTo(po.getActiveTime()));
+
+        assertThat(lastBorrowInstant1, lessThanOrEqualTo(po.getLastBorrowInstant()));
+        assertThat(lastReturnInstant1, lessThanOrEqualTo(po.getLastReturnInstant()));
+        assertThat(lastUsedInstant1, lessThanOrEqualTo(po.getLastUsedInstant()));
+    }
+
     /**
      * On first borrow, first object fails validation, second object is OK.
      * Subsequent borrows are OK. This was POOL-152.
@@ -1165,7 +1174,7 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
         final SimpleFactory factory = new SimpleFactory();
         // Give makeObject a little latency
         factory.setMakeLatency(200);
-        try (final GenericObjectPool<String> pool = new GenericObjectPool<>(factory, new GenericObjectPoolConfig<>())) {
+        try (final GenericObjectPool<String, Exception> pool = new GenericObjectPool<>(factory, new GenericObjectPoolConfig<>())) {
             final String s = pool.borrowObject();
             // First borrow waits on create, so wait time should be at least 200 ms
             // Allow 100ms error in clock times
@@ -1183,7 +1192,7 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
     @Test
     @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
     public void testCloseMultiplePools1() {
-        try (final GenericObjectPool<String> genericObjectPool2 = new GenericObjectPool<>(simpleFactory)) {
+        try (final GenericObjectPool<String, Exception> genericObjectPool2 = new GenericObjectPool<>(simpleFactory)) {
             genericObjectPool.setTimeBetweenEvictionRuns(TestConstants.ONE_MILLISECOND_DURATION);
             genericObjectPool2.setTimeBetweenEvictionRuns(TestConstants.ONE_MILLISECOND_DURATION);
         }
@@ -1193,7 +1202,7 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
     @Test
     @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
     public void testCloseMultiplePools2() throws Exception {
-        try (final GenericObjectPool<String> genericObjectPool2 = new GenericObjectPool<>(simpleFactory)) {
+        try (final GenericObjectPool<String, Exception> genericObjectPool2 = new GenericObjectPool<>(simpleFactory)) {
             // Ensure eviction takes a long time, during which time EvictionTimer.executor's queue is empty
             simpleFactory.setDestroyLatency(1000L);
             // Ensure there is an object to evict, so that above latency takes effect
@@ -1316,8 +1325,8 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
         final long timeBetweenEvictionRunsMillis = 8;
         final boolean blockWhenExhausted = false;
         final boolean lifo = false;
-        final PooledObjectFactory<Object> dummyFactory = new DummyFactory();
-        try (GenericObjectPool<Object> dummyPool = new GenericObjectPool<>(dummyFactory)) {
+        final PooledObjectFactory<Object, RuntimeException> dummyFactory = new DummyFactory();
+        try (GenericObjectPool<Object, RuntimeException> dummyPool = new GenericObjectPool<>(dummyFactory)) {
             assertEquals(GenericObjectPoolConfig.DEFAULT_MAX_IDLE, dummyPool.getMaxIdle());
             assertEquals(BaseObjectPoolConfig.DEFAULT_MAX_WAIT_MILLIS, dummyPool.getMaxWaitMillis());
             assertEquals(GenericObjectPoolConfig.DEFAULT_MIN_IDLE, dummyPool.getMinIdle());
@@ -1362,7 +1371,7 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
         config.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
         assertEquals(timeBetweenEvictionRunsMillis, config.getTimeBetweenEvictionRuns().toMillis());
         config.setBlockWhenExhausted(blockWhenExhausted);
-        try (GenericObjectPool<Object> dummyPool = new GenericObjectPool<>(dummyFactory, config)) {
+        try (GenericObjectPool<Object, RuntimeException> dummyPool = new GenericObjectPool<>(dummyFactory, config)) {
             assertEquals(maxIdle, dummyPool.getMaxIdle());
             assertEquals(maxWaitDuration, dummyPool.getMaxWaitDuration());
             assertEquals(maxWaitMillis, dummyPool.getMaxWaitMillis());
@@ -1394,7 +1403,7 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
     @Test
     public void testEqualsIndiscernible() throws Exception {
         final HashSetFactory factory = new HashSetFactory();
-        try (final GenericObjectPool<HashSet<String>> pool = new GenericObjectPool<>(factory,
+        try (final GenericObjectPool<HashSet<String>, RuntimeException> pool = new GenericObjectPool<>(factory,
                 new GenericObjectPoolConfig<>())) {
             final HashSet<String> s1 = pool.borrowObject();
             final HashSet<String> s2 = pool.borrowObject();
@@ -1407,7 +1416,7 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
     public void testErrorFactoryDoesNotBlockThreads() throws Exception {
 
         final CreateErrorFactory factory = new CreateErrorFactory();
-        try (final GenericObjectPool<String> createFailFactoryPool = new GenericObjectPool<>(factory)) {
+        try (final GenericObjectPool<String, InterruptedException> createFailFactoryPool = new GenericObjectPool<>(factory)) {
 
             createFailFactoryPool.setMaxTotal(1);
 
@@ -1468,7 +1477,7 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
         genericObjectPool.borrowObject(); // numActive = 1, numIdle = 0
         // Create a test thread that will run once and try a borrow after
         // 150ms fixed delay
-        final TestThread<String> borrower = new TestThread<>(genericObjectPool, 1, 150, false);
+        final TestThread<String, Exception> borrower = new TestThread<>(genericObjectPool, 1, 150, false);
         final Thread borrowerThread = new Thread(borrower);
         // Set evictor to run in 100 ms - will create idle instance
         genericObjectPool.setTimeBetweenEvictionRuns(Duration.ofMillis(100));
@@ -1539,7 +1548,7 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
     @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
     public void testEvictionInvalid() throws Exception {
 
-        try (final GenericObjectPool<Object> invalidFactoryPool = new GenericObjectPool<>(new InvalidFactory())) {
+        try (final GenericObjectPool<Object, RuntimeException> invalidFactoryPool = new GenericObjectPool<>(new InvalidFactory())) {
 
             invalidFactoryPool.setMaxIdle(1);
             invalidFactoryPool.setMaxTotal(1);
@@ -1644,7 +1653,7 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
     @Test
     @Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)
     public void testEvictionSoftMinIdle() throws Exception {
-        class TimeTest extends BasePooledObjectFactory<TimeTest> {
+        class TimeTest extends BasePooledObjectFactory<TimeTest, RuntimeException> {
             private final long createTimeMillis;
 
             public TimeTest() {
@@ -1652,7 +1661,7 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
             }
 
             @Override
-            public TimeTest create() throws Exception {
+            public TimeTest create() {
                 return new TimeTest();
             }
 
@@ -1666,7 +1675,7 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
             }
         }
 
-        try (final GenericObjectPool<TimeTest> timePool = new GenericObjectPool<>(new TimeTest())) {
+        try (final GenericObjectPool<TimeTest, RuntimeException> timePool = new GenericObjectPool<>(new TimeTest())) {
 
             timePool.setMaxIdle(5);
             timePool.setMaxTotal(5);
@@ -1832,7 +1841,7 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
     public void testFailingFactoryDoesNotBlockThreads() throws Exception {
 
         final CreateFailFactory factory = new CreateFailFactory();
-        try (final GenericObjectPool<String> createFailFactoryPool = new GenericObjectPool<>(factory)) {
+        try (final GenericObjectPool<String, InterruptedException> createFailFactoryPool = new GenericObjectPool<>(factory)) {
 
             createFailFactoryPool.setMaxTotal(1);
 
@@ -1896,21 +1905,21 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
 
     @Test
     public void testGetFactoryType_DefaultPooledObjectFactory() {
-        try (final GenericObjectPool<String> pool = new GenericObjectPool<>(createDefaultPooledObjectFactory())) {
+        try (final GenericObjectPool<String, RuntimeException> pool = new GenericObjectPool<>(createDefaultPooledObjectFactory())) {
             assertNotNull((pool.getFactoryType()));
         }
     }
 
     @Test
     public void testGetFactoryType_NullPooledObjectFactory() {
-        try (final GenericObjectPool<String> pool = new GenericObjectPool<>(createNullPooledObjectFactory())) {
+        try (final GenericObjectPool<String, RuntimeException> pool = new GenericObjectPool<>(createNullPooledObjectFactory())) {
             assertNotNull((pool.getFactoryType()));
         }
     }
 
     @Test
     public void testGetFactoryType_PoolUtilsSynchronizedDefaultPooledFactory() {
-        try (final GenericObjectPool<String> pool = new GenericObjectPool<>(
+        try (final GenericObjectPool<String, RuntimeException> pool = new GenericObjectPool<>(
                 PoolUtils.synchronizedPooledFactory(createDefaultPooledObjectFactory()))) {
             assertNotNull((pool.getFactoryType()));
         }
@@ -1918,7 +1927,7 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
 
     @Test
     public void testGetFactoryType_PoolUtilsSynchronizedNullPooledFactory() {
-        try (final GenericObjectPool<String> pool = new GenericObjectPool<>(
+        try (final GenericObjectPool<String, RuntimeException> pool = new GenericObjectPool<>(
                 PoolUtils.synchronizedPooledFactory(createNullPooledObjectFactory()))) {
             assertNotNull((pool.getFactoryType()));
         }
@@ -1926,7 +1935,7 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
 
     @Test
     public void testGetFactoryType_SynchronizedDefaultPooledObjectFactory() {
-        try (final GenericObjectPool<String> pool = new GenericObjectPool<>(
+        try (final GenericObjectPool<String, RuntimeException> pool = new GenericObjectPool<>(
                 new TestSynchronizedPooledObjectFactory<>(createDefaultPooledObjectFactory()))) {
             assertNotNull((pool.getFactoryType()));
         }
@@ -1934,7 +1943,7 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
 
     @Test
     public void testGetFactoryType_SynchronizedNullPooledObjectFactory() {
-        try (final GenericObjectPool<String> pool = new GenericObjectPool<>(
+        try (final GenericObjectPool<String, RuntimeException> pool = new GenericObjectPool<>(
                 new TestSynchronizedPooledObjectFactory<>(createNullPooledObjectFactory()))) {
             assertNotNull((pool.getFactoryType()));
         }
@@ -1942,7 +1951,7 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
 
     @Test
     public void testGetStatsString() {
-        try (final GenericObjectPool<String> pool = new GenericObjectPool<>(
+        try (final GenericObjectPool<String, RuntimeException> pool = new GenericObjectPool<>(
                 new TestSynchronizedPooledObjectFactory<>(createNullPooledObjectFactory()))) {
             assertNotNull(pool.getStatsString());
         }
@@ -1959,7 +1968,7 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
     @Test
     public void testInvalidateFreesCapacity() throws Exception {
         final SimpleFactory factory = new SimpleFactory();
-        try (final GenericObjectPool<String> pool = new GenericObjectPool<>(factory)) {
+        try (final GenericObjectPool<String, Exception> pool = new GenericObjectPool<>(factory)) {
             pool.setMaxTotal(2);
             pool.setMaxWaitMillis(500);
             // Borrow an instance and hold if for 5 seconds
@@ -1994,14 +2003,14 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
 
         final GenericObjectPoolConfig<String> config = new GenericObjectPoolConfig<>();
         config.setJmxEnabled(false);
-        try (final GenericObjectPool<String> poolWithoutJmx = new GenericObjectPool<>(simpleFactory, config)) {
+        try (final GenericObjectPool<String, Exception> poolWithoutJmx = new GenericObjectPool<>(simpleFactory, config)) {
             assertNull(poolWithoutJmx.getJmxName());
             config.setJmxEnabled(true);
             poolWithoutJmx.jmxUnregister();
         }
 
         config.setJmxNameBase(null);
-        try (final GenericObjectPool<String> poolWithDefaultJmxNameBase = new GenericObjectPool<>(simpleFactory, config)) {
+        try (final GenericObjectPool<String, Exception> poolWithDefaultJmxNameBase = new GenericObjectPool<>(simpleFactory, config)) {
             assertNotNull(poolWithDefaultJmxNameBase.getJmxName());
         }
     }
@@ -2407,7 +2416,7 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
     @Test
     public void testMultipleReturn() throws Exception {
         final WaiterFactory<String> factory = new WaiterFactory<>(0, 0, 0, 0, 0, 0);
-        try (final GenericObjectPool<Waiter> pool = new GenericObjectPool<>(factory)) {
+        try (final GenericObjectPool<Waiter, IllegalStateException> pool = new GenericObjectPool<>(factory)) {
             pool.setTestOnReturn(true);
             final Waiter waiter = pool.borrowObject();
             pool.returnObject(waiter);
@@ -2427,7 +2436,7 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
     // POOL-248
     @Test
     public void testMultipleReturnOfSameObject() throws Exception {
-        try (final GenericObjectPool<String> pool = new GenericObjectPool<>(simpleFactory, new GenericObjectPoolConfig<>())) {
+        try (final GenericObjectPool<String, Exception> pool = new GenericObjectPool<>(simpleFactory, new GenericObjectPoolConfig<>())) {
 
             assertEquals(0, pool.getNumActive());
             assertEquals(0, pool.getNumIdle());
@@ -2459,7 +2468,7 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
     @Test
     public void testMutable() throws Exception {
         final HashSetFactory factory = new HashSetFactory();
-        try (final GenericObjectPool<HashSet<String>> pool = new GenericObjectPool<>(factory,
+        try (final GenericObjectPool<HashSet<String>, RuntimeException> pool = new GenericObjectPool<>(factory,
                 new GenericObjectPoolConfig<>())) {
             final HashSet<String> s1 = pool.borrowObject();
             final HashSet<String> s2 = pool.borrowObject();
@@ -2490,7 +2499,7 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
         final int delay = 1;
         final int iterations = 1000;
         final AtomicIntegerFactory factory = new AtomicIntegerFactory();
-        try (final GenericObjectPool<AtomicInteger> pool = new GenericObjectPool<>(factory)) {
+        try (final GenericObjectPool<AtomicInteger, RuntimeException> pool = new GenericObjectPool<>(factory)) {
             pool.setMaxTotal(maxTotal);
             pool.setMaxIdle(maxTotal);
             pool.setTestOnBorrow(true);
@@ -2542,7 +2551,7 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
     public void testReturnBorrowObjectWithingMaxWaitMillis() throws Exception {
         final long maxWaitMillis = 500;
 
-        try (final GenericObjectPool<String> createSlowObjectFactoryPool = new GenericObjectPool<>(
+        try (final GenericObjectPool<String, InterruptedException> createSlowObjectFactoryPool = new GenericObjectPool<>(
                 createSlowObjectFactory(60000))) {
             createSlowObjectFactoryPool.setMaxTotal(1);
             createSlowObjectFactoryPool.setMaxWaitMillis(maxWaitMillis);
@@ -2620,7 +2629,7 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
         {
             // The object receives an Exception during its creation to prevent
             // memory leaks. See BaseGenericObjectPool constructor for more details.
-            assertEquals(false, "".equals(genericObjectPool.getCreationStackTrace()));
+            assertNotEquals("", genericObjectPool.getCreationStackTrace());
         }
         {
             assertEquals(0, genericObjectPool.getBorrowedCount());
@@ -2850,7 +2859,7 @@ public class TestGenericObjectPool extends TestBaseObjectPool {
         final SimpleFactory factory = new SimpleFactory();
         factory.setValid(false); // Validate will always fail
         factory.setValidationEnabled(true);
-        try (final GenericObjectPool<String> pool = new GenericObjectPool<>(factory)) {
+        try (final GenericObjectPool<String, Exception> pool = new GenericObjectPool<>(factory)) {
             pool.setMaxTotal(2);
             pool.setMaxWaitMillis(1500);
             pool.setTestOnReturn(true);
diff --git a/src/test/java/org/apache/commons/pool2/impl/TestGenericObjectPoolClassLoaders.java b/src/test/java/org/apache/commons/pool2/impl/TestGenericObjectPoolClassLoaders.java
index 7191523..8298255 100644
--- a/src/test/java/org/apache/commons/pool2/impl/TestGenericObjectPoolClassLoaders.java
+++ b/src/test/java/org/apache/commons/pool2/impl/TestGenericObjectPoolClassLoaders.java
@@ -47,7 +47,7 @@ public class TestGenericObjectPoolClassLoaders {
     }
 
     private static class CustomClassLoaderObjectFactory extends
-            BasePooledObjectFactory<URL> {
+            BasePooledObjectFactory<URL, IllegalStateException> {
         private final int n;
 
         CustomClassLoaderObjectFactory(final int n) {
@@ -55,7 +55,7 @@ public class TestGenericObjectPoolClassLoaders {
         }
 
         @Override
-        public URL create() throws Exception {
+        public URL create() {
             final URL url = Thread.currentThread().getContextClassLoader()
                     .getResource("test" + n);
             if (url == null) {
@@ -81,7 +81,7 @@ public class TestGenericObjectPoolClassLoaders {
         try (final CustomClassLoader cl1 = new CustomClassLoader(1)) {
             Thread.currentThread().setContextClassLoader(cl1);
             final CustomClassLoaderObjectFactory factory1 = new CustomClassLoaderObjectFactory(1);
-            try (final GenericObjectPool<URL> pool1 = new GenericObjectPool<>(factory1)) {
+            try (final GenericObjectPool<URL, IllegalStateException> pool1 = new GenericObjectPool<>(factory1)) {
                 pool1.setMinIdle(1);
                 pool1.setTimeBetweenEvictionRuns(Duration.ofMillis(100));
                 int counter = 0;
@@ -94,7 +94,7 @@ public class TestGenericObjectPoolClassLoaders {
                 try (final CustomClassLoader cl2 = new CustomClassLoader(2)) {
                     Thread.currentThread().setContextClassLoader(cl2);
                     final CustomClassLoaderObjectFactory factory2 = new CustomClassLoaderObjectFactory(2);
-                    try (final GenericObjectPool<URL> pool2 = new GenericObjectPool<>(factory2)) {
+                    try (final GenericObjectPool<URL, IllegalStateException> pool2 = new GenericObjectPool<>(factory2)) {
                         pool2.setMinIdle(1);
 
                         pool2.addObject();
diff --git a/src/test/java/org/apache/commons/pool2/impl/TestGenericObjectPoolFactoryCreateFailure.java b/src/test/java/org/apache/commons/pool2/impl/TestGenericObjectPoolFactoryCreateFailure.java
index e7a6fb1..96eaf95 100644
--- a/src/test/java/org/apache/commons/pool2/impl/TestGenericObjectPoolFactoryCreateFailure.java
+++ b/src/test/java/org/apache/commons/pool2/impl/TestGenericObjectPoolFactoryCreateFailure.java
@@ -35,9 +35,7 @@ import org.junit.jupiter.api.Timeout;
  */
 public class TestGenericObjectPoolFactoryCreateFailure {
 
-    private static final Duration NEG_ONE_DURATION = Duration.ofMillis(-1);
-
-    private static class SingleObjectFactory extends BasePooledObjectFactory<Object> {
+    private static class SingleObjectFactory extends BasePooledObjectFactory<Object, Exception> {
         private final AtomicBoolean created = new AtomicBoolean();
 
         @Override
@@ -62,8 +60,8 @@ public class TestGenericObjectPoolFactoryCreateFailure {
     private static class WinnerRunnable implements Runnable {
         private final CountDownLatch barrier;
         private final AtomicBoolean failed;
-        private final GenericObjectPool<Object> pool;
-        private WinnerRunnable(final GenericObjectPool<Object> pool, final CountDownLatch barrier, final AtomicBoolean failed) {
+        private final GenericObjectPool<Object, Exception> pool;
+        private WinnerRunnable(final GenericObjectPool<Object, Exception> pool, final CountDownLatch barrier, final AtomicBoolean failed) {
             this.pool = pool;
             this.failed = failed;
             this.barrier = barrier;
@@ -92,6 +90,8 @@ public class TestGenericObjectPoolFactoryCreateFailure {
         }
     }
 
+    private static final Duration NEG_ONE_DURATION = Duration.ofMillis(-1);
+
     private static void println(final String msg) {
         // System.out.println(msg);
     }
@@ -113,7 +113,7 @@ public class TestGenericObjectPoolFactoryCreateFailure {
         config.setSoftMinEvictableIdleTime(NEG_ONE_DURATION);
 
         config.setMaxWait(NEG_ONE_DURATION);
-        try (GenericObjectPool<Object> pool = new GenericObjectPool<>(factory, config)) {
+        try (GenericObjectPool<Object, Exception> pool = new GenericObjectPool<>(factory, config)) {
 
             final AtomicBoolean failed = new AtomicBoolean();
             final CountDownLatch barrier = new CountDownLatch(1);
@@ -122,7 +122,7 @@ public class TestGenericObjectPoolFactoryCreateFailure {
 
             // wait for object to be created
             while (!factory.created.get()) {
-                Waiter.sleepQuietly((long) 5);
+                Waiter.sleepQuietly(5);
             }
 
             // now borrow
diff --git a/src/test/java/org/apache/commons/pool2/impl/TestLinkedBlockingDeque.java b/src/test/java/org/apache/commons/pool2/impl/TestLinkedBlockingDeque.java
index 08615b9..c8ea68f 100644
--- a/src/test/java/org/apache/commons/pool2/impl/TestLinkedBlockingDeque.java
+++ b/src/test/java/org/apache/commons/pool2/impl/TestLinkedBlockingDeque.java
@@ -21,7 +21,6 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.junit.jupiter.api.Assertions.fail;
 
 import java.time.Duration;
 import java.util.ArrayList;
diff --git a/src/test/java/org/apache/commons/pool2/impl/TestPoolImplUtils.java b/src/test/java/org/apache/commons/pool2/impl/TestPoolImplUtils.java
index 98b80e2..03ea824 100644
--- a/src/test/java/org/apache/commons/pool2/impl/TestPoolImplUtils.java
+++ b/src/test/java/org/apache/commons/pool2/impl/TestPoolImplUtils.java
@@ -30,7 +30,7 @@ import org.junit.jupiter.api.Test;
 public class TestPoolImplUtils {
 
     @SuppressWarnings("unused")
-    private abstract static class FactoryAB<A, B> extends BasePooledObjectFactory<B> {
+    private abstract static class FactoryAB<A, B> extends BasePooledObjectFactory<B, RuntimeException> {
         // empty by design
     }
 
@@ -53,7 +53,7 @@ public class TestPoolImplUtils {
 
     private static class NotSimpleFactory extends FactoryF<Integer> {
         @Override
-        public Long create() throws Exception {
+        public Long create() {
             return null;
         }
 
@@ -63,9 +63,9 @@ public class TestPoolImplUtils {
         }
     }
 
-    private static class SimpleFactory extends BasePooledObjectFactory<String> {
+    private static class SimpleFactory extends BasePooledObjectFactory<String, RuntimeException> {
         @Override
-        public String create() throws Exception {
+        public String create() {
             return null;
         }
 
diff --git a/src/test/java/org/apache/commons/pool2/impl/TestSoftRefOutOfMemory.java b/src/test/java/org/apache/commons/pool2/impl/TestSoftRefOutOfMemory.java
index 3bd3a67..55044f3 100644
--- a/src/test/java/org/apache/commons/pool2/impl/TestSoftRefOutOfMemory.java
+++ b/src/test/java/org/apache/commons/pool2/impl/TestSoftRefOutOfMemory.java
@@ -34,7 +34,7 @@ import org.junit.jupiter.api.Test;
 /**
  */
 public class TestSoftRefOutOfMemory {
-    public static class LargePoolableObjectFactory extends BasePooledObjectFactory<String> {
+    public static class LargePoolableObjectFactory extends BasePooledObjectFactory<String, RuntimeException> {
         private final String buffer;
         private int counter;
 
@@ -56,7 +56,7 @@ public class TestSoftRefOutOfMemory {
         }
     }
 
-    private static class OomeFactory extends BasePooledObjectFactory<String> {
+    private static class OomeFactory extends BasePooledObjectFactory<String, RuntimeException> {
 
         private final OomeTrigger trigger;
 
@@ -65,7 +65,7 @@ public class TestSoftRefOutOfMemory {
         }
 
         @Override
-        public String create() throws Exception {
+        public String create() {
             if (trigger.equals(OomeTrigger.CREATE)) {
                 throw new OutOfMemoryError();
             }
@@ -78,7 +78,7 @@ public class TestSoftRefOutOfMemory {
         }
 
         @Override
-        public void destroyObject(final PooledObject<String> p) throws Exception {
+        public void destroyObject(final PooledObject<String> p) {
             if (trigger.equals(OomeTrigger.DESTROY)) {
                 throw new OutOfMemoryError();
             }
@@ -105,7 +105,7 @@ public class TestSoftRefOutOfMemory {
         DESTROY
     }
 
-    public static class SmallPoolableObjectFactory extends BasePooledObjectFactory<String> {
+    public static class SmallPoolableObjectFactory extends BasePooledObjectFactory<String, RuntimeException> {
         private int counter;
 
         @Override
@@ -124,7 +124,7 @@ public class TestSoftRefOutOfMemory {
         }
     }
 
-    private SoftReferenceObjectPool<String> pool;
+    private SoftReferenceObjectPool<String, RuntimeException> pool;
 
     @AfterEach
     public void tearDown() {
@@ -135,7 +135,6 @@ public class TestSoftRefOutOfMemory {
         System.gc();
     }
 
-
     @Test
     public void testOutOfMemory() throws Exception {
         pool = new SoftReferenceObjectPool<>(new SmallPoolableObjectFactory());
diff --git a/src/test/java/org/apache/commons/pool2/impl/TestSoftReferenceObjectPool.java b/src/test/java/org/apache/commons/pool2/impl/TestSoftReferenceObjectPool.java
index f29cc6c..a12b7da 100644
--- a/src/test/java/org/apache/commons/pool2/impl/TestSoftReferenceObjectPool.java
+++ b/src/test/java/org/apache/commons/pool2/impl/TestSoftReferenceObjectPool.java
@@ -26,7 +26,7 @@ import org.apache.commons.pool2.TestBaseObjectPool;
  */
 public class TestSoftReferenceObjectPool extends TestBaseObjectPool {
 
-    private static class SimpleFactory extends BasePooledObjectFactory<String>  {
+    private static class SimpleFactory extends BasePooledObjectFactory<String, RuntimeException>  {
         int counter;
         @Override
         public String create() {
@@ -54,13 +54,13 @@ public class TestSoftReferenceObjectPool extends TestBaseObjectPool {
     }
 
     @Override
-    protected ObjectPool<String> makeEmptyPool(final int cap) {
-        return new SoftReferenceObjectPool<>(new SimpleFactory());
+    protected <E extends Exception> ObjectPool<String, E> makeEmptyPool(final int cap) {
+        return (ObjectPool<String, E>) new SoftReferenceObjectPool<>(new SimpleFactory());
     }
 
 
     @Override
-    protected ObjectPool<Object> makeEmptyPool(final PooledObjectFactory<Object> factory) {
+    protected <E extends Exception> ObjectPool<Object, E> makeEmptyPool(final PooledObjectFactory<Object, E> factory) {
         return new SoftReferenceObjectPool<>(factory);
     }
 }
diff --git a/src/test/java/org/apache/commons/pool2/impl/TestSynchronizedPooledObjectFactory.java b/src/test/java/org/apache/commons/pool2/impl/TestSynchronizedPooledObjectFactory.java
index 53d7fe3..ba5f7f9 100644
--- a/src/test/java/org/apache/commons/pool2/impl/TestSynchronizedPooledObjectFactory.java
+++ b/src/test/java/org/apache/commons/pool2/impl/TestSynchronizedPooledObjectFactory.java
@@ -34,13 +34,13 @@ import org.apache.commons.pool2.PooledObjectFactory;
  * library.
  * </p>
  */
-final class TestSynchronizedPooledObjectFactory<T> implements PooledObjectFactory<T> {
+final class TestSynchronizedPooledObjectFactory<T, E extends Exception> implements PooledObjectFactory<T, E> {
 
 	/** Synchronization lock */
 	private final WriteLock writeLock = new ReentrantReadWriteLock().writeLock();
 
 	/** Wrapped factory */
-	private final PooledObjectFactory<T> factory;
+	private final PooledObjectFactory<T, E> factory;
 
 	/**
 	 * Constructs a SynchronizedPoolableObjectFactory wrapping the given factory.
@@ -50,7 +50,7 @@ final class TestSynchronizedPooledObjectFactory<T> implements PooledObjectFactor
 	 * @throws IllegalArgumentException
 	 *             if the factory is null
 	 */
-	TestSynchronizedPooledObjectFactory(final PooledObjectFactory<T> factory) throws IllegalArgumentException {
+	TestSynchronizedPooledObjectFactory(final PooledObjectFactory<T, E> factory) throws IllegalArgumentException {
 		if (factory == null) {
 			throw new IllegalArgumentException("factory must not be null.");
 		}
@@ -61,7 +61,7 @@ final class TestSynchronizedPooledObjectFactory<T> implements PooledObjectFactor
 	 * {@inheritDoc}
 	 */
 	@Override
-	public void activateObject(final PooledObject<T> p) throws Exception {
+	public void activateObject(final PooledObject<T> p) throws E {
 		writeLock.lock();
 		try {
 			factory.activateObject(p);
@@ -74,7 +74,7 @@ final class TestSynchronizedPooledObjectFactory<T> implements PooledObjectFactor
 	 * {@inheritDoc}
 	 */
 	@Override
-	public void destroyObject(final PooledObject<T> p) throws Exception {
+	public void destroyObject(final PooledObject<T> p) throws E {
 		writeLock.lock();
 		try {
 			factory.destroyObject(p);
@@ -87,7 +87,7 @@ final class TestSynchronizedPooledObjectFactory<T> implements PooledObjectFactor
 	 * {@inheritDoc}
 	 */
 	@Override
-	public PooledObject<T> makeObject() throws Exception {
+	public PooledObject<T> makeObject() throws E {
 		writeLock.lock();
 		try {
 			return factory.makeObject();
@@ -100,7 +100,7 @@ final class TestSynchronizedPooledObjectFactory<T> implements PooledObjectFactor
 	 * {@inheritDoc}
 	 */
 	@Override
-	public void passivateObject(final PooledObject<T> p) throws Exception {
+	public void passivateObject(final PooledObject<T> p) throws E {
 		writeLock.lock();
 		try {
 			factory.passivateObject(p);
diff --git a/src/test/java/org/apache/commons/pool2/performance/PerformanceTest.java b/src/test/java/org/apache/commons/pool2/performance/PerformanceTest.java
index 0b19772..03eb38f 100644
--- a/src/test/java/org/apache/commons/pool2/performance/PerformanceTest.java
+++ b/src/test/java/org/apache/commons/pool2/performance/PerformanceTest.java
@@ -126,7 +126,7 @@ public class PerformanceTest {
 
     private int nrIterations = 5;
 
-    private GenericObjectPool<Integer> pool;
+    private GenericObjectPool<Integer, RuntimeException> pool;
 
     private void run(final int iterations, final int nrThreads, final int maxTotal, final int maxIdle) {
         this.nrIterations = iterations;
diff --git a/src/test/java/org/apache/commons/pool2/performance/SleepingObjectFactory.java b/src/test/java/org/apache/commons/pool2/performance/SleepingObjectFactory.java
index bcf0165..a12b664 100644
--- a/src/test/java/org/apache/commons/pool2/performance/SleepingObjectFactory.java
+++ b/src/test/java/org/apache/commons/pool2/performance/SleepingObjectFactory.java
@@ -25,13 +25,13 @@ import org.apache.commons.pool2.impl.DefaultPooledObject;
 /**
  * Sleepy ObjectFactory (everything takes a while longer)
  */
-public class SleepingObjectFactory implements PooledObjectFactory<Integer> {
+public class SleepingObjectFactory implements PooledObjectFactory<Integer, RuntimeException> {
 
     private int counter;
     private boolean debug;
 
     @Override
-    public void activateObject(final PooledObject<Integer> obj) throws Exception {
+    public void activateObject(final PooledObject<Integer> obj) {
         debug("activateObject", obj);
         Waiter.sleepQuietly(10);
     }
@@ -44,7 +44,7 @@ public class SleepingObjectFactory implements PooledObjectFactory<Integer> {
     }
 
     @Override
-    public void destroyObject(final PooledObject<Integer> obj) throws Exception {
+    public void destroyObject(final PooledObject<Integer> obj) {
         debug("destroyObject", obj);
         Waiter.sleepQuietly(250);
     }
@@ -54,7 +54,7 @@ public class SleepingObjectFactory implements PooledObjectFactory<Integer> {
     }
 
     @Override
-    public PooledObject<Integer> makeObject() throws Exception {
+    public PooledObject<Integer> makeObject() {
         // Deliberate choice to create a new object in case future unit tests
         // check for a specific object.
         final Integer obj = Integer.valueOf(counter++);
@@ -64,7 +64,7 @@ public class SleepingObjectFactory implements PooledObjectFactory<Integer> {
     }
 
     @Override
-    public void passivateObject(final PooledObject<Integer> obj) throws Exception {
+    public void passivateObject(final PooledObject<Integer> obj) {
         debug("passivateObject", obj);
         Waiter.sleepQuietly(10);
     }
diff --git a/src/test/java/org/apache/commons/pool2/proxy/BaseTestProxiedKeyedObjectPool.java b/src/test/java/org/apache/commons/pool2/proxy/BaseTestProxiedKeyedObjectPool.java
index b62264c..7c64922 100644
--- a/src/test/java/org/apache/commons/pool2/proxy/BaseTestProxiedKeyedObjectPool.java
+++ b/src/test/java/org/apache/commons/pool2/proxy/BaseTestProxiedKeyedObjectPool.java
@@ -40,18 +40,19 @@ import org.junit.jupiter.api.Test;
 
 public abstract class BaseTestProxiedKeyedObjectPool {
 
-    private static class TestKeyedObjectFactory extends
-            BaseKeyedPooledObjectFactory<String,TestObject> {
+    private static class TestKeyedObjectFactory extends BaseKeyedPooledObjectFactory<String, TestObject, RuntimeException> {
 
         @Override
-        public TestObject create(final String key) throws Exception {
+        public TestObject create(final String key) {
             return new TestObjectImpl();
         }
+
         @Override
         public PooledObject<TestObject> wrap(final TestObject value) {
             return new DefaultPooledObject<>(value);
         }
     }
+
     protected interface TestObject {
         String getData();
         void setData(String data);
@@ -79,7 +80,7 @@ public abstract class BaseTestProxiedKeyedObjectPool {
 
     private static final Duration ABANDONED_TIMEOUT_SECS = Duration.ofSeconds(3);
 
-    private KeyedObjectPool<String,TestObject> pool;
+    private KeyedObjectPool<String, TestObject, RuntimeException> pool;
 
     private StringWriter log;
 
@@ -102,13 +103,10 @@ public abstract class BaseTestProxiedKeyedObjectPool {
         final GenericKeyedObjectPoolConfig<TestObject> config = new GenericKeyedObjectPoolConfig<>();
         config.setMaxTotal(3);
 
-        final KeyedPooledObjectFactory<String, TestObject> factory =
-                new TestKeyedObjectFactory();
+        final KeyedPooledObjectFactory<String, TestObject, RuntimeException> factory = new TestKeyedObjectFactory();
 
         @SuppressWarnings("resource")
-        final KeyedObjectPool<String, TestObject> innerPool =
-                new GenericKeyedObjectPool<>(
-                        factory, config, abandonedConfig);
+        final KeyedObjectPool<String, TestObject, RuntimeException> innerPool = new GenericKeyedObjectPool<>(factory, config, abandonedConfig);
 
         pool = new ProxiedKeyedObjectPool<>(innerPool, getproxySource());
     }
diff --git a/src/test/java/org/apache/commons/pool2/proxy/BaseTestProxiedObjectPool.java b/src/test/java/org/apache/commons/pool2/proxy/BaseTestProxiedObjectPool.java
index 84e6cac..d560fe0 100644
--- a/src/test/java/org/apache/commons/pool2/proxy/BaseTestProxiedObjectPool.java
+++ b/src/test/java/org/apache/commons/pool2/proxy/BaseTestProxiedObjectPool.java
@@ -44,10 +44,10 @@ public abstract class BaseTestProxiedObjectPool {
         void setData(String data);
     }
     private static class TestObjectFactory extends
-            BasePooledObjectFactory<TestObject> {
+            BasePooledObjectFactory<TestObject, RuntimeException> {
 
         @Override
-        public TestObject create() throws Exception {
+        public TestObject create() {
             return new TestObjectImpl();
         }
         @Override
@@ -75,7 +75,7 @@ public abstract class BaseTestProxiedObjectPool {
     private static final Duration ABANDONED_TIMEOUT_SECS = Duration.ofSeconds(3);
 
 
-    private ObjectPool<TestObject> pool;
+    private ObjectPool<TestObject, RuntimeException> pool;
 
     private StringWriter log;
 
@@ -98,11 +98,10 @@ public abstract class BaseTestProxiedObjectPool {
         final GenericObjectPoolConfig<TestObject> config = new GenericObjectPoolConfig<>();
         config.setMaxTotal(3);
 
-        final PooledObjectFactory<TestObject> factory = new TestObjectFactory();
+        final PooledObjectFactory<TestObject, RuntimeException> factory = new TestObjectFactory();
 
         @SuppressWarnings("resource")
-        final ObjectPool<TestObject> innerPool =
-                new GenericObjectPool<>(factory, config, abandonedConfig);
+        final ObjectPool<TestObject, RuntimeException> innerPool = new GenericObjectPool<>(factory, config, abandonedConfig);
 
         pool = new ProxiedObjectPool<>(innerPool, getproxySource());
     }

Debdiff

[The following lists of changes regard files as different if they have different names, permissions or owners.]

Files in second set of .debs but not in first

-rw-r--r--  root/root   /usr/share/maven-repo/org/apache/commons/commons-pool2/2.12.0-SNAPSHOT/commons-pool2-2.12.0-SNAPSHOT.pom
lrwxrwxrwx  root/root   /usr/share/java/commons-pool2-2.12.0-SNAPSHOT.jar -> commons-pool2.jar
lrwxrwxrwx  root/root   /usr/share/maven-repo/org/apache/commons/commons-pool2/2.12.0-SNAPSHOT/commons-pool2-2.12.0-SNAPSHOT.jar -> ../../../../../../java/commons-pool2.jar

Files in first set of .debs but not in second

-rw-r--r--  root/root   /usr/share/maven-repo/org/apache/commons/commons-pool2/2.11.1/commons-pool2-2.11.1.pom
lrwxrwxrwx  root/root   /usr/share/java/commons-pool2-2.11.1.jar -> commons-pool2.jar
lrwxrwxrwx  root/root   /usr/share/maven-repo/org/apache/commons/commons-pool2/2.11.1/commons-pool2-2.11.1.jar -> ../../../../../../java/commons-pool2.jar

No differences were encountered in the control files

More details

Full run details