Apache Commons DbUtils 快速入门与集成示例

从零跑通 Apache Commons DbUtils 1.8.1 的完整 CRUD:建表、插入、查询映射到 Bean 与 Map、更新、删除,并给出绑定数据源、事务回滚、批量执行与取回自增主键的集成示例,代码与回显全部实测。

Apache Commons DbUtilsApache Commons DbUtils2026/10/14

Apache Commons DbUtils 快速入门与集成示例

目标:用一篇文档从零跑通 Apache Commons DbUtils 1.8.1 的完整增删改查,并给出绑定数据源、事务回滚、批量执行、取回自增主键四类集成示例。所有代码与回显已在 Ubuntu 22.04(JDK 17 + Maven,连接 H2 内嵌数据库)实测通过。

一、环境信息

本章交代实测环境与依赖坐标,让文中每个示例都可以独立复现。Commons DbUtils 1.8.1 编译目标是 Java 8,本文用 JDK 17 运行,并用零安装的 H2 内嵌数据库提供真实 JDBC 连接。

cat /etc/os-release | grep -E "^(NAME|VERSION)="
java -version
mvn -v | grep -E "Apache Maven|Java version"

实测回显:

NAME="Ubuntu"
VERSION="22.04.5 LTS (Jammy Jellyfish)"
openjdk version "17.0.20.1" 2026-08-18
OpenJDK Runtime Environment (build 17.0.20.1+1-1-22.04-Ubuntu)
Apache Maven 3.6.3
Java version: 17.0.20.1, vendor: Ubuntu, runtime: /usr/lib/jvm/java-17-openjdk-amd64

文中统一使用的坐标为:

commons-dbutils:commons-dbutils:1.8.1   (Apache Commons DbUtils 本库)
com.h2database:h2:2.5.252               (H2 内嵌数据库驱动)

💡 技术拓展:在 DbUtils 之上「连接从哪来」决定了代码写法,常见三种获取方式对比,本文两种都用到(第三章用 DriverManager,第四、五章用 DataSource):

获取方式载体连接归属适用场景
DriverManager 直连JDBC 驱动调用方自建自关快速验证、临时脚本(本文第三章)
DataSourcejavax.sql.DataSource 实现每次 getConnection 取、用后关与数据源解耦,便于替换实现(本文第四、五章)
连接池DBCP / HikariCP 等池统一取还生产环境,连接复用

DbUtils 只做「执行 SQL + 结果集转换」,不接管连接池与事务,因此上面三种连接来源它都兼容。

二、前置条件与工程骨架

本章搭建一个可运行示例的 Java 工程,后续所有示例类都放进这个工程。创建目录与 pom.xml:

mkdir -p /opt/dbutils-demo/src/main/java/net/quzuo/demo
cd /opt/dbutils-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>net.quzuo.demo</groupId>
  <artifactId>dbutils-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>commons-dbutils</groupId>
      <artifactId>commons-dbutils</artifactId>
      <version>1.8.1</version>
    </dependency>
    <dependency>
      <groupId>com.h2database</groupId>
      <artifactId>h2</artifactId>
      <version>2.5.252</version>
    </dependency>
  </dependencies>
</project>

逐项说明:maven.compiler.source/target 指定按 Java 17 语法编译;project.build.sourceEncoding 强制源码按 UTF-8 读取;依赖里 commons-dbutils 是本库、h2 是验证用的内嵌数据库驱动。确认依赖解析结果:

mvn dependency:tree | grep -E "dbutils|h2"

实测回显:

[INFO] +- commons-dbutils:commons-dbutils:jar:1.8.1:compile
[INFO] \- com.h2database:h2:jar:2.5.252:compile

依赖树只有本库与 H2 驱动两个 compile 节点。示例中要映射的实体,写入 src/main/java/net/quzuo/demo/Product.java:

package net.quzuo.demo;

import java.math.BigDecimal;

public class Product {
    private int id;
    private String name;
    private BigDecimal price;
    private int stock;

    public int getId() { return id; }
    public void setId(int id) { this.id = id; }

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    public BigDecimal getPrice() { return price; }
    public void setPrice(BigDecimal price) { this.price = price; }

    public int getStock() { return stock; }
    public void setStock(int stock) { this.stock = stock; }

    @Override
    public String toString() {
        return "Product{id=" + id + ", name='" + name + "', price=" + price + ", stock=" + stock + "}";
    }
}

该 Bean 有 id/name/price/stock 四个属性,属性名与后面建表语句的列名一一对应(大小写不敏感匹配)。

三、快速入门:完整 CRUD 走一遍

本章用一个程序把增(INSERT)、查(SELECT)、改(UPDATE)、删(DELETE)全部走通,展示 DbUtils 最常用的写法。写入 src/main/java/net/quzuo/demo/QuickStart.java:

package net.quzuo.demo;

import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import org.apache.commons.dbutils.handlers.MapListHandler;
import org.apache.commons.dbutils.handlers.ScalarHandler;

import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.DriverManager;
import java.util.List;
import java.util.Map;

public class QuickStart {

    public static void main(String[] args) throws Exception {
        Connection conn = DriverManager.getConnection("jdbc:h2:mem:quick;DB_CLOSE_DELAY=-1");
        QueryRunner runner = new QueryRunner();

        // 1. 建表
        runner.update(conn, "create table product("
                + "id int auto_increment primary key, name varchar(32), price decimal(10,2), stock int)");
        System.out.println("1) 建表完成");

        // 2. 插入(逐条)
        runner.update(conn, "insert into product(name, price, stock) values(?, ?, ?)", "键盘", new BigDecimal("199.00"), 50);
        runner.update(conn, "insert into product(name, price, stock) values(?, ?, ?)", "鼠标", new BigDecimal("89.50"), 120);
        System.out.println("2) after insert count = "
                + runner.query(conn, "select count(*) from product", new ScalarHandler<>()));

        // 3. 查询映射到 Bean
        List<Product> products = runner.query(conn,
                "select id, name, price, stock from product order by id",
                new BeanListHandler<>(Product.class));
        System.out.println("3) BeanListHandler 映射: " + products);

        // 4. 查询映射到 Map
        List<Map<String, Object>> maps = runner.query(conn,
                "select id, name, stock from product order by id",
                new MapListHandler());
        System.out.println("4) MapListHandler 映射: " + maps);

        // 5. 更新(价格 +30,库存 -5)
        int updated = runner.update(conn,
                "update product set price = price + ?, stock = stock - ? where name = ?",
                new BigDecimal("30.00"), 5, "键盘");
        System.out.println("5) 受影响行数 = " + updated);

        // 6. 验证更新
        Product keyboard = runner.query(conn,
                "select id, name, price, stock from product where name = ?",
                new BeanListHandler<>(Product.class), "键盘").get(0);
        System.out.println("6) 更新后键盘: " + keyboard);

        // 7. 删除
        int deleted = runner.update(conn, "delete from product where name = ?", "鼠标");
        System.out.println("7) 删除行数 = " + deleted);

        // 8. 验证删除后总数
        System.out.println("8) after delete count = "
                + runner.query(conn, "select count(*) from product", new ScalarHandler<>()));

        conn.close();
    }
}

3.1 程序结构与关键写法

程序只用到一个执行入口 QueryRunner 与三个处理器:

  • new QueryRunner():不绑数据源,update/query 全部显式传入 conn(连接由本程序创建与关闭)。
  • runner.update(conn, sql, 可变参数):用于建表、插入、更新、删除,返回受影响行数(DDL 返回 0)。
  • new BeanListHandler<>(Product.class):把结果行映射成 Product 对象列表,供「取实体」用。
  • new MapListHandler():把结果行映射成 Map 列表,适合只取部分列、不建实体的场合。
  • new ScalarHandler<>():取单值,用于 select count(*) 这类聚合。

参数化写法使用 ? 占位符,参数按顺序跟在 SQL 之后(如 runner.update(conn, sql, "键盘", new BigDecimal("199.00"), 50)),由 DbUtils 内部用 PreparedStatement 填充,避免 SQL 拼接注入。

3.2 运行结果

编译运行:

mvn -q compile exec:java -Dexec.mainClass=net.quzuo.demo.QuickStart

实测回显:

1) 建表完成
2) after insert count = 2
3) BeanListHandler 映射: [Product{id=1, name='键盘', price=199.00, stock=50}, Product{id=2, name='鼠标', price=89.50, stock=120}]
4) MapListHandler 映射: [{ID=1, NAME=键盘, STOCK=50}, {ID=2, NAME=鼠标, STOCK=120}]
5) 受影响行数 = 1
6) 更新后键盘: Product{id=1, name='键盘', price=229.00, stock=45}
7) 删除行数 = 1
8) after delete count = 1

逐步解读:

  1. 第 2 行 after insert count = 2:两条 insert 各成功写入一行,用 ScalarHandler 取回的计数为 2。
  2. 第 3 行:BeanListHandler 把每行映射为 Product,中文值、BigDecimal 价格、整型库存都正确落到属性上。
  3. 第 4 行:MapListHandler 只查 id, name, stock 三列,key 是数据库返回的大写列名(H2 把未加引号的标识符折叠为大写)。
  4. 第 5、6 行:一次 update 同时改价格与库存,返回受影响行数 1;再查回来确认 price=229.00(199.00+30)、stock=45(50-5)。
  5. 第 7、8 行:删除返回受影响行数 1,删除后总数为 1,增删改查闭环成立。

至此,DbUtils 的完整 CRUD 已跑通。

四、集成示例一:绑定数据源

本章演示第二种连接来源——把 DataSource 交给 QueryRunner,之后执行 SQL 就不必再手动创建和关闭连接。写入 src/main/java/net/quzuo/demo/DataSourceDemo.java:

package net.quzuo.demo;

import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import org.apache.commons.dbutils.handlers.ScalarHandler;
import org.h2.jdbcx.JdbcDataSource;

import java.math.BigDecimal;
import java.util.List;

public class DataSourceDemo {

    public static void main(String[] args) throws Exception {
        JdbcDataSource ds = new JdbcDataSource();
        ds.setURL("jdbc:h2:mem:dsdemo;DB_CLOSE_DELAY=-1");
        ds.setUser("sa");
        ds.setPassword("");

        QueryRunner runner = new QueryRunner(ds);
        System.out.println("是否绑定同一数据源: " + (runner.getDataSource() == ds));

        runner.update("create table product("
                + "id int auto_increment primary key, name varchar(32), price decimal(10,2), stock int)");
        runner.update("insert into product(name, price, stock) values(?, ?, ?)",
                "键盘", new BigDecimal("199.00"), 50);
        runner.update("insert into product(name, price, stock) values(?, ?, ?)",
                "鼠标", new BigDecimal("89.50"), 120);

        List<Product> products = runner.query("select id, name, price, stock from product order by id",
                new BeanListHandler<>(Product.class));
        System.out.println("查询结果: " + products);
        System.out.println("总记录数: " + runner.query("select count(*) from product", new ScalarHandler<>()));
    }
}

写法要点:new QueryRunner(ds) 绑定数据源后,所有 update/query 都省略了 Connection 参数——DbUtils 会在每次调用时从数据源取连接、用完立即归还并关闭,因此循环里反复执行 SQL 不会泄漏连接。编译运行:

mvn -q compile exec:java -Dexec.mainClass=net.quzuo.demo.DataSourceDemo

实测回显:

是否绑定同一数据源: true
查询结果: [Product{id=1, name='键盘', price=199.00, stock=50}, Product{id=2, name='鼠标', price=89.50, stock=120}]
总记录数: 2

第一行证明 getDataSource() 返回的正是构造时传入的实例;后两行说明省去 Connection 后查询与聚合照常工作。生产环境把这里的 JdbcDataSource 换成连接池的数据源即可,代码其余部分不必改动。

五、集成示例二:事务、批量与取回主键

本章覆盖三类高频集成需求:手动事务、批量执行、取回自增主键。它们都构建在数据源之上。

5.1 事务控制与回滚

QueryRunner 本身不开启事务,事务需要调用方在 Connection 上控制。写入 src/main/java/net/quzuo/demo/TransactionDemo.java:

package net.quzuo.demo;

import org.apache.commons.dbutils.DbUtils;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.ScalarHandler;
import org.h2.jdbcx.JdbcDataSource;

import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.SQLException;

public class TransactionDemo {

    public static void main(String[] args) throws Exception {
        JdbcDataSource ds = new JdbcDataSource();
        ds.setURL("jdbc:h2:mem:tx;DB_CLOSE_DELAY=-1");
        ds.setUser("sa");
        ds.setPassword("");
        QueryRunner runner = new QueryRunner(ds);

        runner.update("create table product("
                + "id int auto_increment primary key, name varchar(32), price decimal(10,2), stock int)");
        runner.update("insert into product(name, price, stock) values(?, ?, ?)",
                "键盘", new BigDecimal("199.00"), 50);

        Connection conn = ds.getConnection();
        try {
            conn.setAutoCommit(false);
            runner.update(conn, "update product set stock = stock - ? where name = ?", 5, "键盘");
            Product after = runner.query(conn, "select id, name, price, stock from product where name = ?",
                    new BeanHandler<>(Product.class), "键盘");
            System.out.println("事务内读取: " + after);
            DbUtils.rollbackAndClose(conn);
            System.out.println("已回滚");
        } catch (SQLException e) {
            DbUtils.rollbackAndClose(conn);
        }

        Product reverted = runner.query("select id, name, price, stock from product where name = ?",
                new BeanHandler<>(Product.class), "键盘");
        System.out.println("回滚后再读: " + reverted);
    }
}

要点:conn.setAutoCommit(false) 开启事务;同一连接上连续执行 SQL;用 DbUtils.rollbackAndClose(conn) 回滚并关闭连接(DbUtils 的工具类 DbUtils 提供 commitAndClose/rollbackAndClose/closeQuietly 等资源管理方法)。编译运行:

mvn -q compile exec:java -Dexec.mainClass=net.quzuo.demo.TransactionDemo

实测回显:

事务内读取: Product{id=1, name='键盘', price=199.00, stock=45}
已回滚
回滚后再读: Product{id=1, name='键盘', price=199.00, stock=50}

事务内读到库存已被减到 45,回滚后再读恢复到 50,说明未提交的修改被正确撤销。

5.2 批量执行

batch 用同一段 SQL 跑多组参数,返回每条语句的受影响行数数组。写入 src/main/java/net/quzuo/demo/BatchDemo.java:

package net.quzuo.demo;

import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.ScalarHandler;
import org.h2.jdbcx.JdbcDataSource;

import java.math.BigDecimal;
import java.util.Arrays;

public class BatchDemo {

    public static void main(String[] args) throws Exception {
        JdbcDataSource ds = new JdbcDataSource();
        ds.setURL("jdbc:h2:mem:batch;DB_CLOSE_DELAY=-1");
        ds.setUser("sa");
        ds.setPassword("");
        QueryRunner runner = new QueryRunner(ds);

        runner.update("create table product("
                + "id int auto_increment primary key, name varchar(32), price decimal(10,2), stock int)");
        int[] affected = runner.batch("insert into product(name, price, stock) values(?, ?, ?)",
                new Object[][]{
                        {"键盘", new BigDecimal("199.00"), 50},
                        {"鼠标", new BigDecimal("89.50"), 120},
                        {"显示器", new BigDecimal("1299.00"), 20}
                });
        System.out.println("batch 每行受影响数: " + Arrays.toString(affected));
        System.out.println("总记录数: " + runner.query("select count(*) from product", new ScalarHandler<>()));
    }
}

要点:batch 的第二个参数是二维数组,每个一维数组是一条语句的一组参数,顺序与外层数组一致。编译运行:

mvn -q compile exec:java -Dexec.mainClass=net.quzuo.demo.BatchDemo

实测回显:

batch 每行受影响数: [1, 1, 1]
总记录数: 3

返回数组 [1, 1, 1] 表示三条 INSERT 各成功插入一行,总数 3 印证批量生效。

5.3 取回自增主键

insert 在写入后用处理器承接数据库回传的生成键。写入 src/main/java/net/quzuo/demo/KeyDemo.java:

package net.quzuo.demo;

import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.ScalarHandler;
import org.h2.jdbcx.JdbcDataSource;

import java.math.BigDecimal;

public class KeyDemo {

    public static void main(String[] args) throws Exception {
        JdbcDataSource ds = new JdbcDataSource();
        ds.setURL("jdbc:h2:mem:keys;DB_CLOSE_DELAY=-1");
        ds.setUser("sa");
        ds.setPassword("");
        QueryRunner runner = new QueryRunner(ds);

        runner.update("create table product("
                + "id int auto_increment primary key, name varchar(32), price decimal(10,2), stock int)");
        Object key1 = runner.insert("insert into product(name, price, stock) values(?, ?, ?)",
                new ScalarHandler<>(), "键盘", new BigDecimal("199.00"), 50);
        Object key2 = runner.insert("insert into product(name, price, stock) values(?, ?, ?)",
                new ScalarHandler<>(), "鼠标", new BigDecimal("89.50"), 120);
        System.out.println("第一条自增主键: " + key1);
        System.out.println("第二条自增主键: " + key2);
    }
}

要点:insert(sql, handler, 可变参数) 与 update 写法一致,差别在于它返回 handler 转换后的结果——ScalarHandler 直接拿到自增主键。编译运行:

mvn -q compile exec:java -Dexec.mainClass=net.quzuo.demo.KeyDemo

实测回显:

第一条自增主键: 1
第二条自增主键: 2

两次插入分别取回主键 1 与 2,可用于插入后立即得到新记录 ID。

⚠️ 注意:若把 insert 的处理器换成 BeanHandler<>(Product.class),返回的 Product 只有主键列被填充,其余属性仍是默认值。实测回显为 Product{id=1, name='null', price=null, stock=0}——因为数据库回传的生成键结果集只含主键列,BeanHandler 只能映射这一列。要拿完整行,需按主键再查一次,或直接用 ScalarHandler 取主键。

六、常见问题

Q1:Runner.update 返回值是 0,是没执行成功吗? 不是。update 返回受影响行数:建表等 DDL 语句本就返回 0,update/delete 命中若干行则返回对应行数(本文实测改库存返回 1、删除返回 1)。判断成败要看是否有异常抛出与后续查询结果,而非单看该返回值。

Q2:用 QueryRunner()(不绑数据源)调用 query(sql, handler) 报错怎么办? 报错信息是「QueryRunner requires a DataSource to be invoked in this way, or a Connection should be passed in」。要么改成 QueryRunner(dataSource)(见第四章),要么在每个方法里显式传入 Connection(见第三章)。

Q3:MapListHandler 返回的 Map 里 key 为什么是大写的? H2 默认把未加引号的标识符折叠为大写,所以返回列名是 ID/NAME 形态。这是数据库的标识符规则,不是 DbUtils 的处理;读取时按实际返回 key 访问即可。

Q4:插入后想拿到新记录的自增主键,该怎么写? 用 runner.insert(sql, new ScalarHandler<>(), 可变参数),返回即主键值(本文 5.3 实测得到 1、2)。不要用 BeanHandler 承接,它只会映射生成键结果集的列。

Q5:事务里怎么保证出错时回滚并释放连接? 用 DbUtils.rollbackAndClose(conn),它同时完成回滚与关闭;正常提交则用 DbUtils.commitAndClose(conn)。这两个工具方法来自 org.apache.commons.dbutils.DbUtils,能避免手动 rollback+close 的遗漏。

Q6:BeanHandler 查出来 createdAt 是 null,数据库列是 created_at,怎么回事? 默认 BeanProcessor 不做下划线转驼峰,created_at 无法映射到 createdAt。改处理器为 new BeanListHandler<>(User.class, new BasicRowProcessor(new GenerousBeanProcessor())) 即可正确映射。

七、参考链接

  1. Apache Commons DbUtils 官方主页:https://commons.apache.org/proper/commons-dbutils/
  2. QueryRunner API 文档:https://commons.apache.org/proper/commons-dbutils/apidocs/org/apache/commons/dbutils/QueryRunner.html
  3. DbUtils 工具类 API 文档:https://commons.apache.org/proper/commons-dbutils/apidocs/org/apache/commons/dbutils/DbUtils.html
  4. handlers 包(各 ResultSetHandler 实现)API 文档:https://commons.apache.org/proper/commons-dbutils/apidocs/org/apache/commons/dbutils/handlers/package-summary.html
  5. Maven 中央仓库 1.8.1 目录:https://repo1.maven.org/maven2/commons-dbutils/commons-dbutils/1.8.1/
  6. H2 Database 主页:https://h2database.com/
  7. H2 生成列与自增语法文档:https://h2database.com/html/features.html