JDBC基础(2)

JDBC基础(2)

作者:没有四次元口袋的蓝胖

日期:2026-06-29

标签:Java, JDBC, 工具类


一、为什么要封装工具类?

原生 JDBC 代码有很多重复:

  • 每次都要写注册驱动、获取连接
  • 每次都要写繁琐的释放资源
  • 连接参数硬编码在代码里,修改不方便

封装 JDBCUtils 的目的: 减少重复代码,提高开发效率。


二、JDBCUtils 工具类的编写

2.1 配置文件(jdbc.properties)

把连接参数放到配置文件里,方便修改:

properties 复制代码
# jdbc.properties
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/db_name?useSSL=false&serverTimezone=UTC&characterEncoding=utf8
username=root
password=123456

2.2 工具类代码

java 复制代码
import java.io.IOException;
import java.io.InputStream;
import java.sql.*;
import java.util.Properties;

/**
 * JDBC 工具类
 * 功能:获取连接、释放资源
 */
public class JDBCUtils {
    private static String driver;
    private static String url;
    private static String username;
    private static String password;

    // 静态代码块:类加载时读取配置文件、注册驱动,只执行一次
    static {
        try {
            // 1. 读取配置文件
            Properties prop = new Properties();
            // 使用类加载器读取 src 下的配置文件
            InputStream is = JDBCUtils.class.getClassLoader().getResourceAsStream("jdbc.properties");
            prop.load(is);

            // 2. 获取参数
            driver = prop.getProperty("driver");
            url = prop.getProperty("url");
            username = prop.getProperty("username");
            password = prop.getProperty("password");

            // 3. 注册驱动
            Class.forName(driver);
        } catch (IOException | ClassNotFoundException e) {
            throw new RuntimeException("JDBC 配置文件加载失败", e);
        }
    }

    /**
     * 获取连接
     */
    public static Connection getConnection() throws SQLException {
        return DriverManager.getConnection(url, username, password);
    }

    /**
     * 释放资源(增删改用,无 ResultSet)
     */
    public static void close(Connection conn, Statement stmt) {
        close(null, stmt, conn);
    }

    /**
     * 释放资源(查询用,有 ResultSet)
     */
    public static void close(ResultSet rs, Statement stmt, Connection conn) {
        if (rs != null) {
            try { rs.close(); } catch (SQLException e) { e.printStackTrace(); }
        }
        if (stmt != null) {
            try { stmt.close(); } catch (SQLException e) { e.printStackTrace(); }
        }
        if (conn != null) {
            try { conn.close(); } catch (SQLException e) { e.printStackTrace(); }
        }
    }
}

2.3 使用工具类

java 复制代码
// 查询示例
public Student findById(int id) {
    Connection conn = null;
    PreparedStatement pstmt = null;
    ResultSet rs = null;
    try {
        conn = JDBCUtils.getConnection();
        String sql = "SELECT * FROM student WHERE id = ?";
        pstmt = conn.prepareStatement(sql);
        pstmt.setInt(1, id);
        rs = pstmt.executeQuery();

        Student stu = null;
        if (rs.next()) {
            stu = new Student();
            stu.setId(rs.getInt("id"));
            stu.setName(rs.getString("name"));
            stu.setAge(rs.getInt("age"));
        }
        return stu;
    } catch (SQLException e) {
        e.printStackTrace();
        return null;
    } finally {
        JDBCUtils.close(rs, pstmt, conn);
    }
}

工具类的好处:

  • 连接参数统一管理,修改方便
  • 获取连接和释放资源不用重复写
  • 代码更简洁

2.4 通用增删改方法

可以进一步封装通用的增删改方法:

java 复制代码
/**
 * 通用增删改方法
 * @param sql  SQL 语句
 * @param args 参数(可变参数,对应 ? 的值)
 * @return 影响的行数
 */
public static int update(String sql, Object... args) {
    Connection conn = null;
    PreparedStatement pstmt = null;
    try {
        conn = getConnection();
        pstmt = conn.prepareStatement(sql);
        // 设置参数
        for (int i = 0; i < args.length; i++) {
            pstmt.setObject(i + 1, args[i]);
        }
        return pstmt.executeUpdate();
    } catch (SQLException e) {
        e.printStackTrace();
        return 0;
    } finally {
        close(null, pstmt, conn);
    }
}

使用:

java 复制代码
// 增
JDBCUtils.update("INSERT INTO student(name, age) VALUES(?, ?)", "王五", 23);
// 删
JDBCUtils.update("DELETE FROM student WHERE id = ?", 3);
// 改
JDBCUtils.update("UPDATE student SET age = ? WHERE name = ?", 25, "王五");

2.5 通用查询方法(返回集合)

java 复制代码
/**
 * 通用查询方法
 * @param sql    SQL 语句
 * @param handler 结果集处理器(自己定义接口)
 * @param args   参数
 */
public static <T> T query(String sql, ResultSetHandler<T> handler, Object... args) {
    Connection conn = null;
    PreparedStatement pstmt = null;
    ResultSet rs = null;
    try {
        conn = getConnection();
        pstmt = conn.prepareStatement(sql);
        for (int i = 0; i < args.length; i++) {
            pstmt.setObject(i + 1, args[i]);
        }
        rs = pstmt.executeQuery();
        return handler.handle(rs); // 交给处理器处理结果集
    } catch (SQLException e) {
        e.printStackTrace();
        return null;
    } finally {
        close(rs, pstmt, conn);
    }
}

// 结果集处理器接口
public interface ResultSetHandler<T> {
    T handle(ResultSet rs) throws SQLException;
}

// 把结果集封装成 List<Student> 的处理器
public class BeanListHandler<T> implements ResultSetHandler<List<T>> {
    private Class<T> clazz;

    public BeanListHandler(Class<T> clazz) {
        this.clazz = clazz;
    }

    @Override
    public List<T> handle(ResultSet rs) throws SQLException {
        List<T> list = new ArrayList<>();
        ResultSetMetaData metaData = rs.getMetaData();
        int columnCount = metaData.getColumnCount();

        while (rs.next()) {
            try {
                T obj = clazz.newInstance();
                for (int i = 1; i <= columnCount; i++) {
                    String columnName = metaData.getColumnName(i);
                    Object value = rs.getObject(i);
                    // 反射给字段赋值
                    Field field = clazz.getDeclaredField(columnName);
                    field.setAccessible(true);
                    field.set(obj, value);
                }
                list.add(obj);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return list;
    }
}

使用:

java 复制代码
List<Student> list = JDBCUtils.query(
    "SELECT * FROM student WHERE age > ?",
    new BeanListHandler<>(Student.class),
    18
);

三、Apache DbUtils 组件

手写通用查询比较麻烦,Apache 提供了成熟的工具类 commons-dbutils,它已经帮我们封装好了通用的增删改查和结果集处理器。

3.1 引入依赖

Maven 项目在 pom.xml 中添加:

xml 复制代码
<dependency>
    <groupId>commons-dbutils</groupId>
    <artifactId>commons-dbutils</artifactId>
    <version>1.7</version>
</dependency>

普通 Java 项目直接导入 jar 包即可:commons-dbutils-1.7.jar

3.2 QueryRunner ------ 核心类

QueryRunner 是 DbUtils 的核心操作类,它提供了增删改查的方法。

java 复制代码
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.*;

// 创建 QueryRunner 对象
QueryRunner qr = new QueryRunner();

3.3 增删改(update)

java 复制代码
// 增
String insertSql = "INSERT INTO student(name, age) VALUES(?, ?)";
int rows = qr.update(conn, insertSql, "赵六", 24);

// 删
String deleteSql = "DELETE FROM student WHERE id = ?";
int rows2 = qr.update(conn, deleteSql, 1);

// 改
String updateSql = "UPDATE student SET age = ? WHERE name = ?";
int rows3 = qr.update(conn, updateSql, 26, "赵六");

如果传入 DataSource(连接池)给 QueryRunner 构造方法,就不用每次传 Connection 了。

3.4 查询 ------ ResultSetHandler 八大实现类

实现类 作用
BeanHandler<T> 将结果集第一行封装为一个 JavaBean 对象
BeanListHandler<T> 将结果集所有行封装为 List
ScalarHandler 将结果集第一行第一列封装为 Object(用于统计查询 count/sum 等)
MapHandler 将结果集第一行封装为 Map(key=列名, value=值)
MapListHandler 将结果集所有行封装为 List
ArrayHandler 将结果集第一行封装为 Object\[\]
ArrayListHandler 将结果集所有行封装为 List<Object\[\]>
ColumnListHandler 将结果集某一列封装为 List

3.5 查询一个对象(BeanHandler)

java 复制代码
String sql = "SELECT * FROM student WHERE id = ?";
Student stu = qr.query(conn, sql, new BeanHandler<>(Student.class), 1);
System.out.println(stu);

3.6 查询集合(BeanListHandler)

java 复制代码
String sql = "SELECT * FROM student WHERE age > ?";
List<Student> list = qr.query(conn, sql, new BeanListHandler<>(Student.class), 18);

for (Student s : list) {
    System.out.println(s);
}

3.7 统计查询(ScalarHandler)

java 复制代码
String sql = "SELECT COUNT(*) FROM student";
Long count = qr.query(conn, sql, new ScalarHandler<>());
System.out.println("总记录数:" + count);

注意: SELECT COUNT(*) 返回的是 Long 类型,不是 int,要用 Long 接收。

3.8 查询 Map 集合

java 复制代码
// 查询一条 → Map
Map<String, Object> map = qr.query(conn, "SELECT * FROM student WHERE id = ?",
    new MapHandler(), 1);

// 查询多条 → List<Map>
List<Map<String, Object>> mapList = qr.query(conn, "SELECT * FROM student",
    new MapListHandler());

3.9 自定义 ResultSetHandler

如果内置的处理器满足不了需求,可以自定义:

java 复制代码
// 自定义:只取姓名列
List<String> names = qr.query(conn, "SELECT name FROM student",
    new ResultSetHandler<List<String>>() {
        @Override
        public List<String> handle(ResultSet rs) throws SQLException {
            List<String> list = new ArrayList<>();
            while (rs.next()) {
                list.add(rs.getString("name"));
            }
            return list;
        }
    });

3.10 DbUtils 工具类

DbUtils 还提供了一个 DbUtils 类,用于释放资源,比自己写的更简洁:

java 复制代码
import org.apache.commons.dbutils.DbUtils;

// 关闭连接
DbUtils.close(conn);
DbUtils.closeQuietly(conn); // 安静关闭,不抛异常

// 关闭多个
DbUtils.closeQuietly(conn, stmt, rs);

四、封装实体类的注意事项

使用 BeanHandler / BeanListHandler 时,JavaBean 必须满足:

  1. 私有字段:成员变量私有
  2. 无参构造:必须提供无参构造方法(反射要用到)
  3. getter/setter:提供所有字段的 getter 和 setter 方法
  4. 字段名对应:数据库列名要和 JavaBean 属性名一致(或用别名对应)
java 复制代码
public class Student {
    private int id;
    private String name;
    private int age;

    // 必须有无参构造
    public Student() {}

    // getter 和 setter
    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 int getAge() { return age; }
    public void setAge(int age) { this.age = age; }
}

列名和属性名不一致怎么办? 用 SQL 别名:

sql 复制代码
SELECT user_id AS userId, user_name AS userName FROM user

五、思维导图速览

复制代码
JDBC 进阶
├── JDBCUtils 工具类
│   ├── 配置文件 jdbc.properties
│   ├── 静态代码块加载配置、注册驱动
│   ├── getConnection() 获取连接
│   ├── close() 释放资源(方法重载)
│   ├── 通用 update(增删改)
│   └── 通用 query + ResultSetHandler
├── Apache DbUtils ✅
│   ├── QueryRunner(核心操作类)
│   │   ├── update(增删改)
│   │   └── query(查询)
│   ├── ResultSetHandler 八大实现
│   │   ├── BeanHandler → 一个对象
│   │   ├── BeanListHandler → List集合
│   │   ├── ScalarHandler → 单个值(统计)
│   │   ├── MapHandler / MapListHandler
│   │   ├── ArrayHandler / ArrayListHandler
│   │   └── ColumnListHandler
│   └── DbUtils 类(释放资源)
└── JavaBean 要求
    ├── 私有字段
    ├── 无参构造
    ├── getter/setter
    └── 列名与属性名对应

六、写在最后

  1. JDBCUtils 必须自己手写一遍:理解工具类的封装思想,这是面试常考的"手写 JDBC 工具类"
  2. DbUtils 要会用:实际开发用它比自己写通用方法方便得多
  3. BeanHandler / BeanListHandler 是重点:最常用的两个结果集处理器
  4. ScalarHandler 注意返回类型:count 返回 Long,别用 int 接

常见面试点

  • 手写 JDBC 工具类
  • PreparedStatement 和 Statement 的区别(SQL 注入)
  • ResultSetHandler 的实现类有哪些
  • JavaBean 封装的要求