JDBC 开发基础

JDBC (Java DataBase Connectivity) 是 Java 程序与关系型数据库交互的统一 API。JDBC 由两部分 API 构成:

  1. 第一部分是面向 Java 开发者的 API,它是一个统一的、标准的 Java API,独立于各个数据库产品的接口规范;
  2. 第二部分是面向数据库驱动程序开发者的 API,它是由各个数据库厂家提供的数据库驱动,是 Java 接口规范的底层实现,用于连接具体的数据库产品。

我们主要关注第一部分。在使用 Java 程序访问数据库时,Java 代码并不是直接通过 TCP 连接去访问数据库,而是通过 JDBC 接口来访问,而 JDBC 接口则通过 JDBC 驱动来实现真正对数据库的访问。

举例来说,如果我们要在 Java 代码中访问 MySQL 数据库,我们需要编写使用 JDBC 接口的代码。JDBC 接口是 Java 标准库内置的一部分,因此可以直接使用。而具体的 JDBC 驱动是由数据库厂商提供的,比如 MySQL 的 JDBC 驱动由 Oracle 提供。因此,要访问特定的数据库,只需引入该数据库厂商提供的 JDBC 驱动,然后就可以通过 JDBC 接口进行访问。这种方法确保我们可以编写一套通用的数据库访问代码,就能访问不同的数据库,因为它们都遵循了统一的 JDBC 驱动标准。

从代码的角度来看,Java 标准库自带的 JDBC 接口实际上只是定义了一组接口规范,而某个具体的 JDBC 驱动则是实现了这些接口规范的类。我们自己编写的代码只需要引用 Java 标准库提供的 java.sql 包中的相关接口,就能通过这些接口间接地调用各类驱动访问数据库服务器。

接下来,我们将以 MySQL 数据库为例,展示如何使用 JDBC 接口完成与数据库相关的开发工作。

1 准备工作

1.1 引入 MySQL 驱动

1
2
3
4
5
6
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.49</version>
    <scope>runtime</scope>
</dependency>
  • 我们 MySQL 服务器版本是 5.7,因此这里选择与其最匹配的 JDBC 版本 5.x.x。
  • 这里添加依赖的 scoperuntime,因为编译 Java 程序并不需要 MySQL 的这个 jar 包,只有在运行期才需要使用。如果把 runtime 改成 compile,虽然也能正常编译,但是在 IDE 里写程序的时候,会多出来一大堆类似 com.mysql.jdbc.Connection 这样的类,非常容易与 Java 标准库的 JDBC 接口混淆,所以坚决不要设置为 compile

1.2 配置连接

同使用 IDE 连接数据库一样,我们需要事先准备好 URL、用户名和口令,才能成功连接到数据库。

URL 是由数据库厂商指定的格式,MySQL 的 URL 格式如下:

1
jdbc:mysql://<hostname>:<port>/<db>?key1=value1&key2=value2

假设数据库运行在本机 localhost,端口使用标准的 3306,数据库名称是 test_db,那么 URL 则为 jdbc:mysql://localhost:3306/test_db?useSSL=false&characterEncoding=utf8mb4

然后将 URL 注册到 DriverManager 连接数据库:

1
2
3
4
5
6
7
8
9
// JDBC 连接的 URL, 不同数据库有不同的格式:
String JDBC_URL = "jdbc:mysql://localhost:3306/test_db";
String JDBC_USER = "root";
String JDBC_PASSWORD = "password";
// 获取连接:
Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
// TODO: 访问数据库 ...
// 关闭连接:
conn.close();
  • Connection 接口,代表一个 JDBC 连接对象,它相当于 Java 程序到数据库的连接,具体的实现类由数据库的厂商实现。
  • 使用 DriverManager 类的静态方法,getConnection 可以获取数据库的连接。DriverManager会自动扫描 classpath,找到所有的 JDBC 驱动,然后根据我们传入的 URL 自动挑选一个合适的驱动。

因为 JDBC 连接是一种昂贵的资源,所以这里推荐使用 try (resource) 来自动释放 JDBC 连接:

1
2
3
try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {
    ...
}

2 查询数据 executeQuery

获取到 JDBC 连接后,下一步我们就可以查询数据库了。查询数据库分以下几步:

  • 第一步,通过 Connection 提供的 createStatement() 方法创建一个 Statement 对象,用于执行一个查询;
  • 第二步,执行 Statement 对象提供的 executeQuery("SELECT * FROM students") 并传入 SQL 语句,执行查询并获得返回的结果集,使用 ResultSet 来引用这个结果集;
  • 第三步,反复调用 ResultSetnext() 方法并读取每一行结果。

示例代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {
    try (Statement stmt = conn.createStatement()) {
        try (ResultSet rs = stmt.executeQuery("SELECT id, grade, name, gender FROM students WHERE gender=1")) {
            while (rs.next()) {
                long id = rs.getLong(1); // 注意:索引从 1 开始
                long grade = rs.getLong(2);
                String name = rs.getString(3);
                int gender = rs.getInt(4);
            }
        }
    }
}

注意要点:

  • StatmentResultSet 都是需要关闭的资源,因此嵌套使用 try (resource) 确保及时关闭;
  • rs.next() 用于判断是否有下一行记录,如果有,将自动把当前行移动到下一行(一开始获得 ResultSet 时当前行不是第一行);
  • ResultSet 获取列时,索引从 1 开始而不是 0
  • 必须根据 SELECT 的列的对应位置来调用 getLong(1)getString(2) 这些方法,否则对应位置的数据类型不对,将报错。

3 PreparedStatement 解决 SQL 注入问题

使用 Statement 拼字符串非常容易引发 SQL 注入的问题,这是因为 SQL 参数往往是从方法参数传入的。

我们来看一个例子:假设用户登录的验证方法如下:

1
2
3
4
5
User login(String name, String pass) {
    ...
    stmt.executeQuery("SELECT * FROM user WHERE login='" + name + "' AND pass='" + pass + "'");
    ...
}
  • 其中,参数 namepass 通常都是 Web 页面输入后由程序接收到的。

如果用户的输入是程序期待的值,就可以拼出正确的 SQL。例如:"bob""1234"

1
SELECT * FROM user WHERE login='bob' AND pass='1234'

但是,如果用户的输入是一个精心构造的字符串,就可以拼出意想不到的 SQL,这个 SQL 也是正确的,但它查询的条件不是程序设计的意图。例如:"bob' OR pass=", " OR pass='"

1
SELECT * FROM user WHERE login='bob' OR pass=' AND pass=' OR pass=''

这个 SQL 语句执行的时候,根本不用判断口令是否正确,这样一来,登录就形同虚设。

要避免 SQL 注入攻击,一个办法是针对所有字符串参数进行转义,但是转义很麻烦,而且需要在任何使用 SQL 的地方增加转义代码,此外还有个更好的办法是使用 PreparedStatement

PreparedStatement 可以完全避免 SQL 注入的问题,因为 PreparedStatement 始终使用 ? 作为占位符,并且把数据连同 SQL 本身传给数据库,这样可以保证每次传给数据库的 SQL 语句是相同的,只是占位符的数据不同,还能高效利用数据库本身对查询的缓存。上述登录 SQL 如果用 PreparedStatement 可以改写如下:

1
2
3
4
5
6
7
8
User login(String name, String pass) {
    ...
    String sql = "SELECT * FROM user WHERE login=? AND pass=?";
    PreparedStatement ps = conn.prepareStatement(sql);
    ps.setObject(1, name);
    ps.setObject(2, pass);
    ...
}

所以,PreparedStatementStatement 更安全,而且性能更高。

4 增删改数据 executeUpdate

4.1 插入数据

插入操作是INSERT,即插入一条新记录。通过 JDBC 进行插入,本质上也是用 PreparedStatement.executeUpdate() 执行一条 SQL 语句。示例代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {
    try (PreparedStatement ps = conn.prepareStatement(
            "INSERT INTO students (id, grade, name, gender) VALUES (?,?,?,?)")) {
        ps.setObject(1, null); // 注意:索引从 1 开始
        ps.setObject(2, 1); // grade
        ps.setObject(3, "Bob"); // name
        ps.setObject(4, "1"); // gender
        int n = ps.executeUpdate(); // 1
    }
}

设置参数与查询是一样的,有几个 ? 占位符就必须设置对应的参数。虽然 Statement 也可以执行插入操作,但无法避免 SQL 注入。当成功执行 executeUpdate() 后,返回值是 int,表示插入的记录数量。此处总是 1,因为只插入了一条记录。

4.2 插入并获取主键

如果数据库的表设置了自增主键,那么在执行 INSERT 语句时,并不需要指定主键,数据库会自动分配主键。对于使用自增主键的程序,有个额外的步骤,就是如何获取插入后的自增主键的值。

要获取自增主键,不能先插入再查询。因为两条 SQL 执行期间可能有别的程序也插入了同一个表。获取自增主键的正确写法是在创建 PreparedStatement 的时候,指定一个 RETURN_GENERATED_KEYS 标志位,表示 JDBC 驱动必须返回插入的自增主键。示例代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {
    try (PreparedStatement ps = conn.prepareStatement(
            "INSERT INTO students (grade, name, gender) VALUES (?,?,?)",
            Statement.RETURN_GENERATED_KEYS)) {
        ps.setObject(1, 1); // grade
        ps.setObject(2, "Bob"); // name
        ps.setObject(3, "1"); // gender
        int n = ps.executeUpdate(); // 1
        try (ResultSet rs = ps.getGeneratedKeys()) {
            if (rs.next()) {
                long id = rs.getLong(1); // 注意:索引从 1 开始
            }
        }
    }
}
  • 调用 prepareStatement() 时,第二个参数必须传入常量 Statement.RETURN_GENERATED_KEYS,否则 JDBC 驱动不会返回自增主键;
  • 执行 executeUpdate() 方法后,必须调用 getGeneratedKeys() 获取一个 ResultSet 对象,这个对象包含了数据库自动生成的主键的值,读取该对象的每一行来获取自增主键的值。如果一次插入多条记录,那么这个 ResultSet 对象就会有多行返回值。如果插入时有多列自增,那么 ResultSet 对象的每一行都会对应多个自增值(自增列不一定必须是主键)。

4.3 更新数据

更新操作是 UPDATE 语句,它可以一次更新若干列的记录。更新操作和插入操作除了 SQL 语句不同外,在 JDBC 代码的层面上实际上没有区别:

1
2
3
4
5
6
7
try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {
    try (PreparedStatement ps = conn.prepareStatement("UPDATE students SET name=? WHERE id=?")) {
        ps.setObject(1, "Bob"); // 注意:索引从 1 开始
        ps.setObject(2, 999);
        int n = ps.executeUpdate(); // 返回更新的行数
    }
}
  • executeUpdate() 返回数据库实际更新的行数。返回结果可能是正数,也可能是 0(表示没有任何记录更新)。

4.4 删除数据

删除操作是 DELETE 语句,它可以一次删除若干列。和更新一样,除了 SQL 语句不同外,JDBC 代码都是相同的:

1
2
3
4
5
6
try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {
    try (PreparedStatement ps = conn.prepareStatement("DELETE FROM students WHERE id=?")) {
        ps.setObject(1, 999); // 注意:索引从 1 开始
        int n = ps.executeUpdate(); // 删除的行数
    }
}

5 批量提交 executeBatch

使用 JDBC 操作数据库的时候,经常会执行一些批量操作。例如,一次性给会员增加可用优惠券若干,我们可以执行以下 SQL 代码:

1
2
3
4
INSERT INTO coupons (user_id, type, expires) VALUES (123, 'DISCOUNT', '2021-12-31');
INSERT INTO coupons (user_id, type, expires) VALUES (234, 'DISCOUNT', '2021-12-31');
INSERT INTO coupons (user_id, type, expires) VALUES (345, 'DISCOUNT', '2021-12-31');
INSERT INTO coupons (user_id, type, expires) VALUES (456, 'DISCOUNT', '2021-12-31');

事实上,在执行 JDBC 时,由于只有占位符参数不同,所以 SQL 实际上是相同的:

1
2
3
4
5
6
7
for (var params : paramsList) {
    PreparedStatement ps = conn.preparedStatement("INSERT INTO coupons (user_id, type, expires) VALUES (?,?,?)");
    ps.setLong(params.get(0));
    ps.setString(params.get(1));
    ps.setString(params.get(2));
    ps.executeUpdate();
}

通过一个循环来执行每个 PreparedStatement 虽然可行,但是性能很低。

实际上,对于 SQL 语句相同但只有参数不同的多个语句,可以作为批处理批量执行。这种操作经过特别优化,执行速度远远快于逐个执行每个 SQL 语句。示例代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
try (PreparedStatement ps = conn.prepareStatement("INSERT INTO students (name, gender, grade, score) VALUES (?, ?, ?, ?)")) {
    // 对同一个 PreparedStatement 反复设置参数并调用 addBatch():
    for (Student s : students) {
        ps.setString(1, s.name);
        ps.setBoolean(2, s.gender);
        ps.setInt(3, s.grade);
        ps.setInt(4, s.score);
        ps.addBatch(); // 添加到 batch
    }
    // 执行 batch:
    int[] ns = ps.executeBatch();
    for (int n : ns) {
        System.out.println(n + " inserted."); 
        // batch 中每个 SQL 执行的结果数量
    }
}

执行批处理需要反复为同一个 PreparedStatement 设置参数并调用 addBatch(),这样就相当于为一个 SQL 语句添加了多组参数,从而将其转化为多行 SQL。

另外,在批量提交时需要调用 executeBatch() 方法,由于我们设置了多组参数,相应地,返回的结果也是一个包含多个 int 值的数组,因此返回类型是 int[]。通过循环遍历 int[] 数组,您可以获取每组参数执行后影响的结果数量。

6 JDBC 事务

要在 JDBC 中执行事务,本质上就是如何把多条 SQL 包裹在一个数据库事务中执行。我们来看 JDBC 的事务代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Connection conn = openConnection();
try {
    // 关闭自动提交:
    conn.setAutoCommit(false);
    // 执行多条 SQL 语句:
    insert(); update(); delete();
    // 提交事务:
    conn.commit();
} catch (SQLException e) {
    // 回滚事务:
    conn.rollback();
} finally {
    conn.setAutoCommit(true);
    conn.close();
}
  • 开启事务的关键代码是 conn.setAutoCommit(false),表示关闭自动提交。提交事务的代码在执行完指定的若干条 SQL 语句后,调用 conn.commit()
  • 事务不可能 100% 提交成功,如果事务提交失败,会抛出 SQL 异常(也可能在执行 SQL 语句的时候就抛出了),此时我们需要捕获异常并调用 conn.rollback() 回滚事务。
  • 最后,在 finally 中通过 conn.setAutoCommit(true)Connection 对象的状态恢复到初始值。

6.1 隐式事务

默认情况下,当我们获取到 Connection 连接后,事务总是处于“自动提交”模式,这意味着每执行一条 SQL 语句都会自动提交为一个事务,这称为“隐式事务”。要关闭这种行为,需要使用 ConnectionsetAutoCommit() 方法来显式关闭自动提交。

6.2 设定隔离级别

如果要设定事务的隔离级别,可以使用如下代码:

1
2
// 设定隔离级别为 READ_COMMITTED:
conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);

如果没有调用上述方法,那么会使用数据库的默认隔离级别。InnoDB 的默认隔离级别是REPEATABLE READ

7 JDBC 连接池

JDBC 连接的创建是非常耗时的,从数据库这一侧看,能够建立的连接数也是有限的,所以在绝大多数场景中,我们都应该使用数据库连接池来缓存、复用数据库连接。使用池化技术缓存数据库连接会带来很多好处,例如:

  • 在空闲时段缓存一定数量的数据库连接备用,防止被突发流量冲垮;
  • 实现数据库连接的重用,从而提高系统的响应速度;
  • 控制数据库连接上限,防止连接过多造成数据库假死;
  • 统一管理数据库连接,避免连接泄漏。

数据库连接池在初始化时,一般会同时初始化特定数量的数据库连接,并缓存在连接池中备用。当我们需要操作数据库时,会从池中获取连接;当使用完一个连接的时候,会将其归还到池中缓存,等待下次使用。

数据库连接池中缓存的连接总量是有上限的,不仅如此,连接池中维护的空闲连接数也是有上限的,下面是使用数据库连接池时几种特殊场景的描述:

  • 如果连接池中维护的总连接数达到上限,且所有连接都已经被调用方占用,则后续获取数据库连接的线程将会被阻塞(进入阻塞队列中等待),直至连接池中出现可用的数据库连接,这个可用的连接是由其他使用方释放得到的。
  • 如果连接池中空闲连接数达到了配置上限,则后续返回到池中的空闲连接不会进入连接池缓存,而是直接关闭释放掉,这主要是为了减少维护空闲数据库连接带来的压力,同时减少数据库的资源开销。
  • 如果将连接总数的上限值设置得过大,可能会导致数据库因连接过多而僵死或崩溃,影响整个服务的可用性;而如果设置得过小,可能会无法完全发挥出数据库的性能,造成数据库资源的浪费。
  • 如果将空闲连接数的上限值设置得过大,可能会造成服务资源以及数据库资源的浪费,毕竟要维护这些空闲连接;如果设置得过小,当出现瞬间峰值请求时,服务的响应速度就会比较慢。

因此,在设置数据库连接池的最大连接数以及最大空闲连接数时,需要进行折中和权衡,当然也要执行一些性能测试来辅助我们判断。

JDBC 连接池有一个标准的接口 javax.sql.DataSource,这个类位于 Java 标准库中,但仅仅是接口。要使用 JDBC 连接池,我们必须选择一个 JDBC 连接池的实现。常用的 JDBC 连接池有:

  • DBCP 连接池
  • C3P0 连接池
  • Druid 连接池

7.1 DBCP 连接池

DBCP 是 tomcat 内置的开源连接池。

7.1.1 使用方法

1)引入依赖

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<dependency>
    <groupId>commons-dbcp</groupId>
    <artifactId>commons-dbcp</artifactId>
    <version>1.4</version>
</dependency>
<dependency>
    <groupId>commons-pool</groupId>
    <artifactId>commons-pool</artifactId>
    <version>1.6</version>
</dependency>

2)编写工具类

连接数据库表的工具类,采用 DBCP 连接池的方式来完成

  • Java 中提供了一个连接池的规则接口: DataSource ,它是 Java 中提供的连接池规范;
  • 在 DBCP 包中提供了 DataSource 接口的实现类 BasicDataSource

代码示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class DBCPUtils {
    //1. 定义常量 保存数据库连接的相关信息
    public static final String DRIVERNAME = "com.mysql.jdbc.Driver";
    public static final String URL = "jdbc:mysql://localhost:3306/test_db?characterEncoding=UTF-8";
    public static final String USERNAME = "root";
    public static final String PASSWORD = "123456";
    //2. 创建连接池对象 (有 DBCP 提供的实现类)
    public static BasicDataSource dataSource = new BasicDataSource();

    //3. 使用静态代码块进行配置
    static {
        dataSource.setDriverClassName(DRIVERNAME);
        dataSource.setUrl(URL);
        dataSource.setUsername(USERNAME);
        dataSource.setPassword(PASSWORD);
    }

    //4. 获取连接的方法
    public static Connection getConnection() throws SQLException {
        //从连接池中获取连接
        Connection connection = dataSource.getConnection();
        return connection;
    }

    //5. 释放资源方法
    public static void close(Connection con, Statement statement) {
        if (con != null && statement != null) {
            try {
                statement.close();
                //归还连接
                con.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
    public static void close(Connection con, Statement statement, ResultSet resultSet) {
        if (con != null && statement != null && resultSet != null) {
            try {
                resultSet.close();
                statement.close();
                //归还连接
                con.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

2)使用示例

1
2
3
4
5
// 从池中获取连接:
Connection connection = DBCPUtils.getConnection();
// TODO: 访问数据库 ...
// 归还连接
DBCPUtils.close(connection, ps);

7.1.2 BasicDataSource 常见配置项

属性描述
driverClassName数据库驱动名称
url数据库地址
username用户名
password密码
maxActive最大连接数量
maxIdle最大空闲连接
minIdle最小空闲连接
initialSize初始化连接

7.2 C3P0 连接池

C3P0 是一个开源的 JDBC 连接池,支持 JDBC3 规范和 JDBC2 的标准扩展。目前使用它的开源项目有 Hibernate、Spring 等。

7.2.1 使用方法

1)引入依赖

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<dependency>
    <groupId>com.mchange</groupId>
    <artifactId>c3p0</artifactId>
    <version>0.9.5.4</version>
</dependency>
<dependency>
    <groupId>com.mchange</groupId>
    <artifactId>mchange-commons-java</artifactId>
    <version>0.2.19</version>
</dependency>

2)导入配置文件 c3p0-config.xml

注意: c3p0-config.xml 文件名不可更改

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<c3p0-config>
    <!--默认配置-->
    <default-config>
        <property name="driverClass">com.mysql.jdbc.Driver</property>
        <property name="jdbcUrl">jdbc:mysql://localhost:3306/test_db?characterEncoding=UTF-8</property>
        <property name="user">root</property>
        <property name="password">123456</property>
        <!-- initialPoolSize:初始化时获取三个连接,
取值应在 minPoolSize 与 maxPoolSize 之间。 -->
        <property name="initialPoolSize">3</property>
        <!-- maxIdleTime:最大空闲时间,60 秒内未使用则连接被丢弃。若为 0 则永不丢弃。-->
        <property name="maxIdleTime">60</property>
        <!-- maxPoolSize:连接池中保留的最大连接数 -->
        <property name="maxPoolSize">100</property>
        <!-- minPoolSize: 连接池中保留的最小连接数 -->
        <property name="minPoolSize">10</property>
    </default-config>
    <!--配置连接池 mysql1-->
    <named-config name="mysql-1">
        <property name="driverClass">com.mysql.jdbc.Driver</property>
        <property name="jdbcUrl">jdbc:mysql://localhost:3306/test_db</property>
        <property name="user">root</property>
        <property name="password">123456</property>
        <property name="initialPoolSize">10</property>
        <property name="maxIdleTime">30</property>
        <property name="maxPoolSize">100</property>
        <property name="minPoolSize">10</property>
    </named-config>
    <!--配置连接池 2, 可以配置多个-->
</c3p0-config>

3)编写 C3P0 工具类

  • C3P0 提供的核心工具类,ComboPooledDataSource,如果想使用连接池,就必须创建该类的对象:
    • new ComboPooledDataSource() 使用默认配置;
    • new ComboPooledDataSource("mysql-1") 使用命名配置,名称是在 c3p0-config.xml 中配置的。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class C3P0Utils {
    //使用指定的配置创建连接池对象 C3P0 对 DataSource 接口的实现类
    public static ComboPooledDataSource dataSource = new ComboPooledDataSource("mysql-1");

    //获取连接的方法
    public static Connection getConnection() throws SQLException {
        return dataSource.getConnection();
    }

    //释放资源
    public static void close(Connection con, Statement statement) {
        if (con != null && statement != null) {
            try {
                statement.close();
                //归还连接
                con.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
    public static void close(Connection con, Statement statement, ResultSet resultSet) {
        if (con != null && statement != null && resultSet != null) {
            try {
                resultSet.close();
                statement.close();
                //归还连接
                con.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

使用方法同 DBCP 连接池,这里省略。

7.2.2 ComboPooledDataSource 常见配置

必填项:

属性描述
user用户名
password密码
driverClass驱动
jdbcUrl路径

基本配置:

属性描述
initialPoolSize连接池初始化时创建的连接数
默认值:3
maxPoolSize连接池允许的最大连接数
默认值:15
minPoolSize连接池保持的早小连接数
默认值:10
maxIdleTime连接的最大空闲时间。如果超过这个时间,某个数据库的连接还没被使用,则断开这个连接。
如果设置为 0,则永不超时
默认值:0

7.3 Druid 连接池

Druid(德鲁伊)是阿里巴巴开发的号称为监控而生的数据库连接池,Druid 是目前最好的数据库连接池。在功能、性能、扩展性方面,都超过其他数据库连接池,同时加入了日志监控,可以很好的监控 DB 池连接和 SQL 的执行情况。

1)引入依赖

1
2
3
4
5
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.0.0</version>
</dependency>

2)导入配置文件

Druid 使用 properties 形式的配置文件:

1
2
3
4
5
6
7
driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/test_db?characterEncoding=UTF-8
username=root
password=123456
initialSize=5
maxActive=10
maxWait=3000

3)编写 Druid 工具类

Druid 可以通过工厂 DruidDataSourceFactory 类的 createDataSource 方法获取连接池,该方法参数可以是一个属性集对象:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class DruidUtils {
    //1. 定义成员变量
    public static DataSource dataSource;

    //2. 静态代码块
    static {
        try {
            //3. 创建属性集对象
            Properties p = new Properties();
            //4. 加载配置文件 Druid 连接池不能够主动加载配置文件 , 需要指定文件
            InputStream inputStream = DruidUtils.class.getClassLoader().getResourceAsStream("druid.properties");
            //5. 使用 Properties 对象的 load 方法 从字节流中读取配置信息
            p.load(inputStream);
            //6. 通过工厂类获取连接池对象
            dataSource = DruidDataSourceFactory.createDataSource(p);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    //获取连接的方法
    public static Connection getConnection() {
        try {
            return dataSource.getConnection();
        } catch (SQLException e) {
            e.printStackTrace();
            return null;
        }
    }

    //释放资源
    public static void close(Connection con, Statement statement) {
        if (con != null && statement != null) {
            try {
                statement.close();
                //归还连接
                con.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
    public static void close(Connection con, Statement statement, ResultSet resultSet) {
        if (con != null && statement != null && resultSet != null) {
            try {
                resultSet.close();
                statement.close();
                //归还连接
                con.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

使用方法同 DBCP 连接池,这里省略。


欢迎关注我的公众号,第一时间获取文章更新:

微信公众号

相关内容