在Java企业级开发中,JDBC(Java Database Connectivity)是Java程序与关系型数据库(MySQL、Oracle、SQL Server等)交互的基础规范,也是所有ORM框架(MyBatis、Hibernate)的底层核心。无论是简单的单表查询,还是复杂的多表联查、事务控制,最终都会通过JDBC体系将SQL语句传递到底层数据库并执行。
很多开发者在使用JDBC时,往往只停留在"CRUD代码模板"的层面,对SQL从"Java代码编写"到"数据库执行返回"的完整链路理解不深,导致遇到性能瓶颈、事务异常、连接泄漏等问题时无从下手。
本文将从JDBC核心架构出发,逐步骤拆解SQL处理的完整流程,结合企业开发中的真实场景,同时补充流程中的优化技巧与常见问题排查方案,帮助开发者从底层吃透JDBC,写出高效、健壮的数据库交互代码。
🌟**【青柠代码录】--- Java全栈成长加速器** 🌟
🔥博客合集: https://www.yuque.com/u12587869/zplytb/ur5ohwqxd2axtiny 🔥
一、JDBC核心架构铺垫
在分析SQL处理流程前,我们先明确JDBC的核心组件及职责------JDBC本质是"一套接口规范",由Java官方定义,数据库厂商(如MySQL、Oracle)提供实现(即数据库驱动Jar包),Java程序通过调用这套接口,间接与数据库交互,实现"跨数据库兼容"(理论上更换驱动包即可切换数据库)。
1.1 核心组件(SQL处理流程的关键参与者)
JDBC体系中,参与SQL处理全流程的核心组件共5个,每个组件的职责的都不可替代,企业开发中必须熟练掌握:
-
Driver(驱动):数据库厂商实现的JDBC接口,负责与数据库建立底层连接(如MySQL的com.mysql.cj.jdbc.Driver),是Java程序与数据库通信的"桥梁"。
-
DriverManager(驱动管理器):Java官方提供的驱动管理工具,负责加载驱动、管理数据库连接池(简单场景)、获取数据库连接(Connection)。
-
Connection(数据库连接):Java程序与数据库之间的"会话通道",所有SQL操作都必须基于Connection完成,负责事务控制(提交、回滚)、创建执行SQL的Statement对象。
-
Statement(SQL执行器):用于将SQL语句发送到数据库并执行,分为3种实现(企业开发中用法有明确区别):
-
Statement:基础执行器,直接执行静态SQL,存在SQL注入风险,企业开发中禁止使用。
-
PreparedStatement:预编译执行器,先将SQL模板发送到数据库预编译,参数通过占位符(?)传入,避免SQL注入,支持批处理,是企业开发中最常用的执行器。
-
CallableStatement:用于执行数据库存储过程/函数,适用于复杂业务逻辑下沉到数据库的场景。
- ResultSet(结果集):用于接收SQL查询(SELECT)的返回结果,提供遍历、获取字段值的方法(如getString()、getInt()),需注意及时关闭,避免资源泄漏。
1.2 核心架构图示
为了直观理解各组件在SQL处理流程中的交互关系,以下是JDBC体系核心架构与SQL处理链路的简化图示:
图示说明:Java程序 → DriverManager(加载驱动) → Connection(建立连接) → Statement(执行SQL) → 数据库(执行SQL并返回结果) → ResultSet(接收结果) → Java程序(处理结果);全程需通过Driver驱动实现底层通信。
1.3 开发环境准备
后续所有代码示例均基于「MySQL 8.0 + JDK 11 + Maven」环境,需提前引入MySQL驱动依赖,Maven依赖如下(可直接复制到pom.xml):
<!-- MySQL 8.0 JDBC驱动(企业开发主流版本) -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.36</version>
<!-- 排除不必要的依赖,避免冲突 -->
<exclusions>
<exclusion>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 可选:引入连接池(企业开发必用,后续流程优化会用到) -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.20</version>
</dependency>
二、JDBC体系中SQL处理全流程(核心重点,分步拆解)
JDBC处理SQL的完整流程,本质是"Java程序通过JDBC组件与数据库建立通信,发送SQL指令,接收执行结果并处理"的过程,共分为7个核心步骤。
我们以「用户查询+新增」场景为例,逐步骤拆解流程。
步骤1:加载数据库驱动(Driver)
核心目的:将数据库厂商提供的Driver实现类加载到JVM中,让DriverManager能够识别并使用该驱动,建立与数据库的底层连接。
1.1 加载方式(2种常用方式)
方式1:Class.forName() 反射加载(推荐,解耦性强,可通过配置文件动态指定驱动类)
方式2:DriverManager.registerDriver() 直接注册(不推荐,会导致驱动被注册2次,且耦合驱动类)
1.2 代码示例
import java.sql.DriverManager;
import java.sql.SQLException;
/**
* 步骤1:加载MySQL驱动(企业开发实战写法)
* 注意:MySQL 8.0 驱动类名是 com.mysql.cj.jdbc.Driver(5.7及以下是 com.mysql.jdbc.Driver)
*/
public class JdbcSqlProcessDemo {
// 数据库连接参数(企业开发中会放到配置文件,如application.properties,此处为了演示简化)
private static final String DRIVER_CLASS = "com.mysql.cj.jdbc.Driver";
private static final String URL = "jdbc:mysql://localhost:3306/enterprise_db?useSSL=false&serverTimezone=UTC&characterEncoding=utf8";
private static final String USERNAME = "root";
private static final String PASSWORD = "123456";
public static void main(String[] args) {
try {
// 方式1:反射加载驱动(推荐),加载后Driver类会自动向DriverManager注册
Class.forName(DRIVER_CLASS);
System.out.println("✅ MySQL驱动加载成功");
// 方式2:直接注册驱动(不推荐,存在冗余,且耦合驱动类)
// DriverManager.registerDriver(new com.mysql.cj.jdbc.Driver());
} catch (ClassNotFoundException e) {
// 驱动加载失败(常见原因:驱动依赖未引入、驱动类名写错、JAR包冲突)
System.err.println("❌ MySQL驱动加载失败,请检查驱动依赖或驱动类名");
e.printStackTrace();
throw new RuntimeException("驱动加载异常", e); // 企业开发中需抛出运行时异常,终止程序(无驱动无法继续)
}
}
}
1.3 注意事项
-
MySQL 8.0 与 5.7 驱动类名不同,且URL必须指定serverTimezone(时区),否则会报时区异常。
-
驱动加载只需执行1次,企业开发中通常在项目启动时加载(如SpringBoot的启动类、自定义初始化类),避免重复加载。
-
若使用连接池(如Druid、HikariCP),连接池会自动加载驱动,无需手动写Class.forName()。
步骤2:通过DriverManager获取数据库连接(Connection)
核心目的:建立Java程序与数据库之间的"会话通道",Connection是JDBC的核心对象,所有SQL操作(执行、事务)都必须基于该对象,连接是稀缺资源,必须及时关闭。
2.1 核心API
DriverManager.getConnection(String url, String username, String password):通过URL、用户名、密码获取Connection对象。
2.2 代码示例
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class JdbcSqlProcessDemo {
// 省略数据库连接参数(同步骤1)...
public static void main(String[] args) {
Connection connection = null; // 声明Connection,放在try外部,方便finally关闭
try {
// 步骤1:加载驱动
Class.forName(DRIVER_CLASS);
System.out.println("✅ MySQL驱动加载成功");
// 步骤2:获取数据库连接(核心步骤)
connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
// 验证连接是否有效(企业开发中可选,用于快速排查连接问题)
if (connection != null && !connection.isClosed()) {
System.out.println("✅ 数据库连接建立成功,连接对象:" + connection);
}
} catch (ClassNotFoundException e) {
System.err.println("❌ MySQL驱动加载失败");
e.printStackTrace();
throw new RuntimeException("驱动加载异常", e);
} catch (SQLException e) {
// 连接建立失败(常见原因:URL错误、用户名密码错误、数据库未启动、端口号错误)
System.err.println("❌ 数据库连接建立失败,请检查连接参数或数据库状态");
e.printStackTrace();
throw new RuntimeException("连接建立异常", e);
} finally {
// 注意:此处先不关闭连接,后续执行SQL还需要使用,后续步骤会完善关闭逻辑
// 核心原则:资源(Connection、Statement、ResultSet)使用后必须关闭,且关闭顺序是 ResultSet → Statement → Connection
}
}
}
2.3 注意事项
-
Connection是稀缺资源,默认情况下,DriverManager每次getConnection()都会创建一个新的物理连接,频繁创建/关闭连接会严重影响系统性能(企业开发中禁止直接使用DriverManager获取连接,必须用连接池)。
-
连接参数URL的规范:jdbc:数据库类型://主机地址:端口号/数据库名?参数1&参数2,MySQL 8.0 必须指定serverTimezone(如UTC、Asia/Shanghai)。
-
Connection的isClosed()方法可用于验证连接是否有效,企业开发中可用于连接池的连接校验。
步骤3:创建SQL执行器(Statement/PreparedStatement)
核心目的:创建用于发送SQL语句到数据库的"执行器",根据SQL类型(静态、动态、存储过程)选择对应的执行器,企业开发中优先使用PreparedStatement,杜绝使用Statement。
3.1 3种执行器对比(选型依据)
| 执行器类型 | 适用场景 | 优点 | 缺点 | 企业开发推荐度 |
|---|---|---|---|---|
| Statement | 静态SQL(无参数),简单查询 | 写法简单,无需预编译 | 存在SQL注入风险,无法预编译优化,不支持批处理 | ❌ 禁止使用 |
| PreparedStatement | 动态SQL(有参数)、CRUD、批处理 | 预编译优化、避免SQL注入、支持批处理、可重复执行 | 写法稍复杂,需设置参数 | ✅ 强烈推荐(99%场景使用) |
| CallableStatement | 执行数据库存储过程/函数 | 专门适配存储过程,支持输入/输出参数 | 耦合数据库,移植性差,写法复杂 | ⚠️ 按需使用(仅存储过程场景) |
3.2 代码示例(PreparedStatement)
场景:查询指定ID的用户信息(SELECT语句)、新增用户(INSERT语句),演示PreparedStatement的使用(占位符、参数设置、执行)。
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class JdbcSqlProcessDemo {
// 省略数据库连接参数(同步骤1)...
public static void main(String[] args) {
Connection connection = null;
PreparedStatement pstmt = null; // 声明PreparedStatement,用于执行SQL
try {
// 步骤1:加载驱动
Class.forName(DRIVER_CLASS);
// 步骤2:获取数据库连接
connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
// -------------- 场景1:查询指定ID的用户(SELECT语句,有返回结果)--------------
// 步骤3:创建PreparedStatement(SQL模板,用?占位符代替参数)
String selectSql = "SELECT id, username, age, create_time FROM t_user WHERE id = ?";
pstmt = connection.prepareStatement(selectSql);
// 设置占位符参数(核心:参数索引从1开始,不是0!企业开发中常犯错误)
pstmt.setInt(1, 1001); // 第一个?:id=1001(参数类型与数据库字段一致)
System.out.println("✅ PreparedStatement创建成功(查询SQL),SQL模板:" + selectSql);
// -------------- 场景2:新增用户(INSERT语句,无返回结果,仅返回影响行数)--------------
// 关闭上一个pstmt,避免资源泄漏(同一时间一个Connection可创建多个Statement,但需及时关闭无用的)
if (pstmt != null) {
pstmt.close();
}
String insertSql = "INSERT INTO t_user (username, age, create_time) VALUES (?, ?, NOW())";
pstmt = connection.prepareStatement(insertSql);
// 设置占位符参数(对应SQL中的3个?,顺序一致)
pstmt.setString(1, "zhangsan"); // 第一个?:username=zhangsan
pstmt.setInt(2, 25); // 第二个?:age=25
// 第三个?:create_time=NOW()(数据库函数,无需手动设置参数)
System.out.println("✅ PreparedStatement创建成功(新增SQL),SQL模板:" + insertSql);
} catch (ClassNotFoundException e) {
System.err.println("❌ MySQL驱动加载失败");
e.printStackTrace();
throw new RuntimeException("驱动加载异常", e);
} catch (SQLException e) {
System.err.println("❌ JDBC操作异常(连接/创建执行器失败)");
e.printStackTrace();
throw new RuntimeException("JDBC操作异常", e);
} finally {
// 关闭资源(顺序:Statement → Connection,先关闭Statement,再关闭Connection)
try {
if (pstmt != null && !pstmt.isClosed()) {
pstmt.close();
System.out.println("✅ PreparedStatement资源关闭成功");
}
if (connection != null && !connection.isClosed()) {
connection.close();
System.out.println("✅ 数据库连接关闭成功");
}
} catch (SQLException e) {
System.err.println("❌ 资源关闭失败");
e.printStackTrace();
}
}
}
}
3.3 注意事项
-
PreparedStatement的占位符(?)只能代替"参数值",不能代替表名、字段名、SQL关键字(如ORDER BY ? 无效,会被当作字符串处理)。
-
参数设置时,类型必须与数据库字段类型一致(如数据库字段是INT,用setInt();VARCHAR用setString()),否则会报类型转换异常。
-
Statement/PreparedStatement使用后必须及时关闭,否则会导致资源泄漏(即使Connection关闭,未关闭的Statement也可能占用数据库游标资源)。
步骤4:执行SQL语句(核心步骤)
核心目的:通过PreparedStatement将SQL语句(预编译后的模板+参数)发送到数据库,执行SQL并获取执行结果(查询语句返回ResultSet,增删改语句返回影响行数)。
PreparedStatement提供3种核心执行方法,对应不同SQL场景,企业开发中需精准选择:
4.1 3种执行方法对比(核心选型)
-
executeQuery():用于执行SELECT语句,返回ResultSet结果集(仅查询场景使用)。
-
executeUpdate():用于执行INSERT、UPDATE、DELETE语句,返回int类型(影响的行数),无结果集(增删改场景使用)。
-
execute():通用执行方法,可执行任意SQL(SELECT/增删改/存储过程),返回boolean类型(true表示有ResultSet,false表示无),企业开发中极少使用(仅复杂动态SQL场景)。
4.2 代码示例(完整执行流程,含查询+新增)
场景:完整演示"新增用户"+"查询新增的用户",覆盖executeUpdate()和executeQuery()的使用,补充结果集处理。
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Date;
public class JdbcSqlProcessDemo {
// 省略数据库连接参数(同步骤1)...
public static void main(String[] args) {
Connection connection = null;
PreparedStatement pstmt = null;
ResultSet rs = null; // 声明ResultSet,用于接收查询结果
try {
// 步骤1:加载驱动
Class.forName(DRIVER_CLASS);
// 步骤2:获取数据库连接
connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
// -------------- 场景1:新增用户(executeUpdate())--------------
String insertSql = "INSERT INTO t_user (username, age, create_time) VALUES (?, ?, NOW())";
pstmt = connection.prepareStatement(insertSql);
// 设置参数
pstmt.setString(1, "lisi");
pstmt.setInt(2, 28);
// 执行SQL(新增语句,用executeUpdate())
int affectRows = pstmt.executeUpdate();
// 验证执行结果(企业开发中必做,判断SQL是否执行成功)
if (affectRows > 0) {
System.out.println("✅ 新增用户成功,影响行数:" + affectRows);
} else {
System.out.println("❌ 新增用户失败,未影响任何行数");
throw new RuntimeException("新增用户失败");
}
// -------------- 场景2:查询新增的用户(executeQuery())--------------
// 关闭上一个pstmt,创建新的执行器
pstmt.close();
String selectSql = "SELECT id, username, age, create_time FROM t_user WHERE username = ?";
pstmt = connection.prepareStatement(selectSql);
pstmt.setString(1, "lisi");
// 执行SQL(查询语句,用executeQuery())
rs = pstmt.executeQuery();
// 处理ResultSet结果集(核心:遍历结果,获取字段值)
while (rs.next()) { // rs.next():移动到下一条记录,true表示有记录,false表示无
// 获取字段值(2种方式:字段名、字段索引,推荐用字段名,避免索引变化导致错误)
Long id = rs.getLong("id"); // 字段名:id(数据库字段名,区分大小写,需与表结构一致)
String username = rs.getString("username");
Integer age = rs.getInt("age");
Date createTime = rs.getTimestamp("create_time"); // 时间类型用getTimestamp()
// 输出查询结果(企业开发中会封装成实体类,如User对象)
System.out.println("✅ 查询到用户信息:");
System.out.println("id: " + id + ", username: " + username + ", age: " + age + ", create_time: " + createTime);
}
} catch (ClassNotFoundException e) {
System.err.println("❌ MySQL驱动加载失败");
e.printStackTrace();
throw new RuntimeException("驱动加载异常", e);
} catch (SQLException e) {
System.err.println("❌ JDBC操作异常(执行SQL失败)");
e.printStackTrace();
throw new RuntimeException("JDBC操作异常", e);
} finally {
// 关闭资源(核心顺序:ResultSet → Statement → Connection,必须严格遵守)
try {
// 先关闭ResultSet
if (rs != null && !rs.isClosed()) {
rs.close();
System.out.println("✅ ResultSet资源关闭成功");
}
// 再关闭PreparedStatement
if (pstmt != null && !pstmt.isClosed()) {
pstmt.close();
System.out.println("✅ PreparedStatement资源关闭成功");
}
// 最后关闭Connection
if (connection != null && !connection.isClosed()) {
connection.close();
System.out.println("✅ 数据库连接关闭成功");
}
} catch (SQLException e) {
System.err.println("❌ 资源关闭失败");
e.printStackTrace();
}
}
}
}
4.3 注意事项
-
executeQuery() 仅能执行SELECT语句,若执行增删改语句,会报SQL异常;executeUpdate() 仅能执行增删改语句,执行查询语句会报异常。
-
ResultSet的遍历:rs.next() 是"移动游标"的核心方法,首次调用移动到第一条记录,循环调用可遍历所有记录;若未调用rs.next(),直接获取字段值,会报异常。
-
ResultSet的字段获取:推荐用"字段名"(如rs.getString("username")),避免用字段索引(如rs.getString(2)),因为表结构调整(字段顺序变化)会导致索引失效。
-
时间类型处理:数据库的DATETIME/TIMESTAMP类型,用rs.getTimestamp() 获取,避免用getString()(格式混乱),获取后可转换为Java的Date/LocalDateTime类型(企业开发主流)。
步骤5:处理SQL执行结果(ResultSet/影响行数)
核心目的:将数据库返回的结果(查询结果集、增删改影响行数)转换为Java程序可使用的数据(实体类、集合、布尔值等),这是JDBC流程中"与业务逻辑衔接"的关键步骤。
5.1 结果处理分类(2种核心场景)
场景1:增删改语句(executeUpdate())------ 处理影响行数
executeUpdate() 返回int类型的"影响行数",企业开发中需根据影响行数判断SQL执行效果,常见处理逻辑:
-
影响行数 > 0:执行成功,继续后续业务(如新增成功后返回用户ID,修改成功后更新缓存)。
-
影响行数 = 0:执行失败(如修改时,条件匹配不到记录;删除时,记录已不存在),需抛出异常或返回提示。
场景2:查询语句(executeQuery())------ 处理ResultSet结果集
ResultSet是"临时结果集",依赖于PreparedStatement和Connection,若关闭了Statement或Connection,ResultSet会自动关闭(企业开发中需先处理结果集,再关闭资源)。
结果集处理的核心:将ResultSet封装为Java实体类(如User、Order),避免直接在业务逻辑中操作ResultSet(解耦、提高可读性)。
5.2 实战示例(结果集封装为实体类)
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* 企业开发实战:将ResultSet结果集封装为实体类(User),符合分层架构规范
*/
public class JdbcSqlProcessEnterpriseDemo {
// 数据库连接参数(企业开发中会放到配置文件,如application.properties)
private static final String DRIVER_CLASS = "com.mysql.cj.jdbc.Driver";
private static final String URL = "jdbc:mysql://localhost:3306/enterprise_db?useSSL=false&serverTimezone=UTC&characterEncoding=utf8";
private static final String USERNAME = "root";
private static final String PASSWORD = "123456";
// 1. 定义用户实体类(对应数据库t_user表,企业开发中会单独放在entity包下)
static class User {
private Long id;
private String username;
private Integer age;
private LocalDateTime createTime; // 企业开发中推荐用Java8新时间类型LocalDateTime
// 构造方法、getter/setter(省略,实际开发中必须有)
public User() {}
public User(Long id, String username, Integer age, LocalDateTime createTime) {
this.id = id;
this.username = username;
this.age = age;
this.createTime = createTime;
}
// toString方法,用于输出
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", age=" + age +
", createTime=" + createTime +
'}';
}
// getter/setter(省略,实际开发中必须有)
public Long getId() { return id; }
public void setId(Long 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; }
public LocalDateTime getCreateTime() { return createTime; }
public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; }
}
public static void main(String[] args) {
// 调用查询方法,获取封装后的用户列表(企业开发中会单独放在dao层,如UserDao)
List<User> userList = queryUserByAge(25);
System.out.println("✅ 查询到的用户列表:" + userList);
}
/**
* 企业开发DAO层方法:根据年龄查询用户列表(完整流程封装)
* @param age 查询条件:年龄
* @return 封装后的用户列表(List<User>)
*/
public static List<User> queryUserByAge(Integer age) {
Connection connection = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
List<User> userList = new ArrayList<>(); // 用于存储封装后的用户实体
try {
// 步骤1:加载驱动(企业开发中会由框架自动加载,如Spring)
Class.forName(DRIVER_CLASS);
// 步骤2:获取连接(企业开发中会用连接池,此处为演示)
connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
// 步骤3:创建PreparedStatement
String sql = "SELECT id, username, age, create_time FROM t_user WHERE age = ?";
pstmt = connection.prepareStatement(sql);
pstmt.setInt(1, age);
// 步骤4:执行SQL,获取结果集
rs = pstmt.executeQuery();
// 步骤5:处理结果集,封装为User实体
while (rs.next()) {
User user = new User();
// 给实体类赋值(字段名与数据库一致,避免错误)
user.setId(rs.getLong("id"));
user.setUsername(rs.getString("username"));
user.setAge(rs.getInt("age"));
// 时间类型转换:ResultSet的Timestamp → Java8 LocalDateTime(企业开发主流写法)
Date createTimeDate = rs.getTimestamp("create_time");
if (createTimeDate != null) {
user.setCreateTime(createTimeDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime());
}
// 将实体类添加到列表
userList.add(user);
}
System.out.println("✅ 结果集处理完成,共查询到 " + userList.size() + " 个用户");
} catch (ClassNotFoundException e) {
System.err.println("❌ 驱动加载失败");
e.printStackTrace();
throw new RuntimeException("驱动加载异常", e);
} catch (SQLException e) {
System.err.println("❌ 查询用户失败,SQL:" + pstmt.toString());
e.printStackTrace();
throw new RuntimeException("查询用户异常", e);
} finally {
// 关闭资源(企业开发中会用try-with-resources自动关闭,简化代码)
closeResources(rs, pstmt, connection);
}
return userList;
}
/**
* 企业开发工具方法:统一关闭JDBC资源(解耦,避免重复代码)
* @param rs ResultSet
* @param stmt Statement/PreparedStatement
* @param conn Connection
*/
public static void closeResources(ResultSet rs, PreparedStatement stmt, Connection conn) {
try {
if (rs != null && !rs.isClosed()) {
rs.close();
}
if (stmt != null && !stmt.isClosed()) {
stmt.close();
}
if (conn != null && !conn.isClosed()) {
conn.close();
}
} catch (SQLException e) {
System.err.println("❌ JDBC资源关闭失败");
e.printStackTrace();
}
}
}
5.3 注意事项
-
结果集封装:开发中必须将ResultSet封装为实体类/VO/DTO,避免在Service、Controller层直接操作ResultSet(降低耦合,提高代码可读性和可维护性)。
-
空值处理:数据库字段可能为NULL,获取时需注意(如rs.getInt("age") 若字段为NULL,会返回0,需用rs.wasNull() 判断是否为NULL,再赋值为null)。
-
资源关闭顺序:必须先处理完ResultSet,再关闭Statement和Connection,否则ResultSet会提前关闭,无法获取数据。
步骤6:事务控制
核心目的:当多个SQL语句需要"原子执行"(要么全部成功,要么全部失败)时,通过Connection控制事务(如转账:扣减余额+增加余额,两个SQL必须同时成功)。
JDBC事务默认是"自动提交"(autoCommit=true),即每执行一条SQL,自动提交到数据库,无法回滚;企业开发中需手动关闭自动提交,手动控制提交/回滚。
6.1 事务控制核心API(Connection提供)
-
connection.setAutoCommit(false):关闭自动提交,开启手动事务(必须在执行SQL前调用)。
-
connection.commit():所有SQL执行成功后,手动提交事务(将修改持久化到数据库)。
-
connection.rollback():SQL执行失败时,手动回滚事务(撤销所有未提交的修改)。
-
connection.setSavepoint():设置事务保存点(可选,用于部分回滚)。
6.2 实战示例(转账场景)
场景:用户A向用户B转账100元,涉及两个SQL:1. 扣减用户A余额100元;2. 增加用户B余额100元,两个SQL必须原子执行,否则会出现"单边转账"问题。
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
/**
* 企业开发实战:JDBC事务控制(转账场景,核心必掌握)
*/
public class JdbcTransactionDemo {
// 数据库连接参数(同前)
private static final String DRIVER_CLASS = "com.mysql.cj.jdbc.Driver";
private static final String URL = "jdbc:mysql://localhost:3306/enterprise_db?useSSL=false&serverTimezone=UTC&characterEncoding=utf8";
private static final String USERNAME = "root";
private static final String PASSWORD = "123456";
public static void main(String[] args) {
// 转账参数(企业开发中由业务逻辑传入)
Long fromUserId = 1001L; // 转账人ID
Long toUserId = 1002L; // 收款人ID
Integer amount = 100; // 转账金额
// 执行转账(调用事务方法)
boolean transferSuccess = transferMoney(fromUserId, toUserId, amount);
System.out.println(transferSuccess ? "✅ 转账成功" : "❌ 转账失败");
}
/**
* 转账业务(含JDBC事务控制)
* @param fromUserId 转账人ID
* @param toUserId 收款人ID
* @param amount 转账金额
* @return 转账是否成功
*/
public static boolean transferMoney(Long fromUserId, Long toUserId, Integer amount) {
Connection connection = null;
PreparedStatement pstmt1 = null; // 扣减余额SQL执行器
PreparedStatement pstmt2 = null; // 增加余额SQL执行器
try {
// 步骤1:加载驱动、获取连接
Class.forName(DRIVER_CLASS);
connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
// 步骤2:开启手动事务(核心:关闭自动提交,必须在执行SQL前调用)
connection.setAutoCommit(false);
System.out.println("✅ 手动事务开启,开始执行转账逻辑");
// 步骤3:执行SQL1:扣减转账人余额(fromUserId)
String sql1 = "UPDATE t_user SET balance = balance - ? WHERE id = ? AND balance >= ?";
pstmt1 = connection.prepareStatement(sql1);
pstmt1.setInt(1, amount); // 扣减金额
pstmt1.setLong(2, fromUserId); // 转账人ID
pstmt1.setInt(3, amount); // 校验余额是否充足
int affectRows1 = pstmt1.executeUpdate();
// 校验扣减是否成功(若影响行数为0,说明余额不足或用户不存在)
if (affectRows1 == 0) {
throw new SQLException("转账人余额不足或用户不存在,扣减余额失败");
}
System.out.println("✅ 转账人余额扣减成功,影响行数:" + affectRows1);
// 步骤4:执行SQL2:增加收款人余额(toUserId)
String sql2 = "UPDATE t_user SET balance = balance + ? WHERE id = ?";
pstmt2 = connection.prepareStatement(sql2);
pstmt2.setInt(1, amount); // 增加金额
pstmt2.setLong(2, toUserId); // 收款人ID
int affectRows2 = pstmt2.executeUpdate();
// 校验增加是否成功
if (affectRows2 == 0) {
throw new SQLException("收款人不存在,增加余额失败");
}
System.out.println("✅ 收款人余额增加成功,影响行数:" + affectRows2);
// 步骤5:所有SQL执行成功,提交事务(持久化修改)
connection.commit();
System.out.println("✅ 转账事务提交成功");
return true;
} catch (ClassNotFoundException e) {
System.err.println("❌ 驱动加载失败");
e.printStackTrace();
return false;
} catch (SQLException e) {
System.err.println("❌ 转账失败,开始回滚事务");
e.printStackTrace();
try {
// 步骤6:SQL执行失败,回滚事务(撤销所有未提交的修改)
if (connection != null && !connection.isClosed()) {
connection.rollback();
System.out.println("✅ 转账事务回滚成功,未造成资金异常");
}
} catch (SQLException ex) {
System.err.println("❌ 事务回滚失败,需人工排查资金异常");
ex.printStackTrace();
}
return false;
} finally {
// 关闭资源(无论事务成功/失败,都必须关闭资源)
closeResources(null, pstmt1, connection);
closeResources(null, pstmt2, null); // pstmt2单独关闭,避免空指针
}
}
/**
* 统一关闭资源工具方法(重载,适配无ResultSet的场景)
*/
public static void closeResources(ResultSet rs, PreparedStatement stmt, Connection conn) {
try {
if (rs != null && !rs.isClosed()) {
rs.close();
}
if (stmt != null && !stmt.isClosed()) {
stmt.close();
}
if (conn != null && !conn.isClosed()) {
conn.close();
}
} catch (SQLException e) {
System.err.println("❌ JDBC资源关闭失败");
e.printStackTrace();
}
}
}
6.3 注意事项(事务控制关键)
-
事务开启时机:setAutoCommit(false) 必须在执行SQL前调用,否则SQL会自动提交,无法回滚。
-
事务回滚:必须在catch块中调用rollback(),确保SQL执行失败时,能够撤销所有修改(避免资金异常、数据不一致)。
-
资源关闭:无论事务成功还是失败,都必须关闭Statement和Connection,否则会导致资源泄漏。
-
开发中,事务通常由框架(如Spring)管理(@Transactional注解),但底层仍是JDBC的事务API,理解底层原理才能排查事务异常(如事务不回滚、事务超时)。
步骤7:关闭JDBC资源(必做,避免资源泄漏)
核心目的:JDBC的Connection、Statement、ResultSet都是"稀缺资源",占用数据库连接、游标等系统资源,若不及时关闭,会导致资源泄漏,最终引发系统性能下降、数据库连接耗尽等严重问题。
7.1 资源关闭核心规则
-
关闭顺序:ResultSet → Statement(PreparedStatement) → Connection(反向创建顺序,避免资源依赖导致关闭失败)。
-
关闭时机:资源使用完毕后立即关闭(如ResultSet处理完成后,立即关闭;SQL执行完成后,立即关闭Statement)。
-
异常处理:关闭资源时需捕获SQLException,避免因关闭失败导致程序异常。
-
简化写法:开发中推荐使用「try-with-resources」语法(Java7+),自动关闭资源,无需手动写finally块(底层会自动按顺序关闭)。
7.2 简化写法(try-with-resources)
try-with-resources 语法:将需要关闭的资源(Connection、Statement、ResultSet)放在try括号中,程序执行完毕后,JVM会自动关闭这些资源(无需手动写finally),简化代码,减少资源泄漏风险。
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* 企业开发简化写法:try-with-resources 自动关闭JDBC资源
*/
public class JdbcTryWithResourcesDemo {
// 数据库连接参数(同前)
private static final String DRIVER_CLASS = "com.mysql.cj.jdbc.Driver";
private static final String URL = "jdbc:mysql://localhost:3306/enterprise_db?useSSL=false&serverTimezone=UTC&characterEncoding=utf8";
private static final String USERNAME = "root";
private static final String PASSWORD = "123456";
// 用户实体类(同前)
static class User {
private Long id;
private String username;
private Integer age;
private LocalDateTime createTime;
// 省略构造方法、getter/setter、toString
}
/**
* 简化版查询方法(try-with-resources 自动关闭资源)
*/
public static List<User> queryUserByAgeSimplify(Integer age) {
List<User> userList = new ArrayList<>();
try {
// 加载驱动
Class.forName(DRIVER_CLASS);
// try-with-resources:自动关闭Connection、PreparedStatement、ResultSet
// 资源放在try括号中,顺序:Connection → PreparedStatement(ResultSet由executeQuery()返回,也会自动关闭)
try (
Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
PreparedStatement pstmt = connection.prepareStatement("SELECT id, username, age, create_time FROM t_user WHERE age = ?")
) {
// 设置参数、执行SQL
pstmt.setInt(1, age);
try (ResultSet rs = pstmt.executeQuery()) { // ResultSet也放入try-with-resources,自动关闭
// 处理结果集
while (rs.next()) {
User user = new User();
user.setId(rs.getLong("id"));
user.setUsername(rs.getString("username"));
user.setAge(rs.getInt("age"));
Date createTimeDate = rs.getTimestamp("create_time");
if (createTimeDate != null) {
user.setCreateTime(createTimeDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime());
}
userList.add(user);
}
}
}
System.out.println("✅ 查询完成,自动关闭所有JDBC资源");
} catch (ClassNotFoundException e) {
System.err.println("❌ 驱动加载失败");
e.printStackTrace();
throw new RuntimeException("驱动加载异常", e);
} catch (SQLException e) {
System.err.println("❌ 查询用户失败");
e.printStackTrace();
throw new RuntimeException("查询用户异常", e);
}
return userList;
}
public static void main(String[] args) {
List<User> userList = queryUserByAgeSimplify(25);
System.out.println("✅ 查询到的用户列表:" + userList);
}
}
7.3 注意事项
-
try-with-resources 仅支持实现了「AutoCloseable」接口的资源,JDBC的Connection、Statement、ResultSet都实现了该接口,可直接使用。
-
资源顺序:try括号中,资源的声明顺序与关闭顺序相反(Connection先声明,后关闭;PreparedStatement后声明,先关闭),符合JDBC资源关闭规则。
-
ResultSet的处理:executeQuery() 返回的ResultSet,需放入单独的try-with-resources中,或在try块中处理完成后,由JVM自动关闭(推荐放入try-with-resources,更安全)。
三、JDBC流程优化(核心进阶)
前面讲解的是JDBC处理SQL的"基础流程",但在企业高并发、高可用场景中,直接使用DriverManager获取连接、手动管理资源,会存在性能瓶颈和稳定性问题,需进行以下优化。
3.1 优化1:使用连接池(企业开发必用)
问题:DriverManager每次getConnection()都会创建新的物理连接,频繁创建/关闭连接会消耗大量系统资源,导致数据库压力过大、系统响应变慢。
解决方案:使用连接池(如Druid、HikariCP、C3P0),连接池会提前创建一定数量的数据库连接,Java程序从连接池获取连接,使用完毕后归还连接(而非关闭),实现连接复用,提升系统性能。
开发实战(Druid连接池示例)
Druid是阿里巴巴开源的连接池,性能优异、功能强大(支持监控、防SQL注入、连接泄漏检测等),是国内企业开发中最常用的连接池:
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.pool.DruidPooledConnection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* 企业开发实战:Druid连接池使用(替代DriverManager,必用优化)
*/
public class DruidPoolDemo {
// 1. 配置Druid连接池参数(企业开发中放入配置文件,如application.properties)
private static final String DRIVER_CLASS = "com.mysql.cj.jdbc.Driver";
private static final String URL = "jdbc:mysql://localhost:3306/enterprise_db?useSSL=false&serverTimezone=UTC&characterEncoding=utf8";
private static final String USERNAME = "root";
private static final String PASSWORD = "123456";
// 连接池核心参数(根据业务调整,避免资源浪费或不足)
private static final int INITIAL_SIZE = 5; // 初始连接数(项目启动时创建的连接数量)
private static final int MAX_ACTIVE = 20; // 最大活跃连接数(同时可使用的最大连接数)
private static final int MAX_WAIT = 60000; // 最大等待时间(获取连接超时时间,单位:毫秒)
private static final int MIN_IDLE = 3; // 最小空闲连接数(空闲时保留的最小连接数)
private static final long TIME_BETWEEN_EVICTION_RUNS_MILLIS = 60000; // 连接检测间隔(单位:毫秒)
// 2. 初始化Druid连接池(全局单例,项目启动时初始化1次,避免重复创建)
private static DruidDataSource dataSource;
static {
try {
dataSource = new DruidDataSource();
// 设置数据库连接参数
dataSource.setDriverClassName(DRIVER_CLASS);
dataSource.setUrl(URL);
dataSource.setUsername(USERNAME);
dataSource.setPassword(PASSWORD);
// 设置连接池核心参数
dataSource.setInitialSize(INITIAL_SIZE);
dataSource.setMaxActive(MAX_ACTIVE);
dataSource.setMaxWait(MAX_WAIT);
dataSource.setMinIdle(MIN_IDLE);
dataSource.setTimeBetweenEvictionRunsMillis(TIME_BETWEEN_EVICTION_RUNS_MILLIS);
// 可选:开启连接泄漏检测(企业开发必开,排查资源泄漏问题)
dataSource.setRemoveAbandoned(true);
dataSource.setRemoveAbandonedTimeout(180); // 连接超时时间(秒)
dataSource.setLogAbandoned(true); // 记录泄漏日志
System.out.println("✅ Druid连接池初始化成功,连接池信息:" + dataSource);
} catch (Exception e) {
System.err.println("❌ Druid连接池初始化失败");
e.printStackTrace();
throw new RuntimeException("连接池初始化异常", e);
}
}
// 3. 从连接池获取连接(替代DriverManager.getConnection())
public static DruidPooledConnection getConnection() throws SQLException {
return dataSource.getConnection();
}
// 4. 实战:使用Druid连接池完成查询(封装DAO层方法)
public static List<User> queryUserByAgeWithDruid(Integer age) {
List<User> userList = new ArrayList<>();
// 从连接池获取连接(无需手动关闭连接,使用完毕后归还到连接池)
try (
DruidPooledConnection connection = getConnection();
PreparedStatement pstmt = connection.prepareStatement("SELECT id, username, age, create_time FROM t_user WHERE age = ?")
) {
pstmt.setInt(1, age);
try (ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
User user = new User();
user.setId(rs.getLong("id"));
user.setUsername(rs.getString("username"));
user.setAge(rs.getInt("age"));
Date createTimeDate = rs.getTimestamp("create_time");
if (createTimeDate != null) {
user.setCreateTime(createTimeDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime());
}
userList.add(user);
}
}
System.out.println("✅ 使用Druid连接池查询完成,当前活跃连接数:" + dataSource.getActiveCount());
} catch (SQLException e) {
System.err.println("❌ Druid连接池查询异常");
e.printStackTrace();
throw new RuntimeException("Druid查询异常", e);
}
return userList;
}
// 用户实体类(与前文一致,简化代码)
static class User {
private Long id;
private String username;
private Integer age;
private LocalDateTime createTime;
// getter/setter、toString省略
public Long getId() { return id; }
public void setId(Long 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; }
public LocalDateTime getCreateTime() { return createTime; }
public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; }
}
public static void main(String[] args) {
List<User> userList = queryUserByAgeWithDruid(25);
System.out.println("✅ 查询到的用户列表:" + userList);
}
}
3.1.1 Druid连接池核心优势
-
连接复用:避免频繁创建/关闭物理连接,降低系统开销,提升高并发场景下的响应速度。
-
监控功能:内置监控页面(可配置),实时查看连接池状态、SQL执行情况、连接泄漏等问题,便于排查故障。
-
安全防护:支持防SQL注入、连接泄漏检测、超时连接回收等功能,提升系统稳定性和安全性。
-
灵活配置:可根据业务需求调整初始连接数、最大活跃连接数等参数,适配不同并发场景。
3.2 优化2:SQL预编译与批处理优化
问题:频繁执行相同SQL模板(仅参数不同)的语句(如批量新增、批量修改),每次都需要重新编译SQL,消耗数据库资源;单条执行SQL效率极低,无法满足批量操作场景(如批量导入数据)。
解决方案:利用PreparedStatement的预编译机制(SQL模板只编译1次,重复使用),结合addBatch()和executeBatch()实现批处理,大幅提升批量操作效率。
开发实战(批处理示例)
场景:批量新增100条用户数据(企业开发中常见的批量导入场景),对比单条执行与批处理的效率差异,演示批处理的正确用法。
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.pool.DruidPooledConnection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.UUID;
/**
* 企业开发实战:PreparedStatement批处理优化(批量操作必用)
*/
public class JdbcBatchDemo {
// 初始化Druid连接池(与前文一致,省略重复代码)
private static DruidDataSource dataSource;
static {
// 此处省略Druid连接池初始化代码,直接复用前文配置
}
// 批量新增用户(批处理优化)
public static void batchInsertUser(int batchSize) {
long startTime = System.currentTimeMillis(); // 记录开始时间
String sql = "INSERT INTO t_user (username, age, create_time) VALUES (?, ?, NOW())";
try (
DruidPooledConnection connection = dataSource.getConnection();
PreparedStatement pstmt = connection.prepareStatement(sql)
) {
// 关闭自动提交(批处理必须关闭,否则每条SQL都会自动提交,失去批处理意义)
connection.setAutoCommit(false);
// 批量添加SQL参数(循环添加,达到批次大小后执行)
for (int i = 0; i < batchSize; i++) {
// 生成随机用户名(模拟批量数据)
String username = "user_" + UUID.randomUUID().toString().substring(0, 8);
int age = 18 + (int) (Math.random() * 10); // 随机年龄(18-27岁)
// 设置参数
pstmt.setString(1, username);
pstmt.setInt(2, age);
// 添加到批处理队列(不立即执行)
pstmt.addBatch();
// 可选:每1000条执行1次批处理(避免队列过长,占用内存)
if ((i + 1) % 1000 == 0) {
pstmt.executeBatch(); // 执行批处理
pstmt.clearBatch(); // 清空批处理队列
}
}
// 执行剩余的批处理(不足1000条的部分)
pstmt.executeBatch();
connection.commit(); // 手动提交事务
long endTime = System.currentTimeMillis();
System.out.println("✅ 批处理新增" + batchSize + "条用户成功,耗时:" + (endTime - startTime) + "ms");
} catch (SQLException e) {
System.err.println("❌ 批处理新增用户失败");
e.printStackTrace();
try {
// 批处理失败,回滚事务
if (dataSource.getConnection() != null && !dataSource.getConnection().isClosed()) {
dataSource.getConnection().rollback();
}
} catch (SQLException ex) {
ex.printStackTrace();
}
throw new RuntimeException("批处理异常", e);
}
}
public static void main(String[] args) {
// 测试:批量新增100条用户数据
batchInsertUser(100);
}
}
3.2.1 批处理核心注意事项
-
批处理必须关闭自动提交(connection.setAutoCommit(false)),否则每条SQL都会单独提交,无法达到批量优化的效果。
-
避免一次性添加过多批处理任务(如10万条),建议分批次执行(每1000-5000条执行1次),防止内存溢出。
-
批处理仅适用于"相同SQL模板、不同参数"的场景(如批量新增、批量修改),不同SQL模板无法使用批处理。
-
MySQL默认关闭批处理优化,需在URL中添加参数「rewriteBatchedStatements=true」,开启批处理优化(否则效率提升不明显)。
3.3 优化3:结果集与实体类映射优化(解耦)
问题:前文演示的结果集封装(手动setter赋值),代码冗余、耦合度高,若数据库表字段较多(如20+字段),会出现大量重复的setter代码,维护成本高。
解决方案:使用反射机制封装通用的结果集映射工具类,实现ResultSet与任意实体类的自动映射,简化代码、降低耦合,企业开发中可直接复用。
开发实战(通用结果集映射工具类)
import java.lang.reflect.Field;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* 企业开发通用工具类:ResultSet结果集自动映射为实体类(解耦,通用复用)
*/
public class ResultSetMapper {
/**
* 通用映射方法:将ResultSet映射为单个实体类
* @param rs 结果集(已执行next()方法,指向单条记录)
* @param clazz 实体类Class对象
* @param <T> 实体类泛型
* @return 映射后的实体类对象
*/
public static <T> T mapToEntity(ResultSet rs, Class<T> clazz) throws SQLException, IllegalAccessException, InstantiationException {
// 1. 获取实体类的所有字段(包括私有字段)
Field[] fields = clazz.getDeclaredFields();
// 2. 获取结果集的元数据(字段名、字段类型)
ResultSetMetaData metaData = rs.getMetaData();
// 3. 实例化实体类
T entity = clazz.newInstance();
// 4. 遍历结果集字段,与实体类字段匹配并赋值
for (int i = 1; i <= metaData.getColumnCount(); i++) {
String columnName = metaData.getColumnName(i); // 数据库字段名
Object columnValue = rs.getObject(i); // 数据库字段值
// 遍历实体类字段,匹配数据库字段名(忽略大小写,适配实体类驼峰命名、数据库下划线命名)
for (Field field : fields) {
// 实体类字段名(驼峰)转数据库字段名(下划线),如username → username,createTime → create_time
String fieldName = camelToUnderline(field.getName());
if (fieldName.equalsIgnoreCase(columnName)) {
// 开启字段访问权限(私有字段可赋值)
field.setAccessible(true);
// 类型转换(适配数据库类型与Java实体类类型)
columnValue = convertType(columnValue, field.getType());
// 给实体类字段赋值
field.set(entity, columnValue);
break;
}
}
}
return entity;
}
/**
* 通用映射方法:将ResultSet映射为实体类列表
* @param rs 结果集(未执行next()方法,指向第一条记录前)
* @param clazz 实体类Class对象
* @param <T> 实体类泛型
* @return 映射后的实体类列表
*/
public static <T> List<T> mapToList(ResultSet rs, Class<T> clazz) throws SQLException, IllegalAccessException, InstantiationException {
List<T> entityList = new ArrayList<>();
while (rs.next()) {
// 调用单个实体类映射方法,添加到列表
T entity = mapToEntity(rs, clazz);
entityList.add(entity);
}
return entityList;
}
/**
* 辅助方法:驼峰命名转下划线命名(适配实体类与数据库字段命名规范)
* @param camelName 驼峰命名(如createTime)
* @return 下划线命名(如create_time)
*/
private static String camelToUnderline(String camelName) {
if (camelName == null || camelName.isEmpty()) {
return camelName;
}
StringBuilder underlineName = new StringBuilder();
underlineName.append(Character.toLowerCase(camelName.charAt(0)));
for (int i = 1; i < camelName.length(); i++) {
char c = camelName.charAt(i);
if (Character.isUpperCase(c)) {
underlineName.append("_").append(Character.toLowerCase(c));
} else {
underlineName.append(c);
}
}
return underlineName.toString();
}
/**
* 辅助方法:类型转换(数据库类型 → Java实体类类型)
* @param columnValue 数据库字段值
* @param targetType 实体类字段类型
* @return 转换后的值
*/
private static Object convertType(Object columnValue, Class<?> targetType) {
if (columnValue == null) {
return null;
}
// 时间类型转换:数据库Timestamp → Java LocalDateTime
if (columnValue instanceof Date && targetType == LocalDateTime.class) {
return ((Date) columnValue).toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
}
// 其他类型自动转换(如Integer、Long、String等,数据库类型与Java类型匹配)
return targetType.cast(columnValue);
}
}
3.3.1 工具类使用示例(简化结果集处理)
// 复用前文的Druid连接池,使用通用工具类映射结果集
public static List<User> queryUserByAgeWithMapper(Integer age) {
List<User> userList = new ArrayList<>();
String sql = "SELECT id, username, age, create_time FROM t_user WHERE age = ?";
try (
DruidPooledConnection connection = dataSource.getConnection();
PreparedStatement pstmt = connection.prepareStatement(sql);
ResultSet rs = pstmt.executeQuery()
) {
pstmt.setInt(1, age);
// 调用通用工具类,自动映射为User列表(无需手动setter赋值)
userList = ResultSetMapper.mapToList(rs, User.class);
System.out.println("✅ 使用通用映射工具类查询完成,查询到" + userList.size() + "条记录");
} catch (Exception e) {
System.err.println("❌ 工具类映射异常");
e.printStackTrace();
throw new RuntimeException("结果集映射异常", e);
}
return userList;
}
3.4 优化4:异常处理与日志规范
问题:前文代码中,异常处理较为简单(仅打印堆栈),企业开发中,不规范的异常处理会导致故障排查困难、日志杂乱,无法定位问题根源。
解决方案:统一异常处理规范,结合日志框架(如SLF4J+Logback)记录日志,区分异常级别(ERROR、WARN、INFO),包含关键信息(SQL语句、参数、异常位置),便于排查问题。
开发实战(异常处理与日志规范)
import com.alibaba.druid.pool.DruidPooledConnection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.List;
/**
* 企业开发实战:JDBC异常处理与日志规范(必守)
*/
public class JdbcExceptionDemo {
// 1. 初始化日志对象(SLF4J+Logback,企业开发主流日志框架)
private static final Logger logger = LoggerFactory.getLogger(JdbcExceptionDemo.class);
// 2. 规范的查询方法(包含日志记录与异常处理)
public static List<User> queryUserByAgeWithLog(Integer age) {
// 日志记录:INFO级别,记录方法入口、参数
logger.info("开始查询年龄为{}的用户,方法:queryUserByAgeWithLog", age);
String sql = "SELECT id, username, age, create_time FROM t_user WHERE age = ?";
List<User> userList = new ArrayList<>();
try (
DruidPooledConnection connection = dataSource.getConnection();
PreparedStatement pstmt = connection.prepareStatement(sql)
) {
pstmt.setInt(1, age);
logger.debug("执行查询SQL:{},参数:{}", sql, age); // DEBUG级别,记录SQL与参数(生产环境可关闭)
try (ResultSet rs = pstmt.executeQuery()) {
userList = ResultSetMapper.mapToList(rs, User.class);
}
// 日志记录:INFO级别,记录方法出口、结果
logger.info("查询年龄为{}的用户完成,共查询到{}条记录", age, userList.size());
} catch (SQLException e) {
// 日志记录:ERROR级别,记录异常信息(SQL、参数、堆栈)
logger.error("查询年龄为{}的用户失败,SQL:{},异常原因:{}", age, sql, e.getMessage(), e);
// 抛出自定义异常(向上层传递,由全局异常处理器统一处理)
throw new BusinessException("用户查询失败,请联系管理员", e);
} catch (Exception e) {
// 日志记录:ERROR级别,处理其他异常(如反射异常、映射异常)
logger.error("查询年龄为{}的用户出现未知异常,异常原因:{}", age, e.getMessage(), e);
throw new BusinessException("系统异常,请稍后重试", e);
}
return userList;
}
// 自定义业务异常(企业开发中,避免抛出原生SQLException,解耦业务与JDBC)
static class BusinessException extends RuntimeException {
public BusinessException(String message, Throwable cause) {
super(message, cause);
}
}
public static void main(String[] args) {
try {
queryUserByAgeWithLog(25);
} catch (BusinessException e) {
// 上层业务处理自定义异常(如返回错误提示给前端)
logger.warn("业务异常处理:{}", e.getMessage());
}
}
}
3.4.1 异常处理与日志核心规范
-
日志级别:INFO(记录正常流程)、DEBUG(记录SQL、参数等调试信息,生产环境关闭)、ERROR(记录异常信息,必须包含堆栈)、WARN(记录警告信息)。
-
异常封装:避免向上层传递原生SQLException,封装为自定义业务异常,隐藏底层实现细节,同时传递异常原因。
-
关键信息:异常日志必须包含SQL语句、参数、异常位置(方法名),便于快速定位问题(如"查询用户失败,SQL:xxx,参数:xxx")。
-
全局异常:企业开发中,结合Spring的@ControllerAdvice,实现全局异常处理器,统一处理自定义异常,返回标准化错误响应。
四、JDBC常见问题排查与解决方案
在JDBC开发过程中,经常会遇到驱动加载失败、连接超时、SQL注入、资源泄漏等问题,以下是企业开发中最常见的8个问题,结合真实场景提供排查思路和解决方案,帮你快速避坑。
4.1 问题1:MySQL驱动加载失败(ClassNotFoundException)
常见原因:
-
Maven依赖未引入,或依赖版本错误(如MySQL 8.0使用5.7的驱动)。
-
驱动类名写错(MySQL 8.0是com.mysql.cj.jdbc.Driver,5.7及以下是com.mysql.jdbc.Driver)。
-
JAR包冲突(如项目中引入了多个版本的MySQL驱动)。
解决方案:
-
确认引入正确的MySQL驱动依赖(参考前文Maven依赖,MySQL 8.0推荐8.0.36版本)。
-
核对驱动类名,MySQL 8.0必须加上"cj",即com.mysql.cj.jdbc.Driver。
-
排查JAR包冲突,使用Maven的dependency:tree命令,排除多余的驱动依赖。
4.2 问题2:数据库连接失败(SQLException: Access denied for user)
常见原因:
-
用户名或密码错误(与数据库配置不一致)。
-
数据库未启动,或端口号错误(默认3306,若修改过需核对)。
-
URL参数错误(如未指定serverTimezone,或数据库名写错)。
-
数据库权限不足(如root用户未开启远程访问权限)。
解决方案:
-
核对用户名、密码、数据库名、端口号,确保与数据库配置一致。
-
确认数据库已启动,本地可通过MySQL客户端连接测试。
-
MySQL 8.0 URL必须添加serverTimezone参数(如serverTimezone=Asia/Shanghai)。
-
给root用户授权远程访问:GRANT ALL PRIVILEGES ON . TO 'root'@'%' IDENTIFIED BY '密码' WITH GRANT OPTION; FLUSH PRIVILEGES;
4.3 问题3:SQL注入漏洞(Statement执行动态SQL)
常见原因:使用Statement执行动态SQL,直接拼接参数(如String sql = "SELECT * FROM t_user WHERE username = '" + username + "'"),恶意用户可通过拼接SQL关键字攻击数据库。
解决方案:
-
杜绝使用Statement,统一使用PreparedStatement,通过占位符(?)传递参数。
-
禁止手动拼接SQL参数,即使是静态SQL,也推荐使用占位符。
-
使用Druid连接池的防SQL注入功能,开启过滤配置。
4.4 问题4:资源泄漏(Connection/Statement/ResultSet未关闭)
常见原因:
-
未在finally块中关闭资源,或关闭顺序错误。
-
使用try-with-resources时,未将所有资源放入try括号中(如ResultSet未放入)。
-
事务回滚时,忘记关闭资源。
解决方案:
-
推荐使用try-with-resources语法,自动关闭资源(无需手动写finally)。
-
手动关闭资源时,严格遵守"ResultSet → Statement → Connection"的顺序。
-
开启Druid连接池的连接泄漏检测功能,及时发现泄漏问题。
4.5 问题5:批处理效率低(MySQL未开启批处理优化)
常见原因:MySQL默认关闭批处理优化,即使使用executeBatch(),也会单条执行SQL,效率极低。
解决方案:在URL中添加参数「rewriteBatchedStatements=true」,开启MySQL批处理优化,同时确保使用PreparedStatement的addBatch()和executeBatch()方法。
4.6 问题6:时间类型异常(java.sql.SQLException: Unsupported conversion)
常见原因:数据库时间类型(DATETIME/TIMESTAMP)与Java类型不匹配,如用getString()获取TIMESTAMP类型,或用getDate()获取LocalDateTime类型。
解决方案:
-
数据库TIMESTAMP/DATETIME类型,用rs.getTimestamp()获取,再转换为Java LocalDateTime类型(推荐)。
-
避免用getString()获取时间类型,防止格式混乱和类型转换异常。
-
使用通用结果集映射工具类,自动处理时间类型转换(参考前文工具类)。
4.7 问题7:事务不回滚(SQL执行失败后,数据仍被提交)
常见原因:
-
未关闭自动提交(connection.setAutoCommit(false)未调用)。
-
事务回滚代码未放在catch块中,或未捕获SQLException。
-
使用了Spring事务管理,未正确配置@Transactional注解(如注解失效)。
解决方案:
-
手动控制事务时,必须在执行SQL前调用connection.setAutoCommit(false)。
-
确保rollback()方法放在catch块中,且能被执行(避免catch块提前return)。
-
Spring事务管理时,确保@Transactional注解标注在public方法上,且异常类型符合配置(默认只回滚RuntimeException)。
4.8 问题8:高并发下连接耗尽(数据库连接不够用)
常见原因:
-
连接池最大活跃连接数配置过小,无法满足高并发需求。
-
资源泄漏,导致连接未被归还到连接池。
-
SQL执行过慢(如未加索引),导致连接长时间被占用。
解决方案:
-
根据业务并发量,调整连接池最大活跃连接数(如MAX_ACTIVE=50,需结合数据库性能)。
-
开启连接泄漏检测,排查并修复资源泄漏问题。
-
优化SQL语句,添加索引,减少SQL执行时间,避免连接长时间占用。