Apache Commons Pool 2.13.1 快速入门与集成示例

以最小可运行工程演示 Apache Commons Pool 2.13.1 的快速上手:Maven 集成、对象借还与复用验证、三个常见坑的实测复现与解法、与 Commons Pool 1.x 同进程共存的集成示例。

Apache Commons PoolApache Commons Pool2026/10/15

Apache Commons Pool 2.13.1 快速入门与集成示例

目标:10 分钟内跑通 Apache Commons Pool 2.13.1 的第一个对象池程序——借出、归还、空闲复用与池统计,并实测复现集成过程中的三个常见坑与「pool2 + pool 1.x 同进程共存」的集成方式;本文全部命令与回显均已在 Ubuntu 22.04 实测通过。

Apache Commons Pool 是把「昂贵对象的创建—复用—回收」托管起来的对象池化框架。接入它的固定套路只有三步:引入 commons-pool2 依赖(零传递依赖)、实现一个 PooledObjectFactory(对象怎么建、怎么激活、怎么校验、怎么销毁)、用工厂构造 GenericObjectPool 并 borrowObject/returnObject。本文在 Maven 工程里完整走一遍,并给出验证点与踩坑实录。

一、环境信息

本节固定可复现基线环境,后续所有命令均在此环境执行。

  • 操作系统:Ubuntu 22.04.5 LTS
  • 架构:x86_64
  • JDK:OpenJDK 17.0.20.1(apt 包 openjdk-17-jdk,Ubuntu 构建)
  • 构建工具:Apache Maven 3.6.3
  • 组件版本:commons-pool2 2.13.1
  • Maven 坐标:org.apache.commons:commons-pool2:2.13.1
  • 工作目录:/root/commons-pool-demo

二、10 分钟最小可用

本节从空目录开始,到「借出—归还—复用」验证程序跑通为止,共三步:建工程、写程序、编译运行。

2.1 创建工程并引入依赖

创建目录与 pom.xml:

mkdir -p /root/commons-pool-demo/src/main/java && cd /root/commons-pool-demo

pom.xml(完整内容,文件路径 /root/commons-pool-demo/pom.xml):

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.example.pool</groupId>
  <artifactId>commons-pool-demo</artifactId>
  <version>1.0.0</version>
  <packaging>jar</packaging>

  <properties>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-pool2</artifactId>
      <version>2.13.1</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.11.0</version>
      </plugin>
    </plugins>
  </build>
</project>

两个必做项说明:

  • <dependencies> 里只声明 org.apache.commons:commons-pool2:2.13.1 一项。commons-pool2 没有任何传递依赖,这一项就是全部。
  • <properties> 里固定 sourceEncoding=UTF-8 与 compiler 插件 3.11.0。省略前者时,含中文注释的源码在 POSIX locale 下编译会报 unmappable character;省略后者时老 Maven 自带的旧编译插件行为可能与预期不一致。

2.2 编写第一个程序

验证程序 PoolCheck.java(文件路径 /root/commons-pool-demo/src/main/java/PoolCheck.java)。程序模拟「创建成本 20ms 的昂贵连接对象」,验证三件事:首次借出触发真实创建、归还进入空闲队列、二次借出按 LIFO 顺序复用且不再新建:

import org.apache.commons.pool2.BasePooledObjectFactory;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

public class PoolCheck {

    /* 模拟创建成本 20ms 的昂贵连接对象 */
    static class Conn {
        final int id;
        Conn(int id) { this.id = id; }
    }

    static class ConnFactory extends BasePooledObjectFactory<Conn> {
        final AtomicInteger seq = new AtomicInteger();
        @Override public Conn create() throws Exception {
            Thread.sleep(20);
            return new Conn(seq.incrementAndGet());
        }
        @Override public PooledObject<Conn> wrap(Conn c) {
            return new DefaultPooledObject<>(c);
        }
    }

    public static void main(String[] args) throws Exception {
        GenericObjectPoolConfig<Conn> cfg = new GenericObjectPoolConfig<>();
        cfg.setMaxTotal(4);
        GenericObjectPool<Conn> pool = new GenericObjectPool<>(new ConnFactory(), cfg);

        // 首次借出:池为空,触发 3 次对象创建
        List<Conn> held = new ArrayList<>();
        StringBuilder first = new StringBuilder();
        for (int i = 0; i < 3; i++) {
            Conn c = pool.borrowObject();
            held.add(c);
            first.append("Conn-").append(c.id);
            if (i < 2) first.append(", ");
        }
        System.out.println("首次借出: " + first + "  numActive=" + pool.getNumActive()
                + ", createdCount=" + pool.getCreatedCount());

        // 全部归还
        for (Conn c : held) {
            pool.returnObject(c);
        }
        System.out.println("归还后: numIdle=" + pool.getNumIdle() + ", numActive=" + pool.getNumActive());

        // 二次借出:全部复用空闲对象(LIFO 顺序),不再新建
        StringBuilder second = new StringBuilder();
        for (int i = 0; i < 3; i++) {
            Conn c = pool.borrowObject();
            second.append("Conn-").append(c.id);
            if (i < 2) second.append(", ");
        }
        System.out.println("二次借出: " + second + "  createdCount=" + pool.getCreatedCount()
                + "(复用空闲对象,未新建)");

        System.out.println("commons-pool2 OK");
        pool.close();
    }
}

代码要点逐项说明:

  • ConnFactory 继承 BasePooledObjectFactory<Conn>,只需实现 create() 与 wrap() 两个方法;它是 PooledObjectFactory 五个钩子的简化基类(激活/钝化/校验默认空实现),完整钩子写法见《Apache Commons Pool 2.13.1 常用 API 与池化策略详解》第三章。
  • GenericObjectPoolConfig 先设 maxTotal=4(池容量上限),其余参数用默认值;全部默认值见《Apache Commons Pool 2.13.1 配置参数详解》第四章。
  • borrowObject():借出对象(池空则触发工厂创建,池满则按 maxWait 配置等待或失败)。
  • returnObject():归还对象回空闲队列;getNumActive/getNumIdle/getCreatedCount 是池的实时计数器。

2.3 编译与运行

编译工程(首次构建需下载依赖与插件,实测耗时约 1 分 36 秒;依赖已缓存后重建在秒级):

mvn -B -q compile; echo "EXIT=$?"
EXIT=0

确认依赖树(commons-pool2 零传递依赖的直接证据):

mvn -B dependency:tree 2>&1 | grep -E "^\[INFO\] (com\.example|\\\- )"
[INFO] com.example.pool:commons-pool-demo:jar:1.0.0
[INFO] \- org.apache.commons:commons-pool2:jar:2.13.1:compile

运行程序(-Dfile.encoding=UTF-8 -Dstdout.encoding=UTF-8 用于固定中文输出,见 3.2 节):

P=$HOME/.m2/repository/org/apache/commons/commons-pool2/2.13.1/commons-pool2-2.13.1.jar
java -Dfile.encoding=UTF-8 -Dstdout.encoding=UTF-8 -cp target/classes:$P PoolCheck
首次借出: Conn-1, Conn-2, Conn-3  numActive=3, createdCount=3
归还后: numIdle=3, numActive=0
二次借出: Conn-3, Conn-2, Conn-1  createdCount=3(复用空闲对象,未新建)
commons-pool2 OK

2.4 验证点与预期结果

对照上节回显逐项核对:

验证点预期实测回显结论
首次借出触发真实创建借 3 个创建 3 个numActive=3, createdCount=3通过
归还进入空闲队列空闲 3、在用 0numIdle=3, numActive=0通过
二次借出复用不新建借出序号倒序、创建数不变Conn-3, Conn-2, Conn-1 且 createdCount=3通过
依赖极简依赖树仅一个组件节点\- commons-pool2:jar:2.13.1:compile通过

要点提示:二次借出 打印的是 Conn-3, Conn-2, Conn-1 而非 Conn-1, Conn-2, Conn-3,这正是池默认 LIFO(后进先出)借出顺序的直接证据——最近归还的对象优先复用;若打印了创建数增长(createdCount 变 6),说明复用失效,需检查对象是否真的调用了 returnObject。

三、Maven 工程常见坑

本节实测复现集成过程中最容易踩的三个坑,每个都给出复现回显与解法。

3.1 坑一:Nothing to compile 假成功——BUILD SUCCESS 但类没编出来

复现:把源码移出 src/main/java(例如临时放在工程根目录),执行 mvn clean compile:

cd /root/commons-pool-demo
mv src/main/java/PoolCheck.java ./PoolCheck.java
mvn -B clean compile 2>&1 | grep -iE "no sources|nothing to compile|BUILD"
[INFO] Nothing to compile - all classes are up to date
[INFO] BUILD SUCCESS

此时运行会直接失败(实测回显):

java -cp target/classes:$HOME/.m2/repository/org/apache/commons/commons-pool2/2.13.1/commons-pool2-2.13.1.jar PoolCheck
Error: Could not find or load main class PoolCheck
Caused by: java.lang.ClassNotFoundException: PoolCheck

原因与解法:Maven 只编译 src/main/java 下的源码,源码放错位置时编译器「无事可做」却仍报 BUILD SUCCESS,坑在于假成功把问题推迟到运行期。解法:把源码放回 src/main/java,并认准编译日志里的 Compiling 1 source file 行——看到它才说明真的编了。

mv ./PoolCheck.java src/main/java/
mvn -B -q compile && echo "重新编译 EXIT=$?"
重新编译 EXIT=0

3.2 坑二:中文输出变问号——JVM 默认编码跟随系统 locale

复现:不加载编码参数,直接运行同一程序(实测回显):

java -cp target/classes:$HOME/.m2/repository/org/apache/commons/commons-pool2/2.13.1/commons-pool2-2.13.1.jar PoolCheck | head -4
????: Conn-1, Conn-2, Conn-3  numActive=3, createdCount=3
???: numIdle=3, numActive=0
????: Conn-3, Conn-2, Conn-1  createdCount=3????????????
commons-pool2 OK

原因与解法:系统 locale 为 POSIX/非 UTF-8 时,JVM 默认输出编码随之变成 ASCII,System.out 打印的中文全部被替换为 ?;纯 ASCII 行(commons-pool2 OK)不受影响——这也是判断「编码问题而非数据问题」的特征。解法二选一:运行时追加 -Dfile.encoding=UTF-8 -Dstdout.encoding=UTF-8,或把系统 locale 调成 UTF-8(export LANG=C.UTF-8)。

3.3 坑三:borrowObject 阻塞或抛 NoSuchElementException——池容量与等待策略

集成后最常见的运行期异常来自「池被借空」。commons-pool2 的默认行为是 blockWhenExhausted=true:池满时 borrowObject 阻塞等待,直到 maxWait 超时才抛异常。实测两种表现(回显取自本文同环境的池实验程序):

== maxWait=300ms,超时失败 ==
捕获 NoSuchElementException, 实际等待=300ms
异常消息: Timeout waiting for idle object, maxWaitDuration=PT0.29994199S

== blockWhenExhausted=false,立即失败 ==
捕获 NoSuchElementException, 耗时 0ms(未等待)
异常消息: Pool exhausted

原因与解法:前者是默认路径——池中对象全部被业务占用,maxWait 到点后抛出;后者是把 blockWhenExhausted 关掉后的立即失败路径。处理思路按优先级:先查业务是否漏还对象(borrowObject 后必须保证 returnObject/invalidateObject 在 finally 中执行);确属并发高峰再调大 maxTotal 或 maxWait。参数含义与默认值见《Apache Commons Pool 2.13.1 配置参数详解》5.1 与 5.2 节。

四、与 Commons Pool 1.x 共存(同进程集成)

遗留系统里常同时存在 pool 1.x 的老代码与 pool2 的新代码。两者的包名完全不同(org.apache.commons.pool 对 org.apache.commons.pool2),同一进程、同一 classpath 可以共存,且 POJO 可以被两种池共享。实测程序(文件路径 /root/pool-exp/Exp10Coexist.java):

import org.apache.commons.pool2.BasePooledObjectFactory;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import org.apache.commons.pool2.impl.GenericObjectPool;

import java.util.concurrent.atomic.AtomicInteger;

public class Exp10Coexist {

    static class Item {
        final int id;
        Item(int id) { this.id = id; }
    }

    // pool2 的工厂
    static class ItemFactory2 extends BasePooledObjectFactory<Item> {
        final AtomicInteger seq = new AtomicInteger();
        @Override public Item create() { return new Item(seq.incrementAndGet()); }
        @Override public PooledObject<Item> wrap(Item i) { return new DefaultPooledObject<>(i); }
    }

    // pool 1.x 的工厂
    static class ItemFactory1 implements org.apache.commons.pool.PoolableObjectFactory<Item> {
        final AtomicInteger seq = new AtomicInteger();
        @Override public Item makeObject() { return new Item(100 + seq.incrementAndGet()); }
        @Override public void activateObject(Item o) { }
        @Override public boolean validateObject(Item o) { return true; }
        @Override public void passivateObject(Item o) { }
        @Override public void destroyObject(Item o) { }
    }

    public static void main(String[] args) throws Exception {
        GenericObjectPool<Item> p2 = new GenericObjectPool<>(new ItemFactory2());
        Item a = p2.borrowObject();
        System.out.println("pool2 类名: " + p2.getClass().getName());
        System.out.println("pool2 借到: Item-" + a.id);
        p2.returnObject(a);

        org.apache.commons.pool.impl.GenericObjectPool<Item> p1 =
                new org.apache.commons.pool.impl.GenericObjectPool<>(new ItemFactory1());
        Item b = p1.borrowObject();
        System.out.println("pool1 类名: " + p1.getClass().getName());
        System.out.println("pool1 借到: Item-" + b.id + "(同一 Item 类,两个池共享 POJO)");
        p1.returnObject(b);
        System.out.println("coexist OK");
    }
}

编译时需要同时把两个 jar 放上 classpath(pool 1.x 最新版 1.6 从中央仓库获取):

cd /root/pool-exp
javac -encoding UTF-8 -cp commons-pool2-2.13.1.jar:commons-pool-1.6.jar -d out Exp10Coexist.java
java -Dfile.encoding=UTF-8 -Dstdout.encoding=UTF-8 -cp out:commons-pool2-2.13.1.jar:commons-pool-1.6.jar Exp10Coexist
pool2 类名: org.apache.commons.pool2.impl.GenericObjectPool
pool2 借到: Item-1
pool1 类名: org.apache.commons.pool.impl.GenericObjectPool
pool1 借到: Item-101(同一 Item 类,两个池共享 POJO)
coexist OK

结果解读:两个 GenericObjectPool 全限定名分别落在 pool2 与 pool 包下,互不冲突;同一个 Item 类先被 pool2 借出(id 从 1 起),再被 pool1 借出(id 从 101 起),证明 POJO 层完全共享。集成时的注意点只有两个:工厂接口不同(1.x 是 PoolableObjectFactory,2.x 是 PooledObjectFactory),不能混用;新代码一律用 2.x(1.x 已停止维护)。

五、集成选型建议

本节给出把 Commons Pool 集成进项目的选型决策树。

需求场景推荐方案理由
池化任意昂贵对象(连接、会话、缓冲区、解析器)commons-pool2 2.13.1通用抽象 + 生命周期钩子 + 驱逐 + JMX,实测零传递依赖
遗留代码已在 pool 1.x 上1.x 维持、新模块并行引入 2.x实测同进程可共存(第四章),包名隔离无冲突
纯 JDBC 连接池DBCP2(构建于 pool2)或 HikariCP专用实现针对 JDBC 语义做了优化,无需自己拼装
不想引入构建工具官方 bin 包 + jar 直跑主 jar 仅 156457 字节,javac/java -cp 即可

要点提示:commons-pool2 是「框架」而不是「成品」,必须自己实现工厂钩子(尤其 validateObject 与 destroyObject 的健壮性),集成工作量集中在工厂实现与参数调优;只池化 JDBC 连接时优先考虑现成连接池组件。

六、参考链接

  1. Apache Commons Pool 官方主页与用户指南:https://commons.apache.org/proper/commons-pool/
  2. Commons Pool 2.13.1 Javadoc(GenericObjectPool/PooledObjectFactory):https://commons.apache.org/proper/commons-pool/apidocs/
  3. Maven 中央仓库 commons-pool2 2.13.1:https://repo1.maven.org/maven2/org/apache/commons/commons-pool2/2.13.1/
  4. Maven 中央仓库 commons-pool 1.6(1.x 末版):https://repo1.maven.org/maven2/commons-pool/commons-pool/1.6/
  5. Commons Pool 2.13.1 发布说明:https://downloads.apache.org/commons/pool/RELEASE-NOTES.txt