一、前置准备(统一基础环境)
1. MySQL 表结构(不变)
sql
CREATE TABLE `user` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL, `age` INT NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8;2. 通用依赖(新增 C3P0 依赖)
xml
<!-- MySQL驱动(三者共用) --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <!-- DbUtils依赖 --> <dependency> <groupId>commons-dbutils</groupId> <artifactId>commons-dbutils</artifactId> <version>1.7</version> </dependency> <!-- MyBatis依赖 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.13</version> </dependency> <!-- C3P0数据源核心依赖 --> <dependency> <groupId>com.mchange</groupId> <artifactId>c3p0</artifactId> <version>0.9.5.5</version> </dependency> <!-- 日志(MyBatis调试用) --> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.4.8</version> </dependency>3. 通用实体类(不变)
java
运行
public class User { private Integer id; private String username; private Integer age; // 无参构造(MyBatis/DbUtils必需) public User() {} // 有参构造 public User(String username, Integer age) { this.username = username; this.age = age; } // getter/setter public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } // toString @Override public String toString() { return "User{" + "id=" + id + ", username='" + username + '\'' + ", age=" + age + '}'; } }4. C3P0 核心 XML 配置文件(c3p0-config.xml)
放置在resources根目录下(C3P0 默认读取该路径的配置文件):
xml
<?xml version="1.0" encoding="UTF-8"?> <c3p0-config> <!-- 默认配置(DbUtils/MyBatis共用) --> <default-config> <!-- 数据库连接基本信息 --> <property name="driverClass">com.mysql.cj.jdbc.Driver</property> <property name="jdbcUrl">jdbc:mysql://localhost:3306/test_db?useSSL=false&serverTimezone=UTC</property> <property name="user">root</property> <property name="password">123456</property> <!-- 连接池核心配置 --> <property name="initialPoolSize">5</property> <!-- 初始连接数 --> <property name="maxPoolSize">20</property> <!-- 最大连接数 --> <property name="minPoolSize">5</property> <!-- 最小连接数 --> <property name="maxIdleTime">300</property> <!-- 连接最大空闲时间(秒) --> <property name="acquireIncrement">2</property> <!-- 连接不足时的增量 --> <property name="checkoutTimeout">3000</property> <!-- 获取连接超时时间(毫秒) --> </default-config> <!-- 自定义配置(可选,可针对不同环境配置) --> <named-config name="custom-c3p0-config"> <property name="driverClass">com.mysql.cj.jdbc.Driver</property> <property name="jdbcUrl">jdbc:mysql://localhost:3306/test_db?useSSL=false&serverTimezone=UTC</property> <property name="user">root</property> <property name="password">123456</property> <property name="initialPoolSize">10</property> <property name="maxPoolSize">30</property> </named-config> </c3p0-config>二、Demo 改造(DbUtils + MyBatis 适配 C3P0 XML 配置)
Demo1:原生 JDBC 实现(不变,作为对比基准)
java
运行
import java.sql.*; public class JdbcDemo { // 数据库连接配置 private static final String URL = "jdbc:mysql://localhost:3306/test_db?useSSL=false&serverTimezone=UTC"; private static final String USER = "root"; private static final String PWD = "123456"; // 根据ID查询用户 public static User getUserById(Integer id) { Connection conn = null; PreparedStatement pstmt = null; ResultSet rs = null; User user = null; try { // 1. 获取连接 conn = DriverManager.getConnection(URL, USER, PWD); // 2. 创建预编译语句 String sql = "SELECT id, username, age FROM user WHERE id = ?"; pstmt = conn.prepareStatement(sql); pstmt.setInt(1, id); // 3. 执行查询 rs = pstmt.executeQuery(); // 4. 手动映射结果集 if (rs.next()) { user = new User(); user.setId(rs.getInt("id")); user.setUsername(rs.getString("username")); user.setAge(rs.getInt("age")); } } catch (SQLException e) { e.printStackTrace(); } finally { // 5. 手动关闭资源(先开后关) try { if (rs != null) rs.close(); } catch (SQLException e) {} try { if (pstmt != null) pstmt.close(); } catch (SQLException e) {} try { if (conn != null) conn.close(); } catch (SQLException e) {} } return user; } // 插入用户(手动事务) public static boolean insertUser(User user) { Connection conn = null; PreparedStatement pstmt = null; try { // 1. 获取连接 conn = DriverManager.getConnection(URL, USER, PWD); // 2. 关闭自动提交(开启事务) conn.setAutoCommit(false); // 3. 创建预编译语句 String sql = "INSERT INTO user(username, age) VALUES (?, ?)"; pstmt = conn.prepareStatement(sql); pstmt.setString(1, user.getUsername()); pstmt.setInt(2, user.getAge()); // 4. 执行插入 int rows = pstmt.executeUpdate(); // 5. 提交事务 conn.commit(); return rows > 0; } catch (SQLException e) { // 6. 异常回滚 try { if (conn != null) conn.rollback(); } catch (SQLException ex) {} e.printStackTrace(); return false; } finally { // 7. 关闭资源 try { if (pstmt != null) pstmt.close(); } catch (SQLException e) {} try { if (conn != null) conn.close(); } catch (SQLException e) {} } } public static void main(String[] args) { // 查询测试 User user = getUserById(1); System.out.println("JDBC查询结果:" + user); // 插入测试 User newUser = new User("test_jdbc", 25); boolean success = insertUser(newUser); System.out.println("JDBC插入结果:" + success); } }Demo2:DbUtils 实现(C3P0 XML 数据源版)
java
运行
import org.apache.commons.dbutils.DbUtils; import org.apache.commons.dbutils.QueryRunner; import org.apache.commons.dbutils.handlers.BeanHandler; import com.mchange.v2.c3p0.ComboPooledDataSource; import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; public class DbUtilsDemo { // 读取C3P0 XML配置(默认配置) private static DataSource getC3p0DataSource() { // ComboPooledDataSource默认读取resources/c3p0-config.xml的default-config // 若要读取自定义配置:new ComboPooledDataSource("custom-c3p0-config") return new ComboPooledDataSource(); } // 根据ID查询用户(自动资源关闭 + 自动结果映射) public static User getUserById(Integer id) throws SQLException { // 1. 创建QueryRunner(传入C3P0数据源) QueryRunner qr = new QueryRunner(getC3p0DataSource()); // 2. 执行查询(BeanHandler自动映射为User) String sql = "SELECT id, username, age FROM user WHERE id = ?"; return qr.query(sql, new BeanHandler<>(User.class), id); } // 插入用户(手动事务) public static boolean insertUser(User user) { Connection conn = null; try { // 1. 从C3P0数据源手动获取连接(事务需要) conn = getC3p0DataSource().getConnection(); conn.setAutoCommit(false); // 2. 创建无参QueryRunner(手动控制连接) QueryRunner qr = new QueryRunner(); String sql = "INSERT INTO user(username, age) VALUES (?, ?)"; // 3. 执行插入 int rows = qr.update(conn, sql, user.getUsername(), user.getAge()); // 4. 提交事务 conn.commit(); return rows > 0; } catch (SQLException e) { // 5. 回滚 + 安静关闭 DbUtils.rollbackAndClose(conn); e.printStackTrace(); return false; } finally { // 6. 安静关闭连接 DbUtils.closeQuietly(conn); } } public static void main(String[] args) throws SQLException { // 查询测试 User user = getUserById(1); System.out.println("DbUtils查询结果:" + user); // 插入测试 User newUser = new User("test_dbutils_c3p0", 26); boolean success = insertUser(newUser); System.out.println("DbUtils插入结果:" + success); } }Demo3:MyBatis 实现(C3P0 XML 数据源版)
核心改造:MyBatis 配置文件中替换为 C3P0 数据源,读取 XML 配置
步骤 1:MyBatis 核心配置文件(mybatis-config.xml)
xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "https://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <!-- 环境配置:整合C3P0数据源 --> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <!-- 事务管理复用JDBC --> <!-- 替换为C3P0数据源(type="POOLED"改为自定义C3P0实现) --> <dataSource type="com.example.mybatis.C3P0DataSourceFactory"> <!-- 无需硬编码配置,C3P0内部读取XML --> <property name="configName" value="default-config"/> <!-- 指定C3P0配置名称 --> </dataSource> </environment> </environments> <!-- 映射器配置 --> <mappers> <mapper resource="UserMapper.xml"/> </mappers> </configuration>步骤 2:MyBatis 整合 C3P0 的数据源工厂类
MyBatis 的DataSourceFactory接口实现,用于加载 C3P0 XML 配置:
java
运行
import org.apache.ibatis.datasource.DataSourceFactory; import com.mchange.v2.c3p0.ComboPooledDataSource; import javax.sql.DataSource; import java.util.Properties; public class C3P0DataSourceFactory implements DataSourceFactory { private Properties props; private ComboPooledDataSource dataSource; public C3P0DataSourceFactory() { this.dataSource = new ComboPooledDataSource(); } @Override public void setProperties(Properties props) { this.props = props; // 读取MyBatis配置中指定的C3P0配置名称(default-config/custom-c3p0-config) String configName = props.getProperty("configName", "default-config"); // 重新初始化C3P0数据源,读取指定名称的XML配置 this.dataSource = new ComboPooledDataSource(configName); } @Override public DataSource getDataSource() { return this.dataSource; } }步骤 3:Mapper 映射文件(UserMapper.xml,不变)
xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "https://mybatis.org/dtd/mybatis-3-mapper.dtd"> <!-- 命名空间对应Mapper接口 --> <mapper namespace="com.example.mapper.UserMapper"> <!-- 根据ID查询用户(自动映射) --> <select id="getUserById" parameterType="java.lang.Integer" resultType="com.example.entity.User"> SELECT id, username, age FROM user WHERE id = #{id} </select> <!-- 插入用户 --> <insert id="insertUser" parameterType="com.example.entity.User"> INSERT INTO user(username, age) VALUES (#{username}, #{age}) </insert> </mapper>步骤 4:Mapper 接口(不变)
java
运行
package com.example.mapper; import com.example.entity.User; public interface UserMapper { // 根据ID查询用户 User getUserById(Integer id); // 插入用户 int insertUser(User user); }步骤 5:MyBatis 核心实现类
java
运行
import com.example.entity.User; import com.example.mapper.UserMapper; import org.apache.ibatis.io.Resources; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import java.io.IOException; import java.io.InputStream; public class MyBatisDemo { // 构建SqlSessionFactory(全局唯一) private static SqlSessionFactory getSqlSessionFactory() throws IOException { String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); return new SqlSessionFactoryBuilder().build(inputStream); } // 根据ID查询用户 public static User getUserById(Integer id) throws IOException { // 1. 获取SqlSession(自动管理C3P0连接) try (SqlSession session = getSqlSessionFactory().openSession(true)) { // true=自动提交 // 2. 获取Mapper代理对象 UserMapper mapper = session.getMapper(UserMapper.class); // 3. 调用方法(MyBatis自动执行SQL+映射结果) return mapper.getUserById(id); } } // 插入用户(手动事务) public static boolean insertUser(User user) throws IOException { // 1. 打开SqlSession(关闭自动提交) try (SqlSession session = getSqlSessionFactory().openSession(false)) { UserMapper mapper = session.getMapper(UserMapper.class); // 2. 执行插入 int rows = mapper.insertUser(user); // 3. 提交事务 session.commit(); return rows > 0; } catch (Exception e) { e.printStackTrace(); return false; } } public static void main(String[] args) throws IOException { // 查询测试 User user = getUserById(1); System.out.println("MyBatis查询结果:" + user); // 插入测试 User newUser = new User("test_mybatis_c3p0", 27); boolean success = insertUser(newUser); System.out.println("MyBatis插入结果:" + success); } }三、深度问答解析
问题 1:DbUtils 整合 C3P0 XML 配置的核心逻辑是什么?和硬编码数据源有什么区别?
引导思考:C3P0 如何读取 XML 配置?XML 配置的优势是什么?核心解答:
- 核心逻辑:
- C3P0 的
ComboPooledDataSource默认会扫描resources根目录下的c3p0-config.xml; - 无参构造
new ComboPooledDataSource()读取<default-config>,带参构造new ComboPooledDataSource("custom-c3p0-config")读取指定<named-config>; - DbUtils 的
QueryRunner只需传入 C3P0 数据源实例,即可自动从连接池获取 / 归还连接,无需手动管理连接创建。
- C3P0 的
- XML 配置 vs 硬编码的优势:
- 解耦:数据库配置与代码分离,修改配置无需重新编译代码;
- 多环境适配:可配置
<named-config>分别对应开发 / 测试 / 生产环境,仅需修改构造参数即可切换; - 连接池参数集中管理:连接池的初始连接数、最大连接数等参数统一配置,便于调优。
问题 2:MyBatis 整合 C3P0 为什么需要自定义DataSourceFactory?核心作用是什么?
引导思考:MyBatis 默认的POOLED数据源是内置实现,如何适配第三方数据源(如 C3P0)?核心解答:
- 底层原因:MyBatis 的
<dataSource>标签默认只支持UNPOOLED/POOLED/JNDI三种类型,无法直接识别 C3P0,因此需要实现DataSourceFactory接口适配第三方数据源; - 自定义
C3P0DataSourceFactory的核心作用:- 实现
setProperties()方法,接收 MyBatis 配置中传递的参数(如 C3P0 配置名称); - 初始化 C3P0 数据源实例,读取 XML 配置文件;
- 实现
getDataSource()方法,向 MyBatis 返回 C3P0 数据源实例,让 MyBatis 通过 C3P0 连接池管理连接。
- 实现
问题 3:使用 C3P0 连接池后,DbUtils/MyBatis 的连接管理和原生 JDBC 有什么本质差异?
引导思考:原生 JDBC 每次DriverManager.getConnection()都是新建物理连接,C3P0 连接池的 “连接复用” 如何实现?核心解答:
| 维度 | 原生 JDBC | DbUtils+C3P0/MyBatis+C3P0 |
|---|---|---|
| 连接创建方式 | 每次调用getConnection()新建 TCP 物理连接 | 从 C3P0 连接池获取空闲连接(复用),无需新建物理连接 |
| 连接销毁方式 | 调用close()关闭物理连接(TCP 断开) | 调用close()将连接归还连接池(物理连接不关闭) |
| 性能 | 高开销(TCP 连接 / 断开耗时) | 低开销(连接复用,仅管理连接状态) |
| 资源管控 | 无限制创建连接,易导致数据库连接数超限 | 连接池限制最大连接数,避免资源耗尽 |
问题 4:C3P0 的 XML 配置中maxIdleTime/checkoutTimeout等参数的核心作用是什么?对业务有什么影响?
引导思考:连接池参数配置不当会导致什么问题(如连接泄漏、获取连接超时)?
核心解答:
maxIdleTime(连接最大空闲时间):空闲连接超过该时间会被 C3P0 自动关闭,释放数据库资源,避免连接长期空闲占用资源;checkoutTimeout(获取连接超时时间):当连接池无空闲连接时,等待该时间仍未获取到连接则抛出异常,避免业务线程无限等待;maxPoolSize(最大连接数):限制连接池的最大物理连接数,需根据数据库的max_connections参数配置(如数据库最大连接数为 100,则 C3P0 的maxPoolSize建议配置为 80-90),避免超出数据库承载能力。
问题 5:DbUtils 和 MyBatis 使用 C3P0 的核心注意事项是什么?
核心解答:
- DbUtils 注意事项:
- 事务场景下,手动获取的连接需确保
commit()/rollback()后调用close()归还连接池,避免连接泄漏; - 避免长时间占用连接(如执行业务逻辑时持有连接),否则会导致连接池无空闲连接,其他请求阻塞。
- 事务场景下,手动获取的连接需确保
- MyBatis 注意事项:
SqlSession关闭时会自动将连接归还 C3P0,需确保SqlSession在try-with-resources中自动关闭,或手动调用close();- 避免
SqlSession长时间存活(如全局单例SqlSession),否则会导致连接长期被占用,连接池耗尽。
四、总结:C3P0 XML 配置核心价值
- 解耦配置与代码:数据库连接信息、连接池参数通过 XML 配置,无需硬编码,便于维护和多环境切换;
- 连接复用提升性能:C3P0 连接池复用物理连接,避免原生 JDBC 频繁创建 / 关闭连接的性能损耗;
- 资源管控更安全:连接池限制最大连接数、空闲时间等参数,避免数据库连接数超限导致的服务不可用;
- 框架适配性强:DbUtils 可直接复用 C3P0 数据源,MyBatis 通过自定义
DataSourceFactory适配后,也能无缝使用 C3P0 的连接池能力。
从原生 JDBC → DbUtils+C3P0 → MyBatis+C3P0,核心演进逻辑是:从 “手动管理所有细节” 到 “框架封装细节 + 连接池优化性能 + 配置解耦提升可维护性”,这也是企业级开发中数据库操作的主流实践方式。