Apache Commons Pool 2.13.1 快速入门与集成示例
以最小可运行工程演示 Apache Commons Pool 2.13.1 的快速上手:Maven 集成、对象借还与复用验证、三个常见坑的实测复现与解法、与 Commons Pool 1.x 同进程共存的集成示例。
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、在用 0 | numIdle=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 连接时优先考虑现成连接池组件。
六、参考链接
- Apache Commons Pool 官方主页与用户指南:https://commons.apache.org/proper/commons-pool/
- Commons Pool 2.13.1 Javadoc(GenericObjectPool/PooledObjectFactory):https://commons.apache.org/proper/commons-pool/apidocs/
- Maven 中央仓库 commons-pool2 2.13.1:https://repo1.maven.org/maven2/org/apache/commons/commons-pool2/2.13.1/
- Maven 中央仓库 commons-pool 1.6(1.x 末版):https://repo1.maven.org/maven2/commons-pool/commons-pool/1.6/
- Commons Pool 2.13.1 发布说明:https://downloads.apache.org/commons/pool/RELEASE-NOTES.txt