news 2026/10/14 12:09:29

JDBC连接MySQL实现增删改查:从入门到避坑实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JDBC连接MySQL实现增删改查:从入门到避坑实战指南

简介:这是一份面向Java初学者的JDBC实战讲解PDF,围绕使用Java JDBC连接MySQL数据库并完成增删改查(CRUD)全流程展开,适合正在学习Java Web、数据库编程,或希望为后续掌握Hibernate、MyBatis等ORM框架打基础的开发者。文件从开发环境准备入手,依次说明MySQL数据库与表的创建、DBUtil连接工具类的静态初始化、Goddess实体类设计,以及DAO层中插入、查询、更新、删除操作的实现方式,并特别使用PreparedStatement预防SQL注入,能帮助读者完整理清JDBC核心流程与分层思路。内容对驱动加载、连接获取、结果集遍历等常见问题也有直观体现,可照着示例独立搭建一套可运行的CRUD项目。资源为单个PDF文档,压缩包整体约326KB,轻量便携,可直接阅读或打印,适合碎片化学习。该资料已有5619人学习浏览,覆盖连接配置、常见踩坑点和JDBC与ORM框架的关系,适合作为系统学习或考前复习的速查手册。

1. 先把 JDBC 连 MySQL 这件事从"会背"变成"能跑"

java jdbc连接mysql数据库实现增删改查操作 是每个 Java 后端开发者都绕不过去的基本功。我见过不少能背出八股文的人,真到要连一次 MySQL 时,却被 Communications link failure、Public Key Retrieval is not allowed 和 serverTimezone 三个报错按在地上摩擦。JDBC 不算框架,它是 Java 访问关系数据库的标准接口;MySQL 驱动(mysql-connector-j)只是它的实现。学会用 JDBC 做增删改查,意味着你理解了数据库操作的最小单元:拿连接、发 SQL、处理结果、关资源。这里的内容面向两类人:一是刚学完 Java 基础、准备接 SSM/SpringBoot 项目的新手;二是面试前想把手写 JDBC 流程重新捡起来的求职者。按下面的路径走,不依赖任何重量级框架,一个 Maven 工程就能跑通。

2. 搭好最小可运行环境:驱动、连接串与第一个 JDBC 程序

在写增删改查之前,先确认环境能连上。JDBC 连接 MySQL 的关键不是记住 API,而是理解驱动和连接串里的每个参数,否则后面所有操作都会变成玄学。下面从依赖引入、连接代码和参数含义三个角度把地基打牢。

2.1 选驱动包:mysql-connector-j 的版本和 MySQL 版本怎么配

常见做法是使用 Maven 管理依赖。如果你用的是 MySQL 8.0 及以上,推荐使用com.mysql:mysql-connector-j而不是老的mysql:mysql-connector-java。从 8.0.31 开始,官方把坐标从mysql:mysql-connector-java迁移到了com.mysql:mysql-connector-j,两者实质是同一个 jar,但新坐标更规范。如果项目里还有老代码,也不要急着改坐标,先看它能不能连上,改版本引发的驱动行为差异反而更坑。

<dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.0.33</version> </dependency>

驱动版本不必追求最新。MySQL 8.0 的服务端配 8.0.33 的驱动是稳妥组合;如果是 MySQL 5.7,老项目的5.1.49也能正常工作。高版本驱动默认兼容低版本服务端,但反过来,老驱动连 MySQL 8 会报Unable to load authentication plugin 'caching_sha2_password'。所以一个原则:驱动版本不低于服务端主版本。这里给8.0.33是常见做法,如果公司私服里没有,就换一个近似的 8.x 版本,能跑通即可。

再补一个容易混淆的点:MySQL Connector/J 5.1 系列的驱动类名是com.mysql.jdbc.Driver,8.x 系列的驱动类名是com.mysql.cj.jdbc.Driver。如果从老项目升级驱动,不要只换 jar 不换 className,否则会报 ClassNotFoundException。常见搭配如下表:

MySQL 版本驱动坐标示例驱动类连接串前缀
5.6 / 5.7mysql:mysql-connector-java:5.1.49com.mysql.jdbc.Driverjdbc:mysql://
8.0+com.mysql:mysql-connector-j:8.0.33com.mysql.cj.jdbc.Driverjdbc:mysql://

这个表在实际排查时很有用,因为你在网上搜到的报错帖可能来自 5.x,照抄后驱动类名对不上,问题反而更复杂。

2.2 第一个 JDBC 连接:Class.forName 到底还能不能删

写完 pom 依赖,下一步是建立连接。先看最简单的连接代码,这里我把驱动加载、URL、账号密码都写在 main 方法里,方便新手观察每一步。

import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class JdbcFirstDemo { public static void main(String[] args) { String url = "jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&characterEncoding=utf8"; String user = "root"; String password = "root123"; // 8.x 驱动在 JDBC 4.0 之后会自动注册,但保留这行能兼容老环境和老代码习惯 try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { e.printStackTrace(); } try (Connection connection = DriverManager.getConnection(url, user, password)) { System.out.println("连接成功,conn=" + connection); } catch (SQLException e) { e.printStackTrace(); } } }

这段代码的逻辑很简单:先把驱动类加载进 JVM,然后用 DriverManager 根据 URL 找到对应的 Driver 实现,最后通过 getConnection 拿到物理连接。需要注意的是Class.forName在 JDBC 4.0 以后已经不是必需的了,只要 jar 在 classpath 里,DriverManager 会通过 SPI 自动加载com.mysql.cj.jdbc.Driver。我一般会保留它,一是老项目里常见,二是显式加载能避免某些 Web 容器里类加载顺序导致的 "No suitable driver" 玄学。try-with-resources会自动关闭 Connection,但并不会真正关闭物理连接,只是把连接还给驱动内部的管理逻辑;这里因为直接用 DriverManager,close 会释放连接。

实际运行时,DriverManager.getConnection可能会抛出几个常见异常,这里提前说:SSL 相关报错多半是useSSL没写或服务端证书不受信任;时区解析报错多半是serverTimezone缺失;Access denied多半是账号、密码或 host 匹配问题。这些在第五章会逐个展开,现在先让自己的本地环境能打成 "连接成功" 的输出。

2.3 连接串参数:useSSL、serverTimezone、allowPublicKeyRetrieval 缺一不可

连接串是 JDBC 踩坑重灾区,尤其是 MySQL 8 改成默认 caching_sha2_password 认证后,少一个参数都会报错。最常见的是这一条:

jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&characterEncoding=utf8

逐个说参数。useSSL=false表示本地开发不走 SSL 加密。注意 MySQL Connector/J 8.0.27 之后官方推荐用sslMode替代useSSL,写法是sslMode=DISABLED或sslMode=PREFERRED。但很多老代码和低版本驱动只认useSSL,所以在兼容性优先时仍写useSSL=false。serverTimezone=Asia/Shanghai是时区参数,如果数据库是 MySQL 8,不写它经常会报The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized。allowPublicKeyRetrieval=true这个参数最容易被忽略,当连接开启 SSL 但服务端拿不到 RSA 公钥时会报Public Key Retrieval is not allowed。characterEncoding=utf8管字符集,这里注意只能写utf8,不能写utf-8,因为 MySQL 的 charset 名不带横线。

常用参数汇总如下:

参数默认值含义本地开发推荐
useSSLtrue是否启用 SSL 加密false
sslModePREFERRED8.0.27+ 的 SSL 模式DISABLED
serverTimezone无服务端时区Asia/Shanghai
allowPublicKeyRetrievalfalse是否允许获取 RSA 公钥true
characterEncoding自动检测客户端字符集utf8
rewriteBatchedStatementsfalse批量改写为多值插入true

sslMode=DISABLED和useSSL=false不能同时写,某些驱动版本会报参数冲突。生产环境不要因为本地跑了useSSL=false就照搬到线上,线上至少用sslMode=REQUIRED。时区建议用Asia/Shanghai而非GMT+8,因为在某些 Linux 发行版上Asia/Shanghai可能因为缺少 tzdata 而解析失败,这时再退回到GMT%2B8(注意+要 URL 编码成%2B)。这些参数建议统一放到application.properties里,代码里只读配置,不要散落在 DAO 里。

3. 用 Statement 还是 PreparedStatement:选型决定后续好不好维护

连接搞定了,接下来做增删改查。很多教程一上来就写 ResultSet 遍历,但我想先讨论选型:为什么增删改查要优先用 PreparedStatement,而不是用 Statement 拼字符串。这一步定了,后面所有代码的风格和安全性都由它决定。

3.1 Statement 拼接的 SQL 注入陷阱:你真的相信用户不会输入特殊符号

用 Statement 做查询最简单,但也是最容易翻车的写法。看下面这段登录校验代码:

String username = request.getParameter("username"); String password = request.getParameter("password"); Statement stmt = connection.createStatement(); String sql = "SELECT * FROM user WHERE username='" + username + "' AND password='" + password + "'"; ResultSet rs = stmt.executeQuery(sql);

如果用户输入的用户名是' OR '1'='1' --,最后的 SQL 就变成了SELECT * FROM user WHERE username='' OR '1'='1' -- ' AND password='...'。两个反常识的点:第一,--在 MySQL 里是注释,后面的AND password会被直接忽略;第二,OR '1'='1'让 WHERE 条件永远成立,这条 SQL 会把整张表返回。Statement 的问题不是它慢,而是它让 SQL 字符串和用户输入混在一起,没有词法边界。JDBC 本身不阻止这种拼接,所以只能靠写代码的人约束自己。

另一个问题在性能上。Statement 每次执行都要让数据库重新解析 SQL,PreparedStatement 则可以复用执行计划。单次操作没差别,但循环插入一万条时差距就明显了。更关键的是,PreparedStatement 的?占位符天然把数据和 SQL 结构隔开,就算用户输入' OR 1=1,它也只会被当成一个字符串字面量。我一般对增删改查一律使用 PreparedStatement,Statement 只会在动态拼接表名、排序字段这种无法预编译的场景才出现,而且这类字段必须做白名单校验。

3.2 增删改:PreparedStatement 的 executeUpdate 与参数绑定

先写增加操作的完整代码,这里不再用 main 方法,而是把逻辑拆成静态方法,体现可复用性。

public static void insertUser(Connection conn, User user) throws SQLException { String sql = "INSERT INTO t_user (username, age, email) VALUES (?, ?, ?)"; try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, user.getUsername()); ps.setInt(2, user.getAge()); ps.setString(3, user.getEmail()); int rows = ps.executeUpdate(); System.out.println("影响行数: " + rows); } }

executeUpdate返回的是受影响行数,对于 INSERT/UPDATE/DELETE 通常是 1 或大于 1 的数字,可用它判断操作是否成功。四个参数说明:setString绑定字符串,注意 MySQL 的VARCHAR对应 Java 的String;setInt绑定整数;setObject是个万能方法,但会丢失类型语义,不建议在正式代码里滥用;占位符下标从1开始,不是从0开始,这是我见过新手最容易写错的地方。执行完后 PreparedStatement 必须关闭,这里用 try-with-resources 包住,即使异常也会自动 close。

更新和删除结构一样,只是 SQL 不同:

public static int updateUser(Connection conn, int userId, String newEmail) throws SQLException { String sql = "UPDATE t_user SET email = ? WHERE id = ?"; try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, newEmail); ps.setInt(2, userId); return ps.executeUpdate(); } } public static int deleteUser(Connection conn, int userId) throws SQLException { String sql = "DELETE FROM t_user WHERE id = ?"; try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, userId); return ps.executeUpdate(); } }

这里返回int而不是boolean,原因是对 UPDATE 来说,影响 0 行并不代表失败,可能是条件不命中;调用方可以根据返回值决定是继续业务还是抛异常。参数顺序必须和 SQL 里的?顺序一一对应,改代码时经常出现先删了 SQL 里的字段却忘了调 setXxx 位置的失误,所以保持 "先改 SQL、再改绑定" 的顺序是个好习惯。另一个细节是 setTimestamp 对应 MySQL 的DATETIME/TIMESTAMP字段,如果 Java 侧传的是java.util.Date,要先转成java.sql.Timestamp,否则会报类型不匹配。

3.3 查:ResultSet 遍历、列名映射与结果集关闭顺序

查询是 CRUD 里最容易泄漏连接的环节。原因在于 ResultSet 依赖 Statement,Statement 依赖 Connection,关闭顺序错了,内存和连接都会被拖垮。看最小查询写法:

public static List<User> queryUsers(Connection conn, int minAge) throws SQLException { String sql = "SELECT id, username, age, email FROM t_user WHERE age >= ?"; List<User> result = new ArrayList<>(); try (PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { ps.setInt(1, minAge); while (rs.next()) { User user = new User(); user.setId(rs.getInt("id")); user.setUsername(rs.getString("username")); user.setAge(rs.getInt("age")); user.setEmail(rs.getString("email")); result.add(user); } } return result; }

注意try-with-resources同时管住了Statement和ResultSet,关闭顺序是反着的:先 close ResultSet,再 close Statement,这是 JDBC 规范里声明的顺序;Java 的 try-with-resources 会自动按声明逆序关闭,所以我把ps写在前面、rs写在后面,这样正常退出时会先关闭 rs,再关闭 ps。rs.next()是判断结果是否还有下一行,第一行数据要调用一次 next 才能拿到,很多人第一次写就忘掉这个调用。取列值可以用列索引rs.getInt(1),也可以用列名rs.getInt("id"),列名更可读,但如果 SQL 里写了别名,就必须用别名,比如SELECT COUNT(*) AS cnt就要用rs.getLong("cnt")而不是getLong(1)。

如果数据量不大,把结果集映射到 POJO 是合适的;但数据量大时不要在 while 里做复杂计算,否则会把数据库连接占住很久。一个常见翻车点是:在finally里重复关闭连接,导致二次异常;用 try-with-resources 能避免,但如果你的代码必须兼容 Java 7 以下,就用try { ... } finally { closeQuietly(rs); closeQuietly(ps); closeQuietly(conn); }。这些细节在面试里也很容易被追问,尤其是 "ResultSet 什么时候关闭" 这道题。

再补一张常用类型映射表,写代码时对照着用,能少踩一半的类型坑:

MySQL 列类型JDBC 取值方法推荐的 Java 类型
INTgetIntInteger
BIGINTgetLongLong
VARCHARgetStringString
DATETIME / TIMESTAMPgetTimestampjava.time.LocalDateTime
DECIMALgetBigDecimaljava.math.BigDecimal

如果你要处理超大结果集,尽量不要一次性SELECT *拉全量数据,而是加LIMIT分页,或者使用流式读取:conn.prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY)并设置较小的fetchSize。这个技巧对大表遍历有用,但对内存和游标都有额外要求,后续需要时再说,现在先把最基本的查询写对。

4. 把 CRUD 封装成 DAO:连接管理、事务边界与连接池选型

前面的代码都是单方法操作,实际项目里还得解决谁来负责拿连接、谁负责关连接、多个操作怎样保持一致。下面从 DAO 封装讲起,顺带把连接池选型说明白。

4.1 DAO 接口设计与工具类:别把 SQL 散落在 Service 层

常见做法是每个实体对应一个 DAO 接口和一个实现类,接口方法名直接叫insert/update/delete/selectById/selectList。接口不关心 SQL 细节,只暴露业务语义;实现类里才写 PreparedStatement。为了不重复写DriverManager.getConnection,我一般会做一个极简的DBUtils:

public class DBUtils { private static final String URL = "jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&characterEncoding=utf8"; private static final String USER = "root"; private static final String PASSWORD = "root123"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }

这个类的坑在于,如果它被用在多线程环境,每次getConnection都新建物理连接,数据库连接数会被迅速打满。所以它只能用于学习阶段或极小的工具项目。正式项目里要把 URL 和账号密码放到*.properties或配置中心,这里写死只是为了演示。静态代码块确保驱动只加载一次,避免每次拿连接都触发类加载。

典型 DAO 实现长这样:

public class UserDaoImpl implements UserDao { @Override public User selectById(int id) { String sql = "SELECT id, username, age, email FROM t_user WHERE id = ?"; try (Connection conn = DBUtils.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, id); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { return mapRow(rs); } } } catch (SQLException e) { throw new RuntimeException("查询失败", e); } return null; } }

这里mapRow(rs)是一个私有方法,负责把当前行的列值塞进 User 对象,避免每个方法里都写一遍 getXxx。接口层只暴露selectById(int id)这种语义化方法,调用方不需要知道表名和列名。事务不放在 DAO 里,而是由 Service 控制连接边界,这也是初学者最容易踩坑的地方:如果每个 DAO 方法都自己拿连接、自己提交,那么 "转账 A 扣钱、B 加钱" 这种操作就无法保证原子性。

4.2 手写连接工具类 vs 引入连接池:什么时候该换

继续用DriverManager.getConnection在连接数超过十几以后,性能会肉眼可见变差。因为每次连接都要经过 TCP 握手、MySQL 认证、权限检查,这个开销通常在几十毫秒到几百毫秒,而连接池把连接复用后,一次查询可能只要几毫秒。所以判断标准很简单:连接次数低的小工具、临时脚本,手写没问题;需要承接并发请求的应用,必须用连接池。

最常用的连接池是 HikariCP 和 Druid。HikariCP 轻量、默认值合理,适合 SpringBoot 项目;Druid 自带监控页面和 SQL 防注入能力,在需要可视化监控的时候更好用。两者的选择看团队习惯,我这里给一张客观对比表:

对比项DriverManagerHikariCPDruid
连接复用不支持支持支持
监控无基础指标自带监控台
配置复杂度无低中
推荐场景学习/脚本SpringBoot 默认需要 SQL 监控

引入 Druid 的配置示例(druid.properties):

driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username=root password=root123 initialSize=5 maxActive=20 minIdle=1 maxWait=60000

参数说明:initialSize是启动时初始连接数,maxActive是最大活跃连接数,minIdle是空闲时保底连接数,maxWait是拿连接的超时毫秒数。这些参数不是越大越好,maxActive=50时数据库自身的max_connections也要同步调大,否则连接池还没满,数据库已经拒绝新连接了。如果请求峰值在 100 并发,maxActive可以设成50,配合maxWait=30000,比设成 100 更稳。连接池还有一个容易被忽略的点:连接验证。Druid 的testWhileIdle默认为 true,会在拿到空闲连接时执行validationQuery,MySQL 常用SELECT 1。这样能避免数据库重启后,连接池里全是已失效的连接。

4.3 事务与批量操作:多语句原子性的实现与常见误区

先看一个典型转账场景,必须手动控制事务:

public void transfer(Connection conn, int fromId, int toId, double amount) throws SQLException { boolean autoCommitOld = conn.getAutoCommit(); conn.setAutoCommit(false); try { String sql1 = "UPDATE account SET balance = balance - ? WHERE id = ?"; try (PreparedStatement ps1 = conn.prepareStatement(sql1)) { ps1.setDouble(1, amount); ps1.setInt(2, fromId); ps1.executeUpdate(); } String sql2 = "UPDATE account SET balance = balance + ? WHERE id = ?"; try (PreparedStatement ps2 = conn.prepareStatement(sql2)) { ps2.setDouble(1, amount); ps2.setInt(2, toId); ps2.executeUpdate(); } conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(autoCommitOld); } }

setAutoCommit(false)关闭自动提交后,每条 SQL 都不会立刻落地,必须显式commit才算结束;任一步抛异常,rollback会把之前所有修改全部撤销。我见过不少人忘记在 finally 里恢复autoCommit,导致连接归还连接池后,下一位拿到这条连接的调用方还处在事务里,数据延迟可见。所以连接池场景下必须把setAutoCommit(true)恢复。conn.setAutoCommit(autoCommitOld)这样写比直接写true更稳,因为调用方可能原本设置过一次事务。

批量插入是另一个高频性能点。用 PreparedStatement 的addBatch/executeBatch,比循环 executeUpdate 快一个量级:

public void batchInsert(Connection conn, List<User> users) throws SQLException { String sql = "INSERT INTO t_user (username, age, email) VALUES (?, ?, ?)"; try (PreparedStatement ps = conn.prepareStatement(sql)) { conn.setAutoCommit(false); for (User user : users) { ps.setString(1, user.getUsername()); ps.setInt(2, user.getAge()); ps.setString(3, user.getEmail()); ps.addBatch(); } ps.executeBatch(); conn.commit(); } }

addBatch是把参数放入一批,executeBatch时 JDBC 驱动可能按rewriteBatchedStatements=true来决定是否合并成一条大的 INSERT 语句。这个连接参数如果不开,MySQL 驱动会一条条发给服务端,性能收益有限。可以在连接串里加上rewriteBatchedStatements=true,它能显著降低网络往返。批量操作时要注意批次大小,建议每 500 到 1000 条提交一次,避免单次事务过大导致 undo log 膨胀;如果中途数据质量差,也要保证在一个事务边界内,否则会出现部分成功、部分失败的脏状态。批量更新时executeBatch返回的 int 数组长度必须和 SQL 条数一致,如果发现长度对不上,优先检查是不是驱动版本差异。

最后说一个事务里容易犯的错:把查询操作也放在事务里,并且长时间不提交。比如SELECT ... FOR UPDATE会锁行,如果后面还跟着远程调用或用户输入等待,事务会一直持有锁。遇到这类问题的典型表现是另一个线程在同一个 id 上UPDATE卡住。解决思路是把耗时操作移到事务外,事务里只保留真正的写入语句。如果你用连接池,还要注意同一个事务必须用同一个 Connection,不能在 DAO 方法里拿到不同连接,否则 commit 的是另一条连接,事务自然不生效。

5. JDBC 连接 MySQL 避坑指南:6 个翻车现场与排查路径

这部分写给照着上面代码写还是报错的人。JDBC 的问题几乎都集中在连接和数据类型两层,我把最常见的翻车现场按现象、原因、解决三步写清楚。这里的每一个问题都是我在实际项目里遇到过的,不是从报错文档里抄来的。

5.1 连接层翻车:serverTimezone、Public Key Retrieval 和 No suitable driver

现象:启动程序报The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized or represents more than one time zone,或者中文乱码。

原因:MySQL 8.0 的 serverTimezone 默认值在某些系统下返回的不是标准时区标识,JDBC 驱动无法解析。另一个原因是characterEncoding没设置,或者设置了utf-8这种带横线的非法字符集名。

解决:在连接串 URL 后追加serverTimezone=Asia/Shanghai,字符集写characterEncoding=utf8。如果不想在代码里写死,也可以在 my.cnf 的[mysqld]段设置default-time-zone='+08:00'。注意时区不要写成Asia/Shanghai以外的冷门名称,某些驱动版本不识别。还有低版本 JDK 里Asia/Shanghai需要依赖 tzdata 库,老系统可能识别失败,可以改写成serverTimezone=GMT%2B8,但这时查询结果里的时间会少 8 小时,需要谨慎。

现象:第一次连接报Public Key Retrieval is not allowed,即使密码正确也连不上。

原因:MySQL 8 默认认证插件是caching_sha2_password。客户端在非 SSL 连接下要获取服务端 RSA 公钥来加密密码,但驱动默认不允许直接请求公钥,属于安全限制。

解决:在连接串里加allowPublicKeyRetrieval=true。这个参数只推荐在受信任的内网或本地开发环境开启,生产环境更合适的是单独配置 SSL,让公钥通过证书通道下发。如果不想开这个参数,可以给 MySQL 用户改成mysql_native_password认证,但新版本 MySQL 对这种插件支持正在弱化,不建议为了省参数而改认证插件。

现象:老项目里出现No suitable driver found for jdbc:mysql://...。

原因:驱动 jar 没打进 classpath,或者使用了 8.0 之前的老驱动包但连接串写的是com.mysql.cj.jdbc.Driver触发类加载失败,也可能是 SPI 自动加载没生效。

解决:先确认 Maven 依赖有没有scope=provided之类问题;再用jar tf mysql-connector-j-8.x.jar | grep Driver看驱动类是否在包里;最后保留显式Class.forName("com.mysql.cj.jdbc.Driver"),这是最直接的稳定办法。注意 5.x 的驱动类是com.mysql.jdbc.Driver,8.x 的驱动类是com.mysql.cj.jdbc.Driver,二者不能混写。

5.2 数据读写层翻车:Access denied、连接泄漏和类型不匹配

现象:报Access denied for user 'root'@'localhost' (using password: YES)。

原因:多数时候不是密码错,而是 MySQL 的 host 匹配。MySQL 里用户由user + host共同组成,例如root@'%'和root@'localhost'是两个不同账号。Java 程序通过 TCP 连接到localhost,命中的是root@'localhost',如果只创建了'root'@'%',就会匹配不到。

解决:用SELECT user, host FROM mysql.user;查看现有账号,如果程序连接串写的是localhost,就确保存在root@'localhost';如果写的是127.0.0.1,虽然通常等价,但某些系统会区分localhost和127.0.0.1。开发环境最简单的做法是创建'root'@'%',但生产环境必须按来源 IP 收紧。这个报错还可能是密码里包含特殊字符但没有正确配置 URL encode,比如密码是a&b,在 URL 里&会被当成参数分隔符,导致连接串解析错乱。这种情况要把密码放到Properties里通过DriverManager.getConnection(url, props)传入。

现象:程序运行一段时间后,数据库连接数被打满,报Too many connections,或者应用频繁卡顿。

原因:代码里Connection/Statement/ResultSet没有正确关闭,尤其是 ResultSet 和 Statement 没关,连接最终泄漏。另一个常见原因是连接池maxActive设置得比数据库max_connections还大,或者 SQL 执行时间过长,把连接占住不放。

解决:严格遵循"连接、语句、结果集"的关闭顺序,写成 try-with-resources;使用连接池时设置validationQuery和testWhileIdle,把失效连接剔除。排查时先执行SHOW PROCESSLIST;看是否有大量Sleep状态的连接,再根据 host/db 定位是哪一个应用。如果是慢 SQL 占用连接,就需要去看 SQL 的执行计划,而不是单纯调大连接数;调大连接数只是把症状往后推,慢了还会爆。也可以在连接池参数里把maxActive降到30,配合maxWait=5000,让等待超时的请求快速失败,避免全部线程堆积。

现象:setObject传入java.util.Date后,MySQL 报Incorrect date value,或者BigDecimal精度丢失。

原因:java.util.Date和java.sql.Date不是同一个类型,JDBC 驱动期望的是java.sql.Timestamp;另外Float/Double直接写进DECIMAL字段会有精度误差。

解决:对日期统一用java.time.LocalDateTime并调用ps.setTimestamp(1, Timestamp.valueOf(localDateTime));金额字段用BigDecimal配合setBigDecimal,数据库侧用DECIMAL(10,2)。取数侧同样注意:rs.getDate返回java.sql.Date,如果业务要用LocalDate,就rs.getDate(...).toLocalDate()。类型映射这块没有玄学,就是 JDBC 的映射表和 MySQL 的列类型要一一对上,平时写代码时多看一眼建表语句能省很多排查时间。

6. 把 CRUD 从"能跑"推进到"能扛住":验证方法、连接池参数与慢 SQL 定位

最后这部分不说新 API,而是教你如何验证自己写的 CRUD 是否合格。我习惯先用一段压测代码看超时和吞吐,再通过连接池日志和 MySQL 慢查询日志定位瓶颈。验证 CRUD 的最简单方式是写一个基于 JDBC 的循环请求,观察 P99 和错误率。

public class CrudSmokeTest { public static void main(String[] args) throws Exception { long start = System.currentTimeMillis(); for (int i = 0; i < 1000; i++) { UserDao dao = new UserDaoImpl(); dao.insertUser(new User("user" + i, 20 + (i % 20), i + "@test.com")); } long cost = System.currentTimeMillis() - start; System.out.println("1000 次插入耗时 " + cost + " ms"); } }

这里每次循环都通过 DAO 拿新连接,在连接池下能撑住,但在手写DriverManager.getConnection下会非常慢。所以这个测试的真正用途是确认连接池是否生效:如果加了 HikariCP 或 Druid 后耗时明显下降,说明连接复用正常。压测时建议把maxActive从 20 调到 5,刻意制造等待,观察maxWait的体现;如果看到很多Connection is not available, request timed out,就要回头查是不是 SQL 锁等待,而不是先调大连接数。

慢 SQL 的定位我一般做三件事:一是在 MySQL 里执行SHOW VARIABLES LIKE 'slow_query_log';确认慢查询开关,二是用mysqldumpslow -s t聚合慢查询日志,三是在连接池监控里看活跃连接数。真正让 CRUD 变快的手段通常不在 JDBC 代码里,而在索引、批量提交、避免SELECT *这三件事上。比如WHERE age >= ?的字段没索引时,MySQL 全表扫描,连接再多也扛不住。我踩过最后悔的一个坑是:把maxActive调到 100 后以为高枕无忧,结果数据库 CPU 100%,一查是一条LIKE '%xxx%'把缓存全打穿。从那以后,我每次写完 JDBC 代码都会顺手用EXPLAIN查一下执行计划,这个习惯比任何连接池调参都管用。

新手上路建议先不做任何优化,把 CRUD 的每一个try-with-resources都写对,然后跑一次上面的压测;接着把连接串参数、连接池参数抄到配置文件里,感受不同maxWait带来的失败表现;最后再看慢查询日志。把这三步走完,你对 JDBC 的理解就已经超过绝大多数只会背八股文的人了。希望这些踩坑记录能帮你少走点弯路。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 12:09:05

自适应PSO算法在梯级水电站优化调度中的应用

1. 项目背景与核心价值 水电作为清洁可再生能源的代表&#xff0c;其调度优化一直是能源领域的重点研究方向。梯级水电站群由于存在水力、电力、时空等多重耦合约束&#xff0c;其优化调度问题比单一水电站复杂一个数量级。传统调度方法往往面临维度灾难、收敛速度慢、易陷入局…

作者头像 李华
网站建设 2026/10/14 12:08:51

ppt-master:可编程幻灯片生成引擎,实现PPT自动化编译

1. 项目概述&#xff1a;这不是一个PPT模板库&#xff0c;而是一套可编程的幻灯片生成引擎“ppt-master”这个名字乍一听像某个网红PPT资源站&#xff0c;但实际接触过的人很快会发现——它根本不是传统意义上的模板合集。我第一次在某高校数字媒体实验室看到它被用于自动化生成…

作者头像 李华
网站建设 2026/10/14 12:08:05

ArtCraft:面向非设计师的视觉生产力系统

1. 项目概述&#xff1a;ArtCraft 不是“艺术工艺”的简单拼接&#xff0c;而是一套可落地的创意生产系统ArtCraft 这个词乍看像英文直译——art&#xff08;艺术&#xff09;和 craft&#xff08;手工艺&#xff09;的组合&#xff0c;但如果你真去翻最近三个月的设计类社区、…

作者头像 李华
网站建设 2026/10/14 12:07:41

主成分分析PCA核心原理与实战:从协方差矩阵到降维建模

1. 为什么PCA被当成"终极方案"&#xff1a;先看它解决的是什么问题我第一次真正被PCA打动&#xff0c;不是在高维数据竞赛里冲榜的时候&#xff0c;而是处理一批某个传感器采集系统的样本——当时特征维度已经堆到200多&#xff0c;但样本量只有80个出头。模型训练倒…

作者头像 李华
网站建设 2026/10/14 12:07:40

Transformer 多头注意力为什么要分成多个头?

多头让模型拿到多套独立的"信息路由规则",单头无论做多宽只有一套。这句话经得起推敲。我想把它往下拆一层:一套和八套的差距有多大,"单头会平均化"这句话里的平均,代价落在什么量级。 我用一个最小合成任务把"两种路由冲突"摆到明面上,把…

作者头像 李华
网站建设 2026/10/14 12:07:11

PPT工程化方法论:Slide Master六层防御设计

1. “PPT-Master”不是软件&#xff0c;而是一套可复用的PPT工程化方法论 你可能在某次团队汇报前夜&#xff0c;被临时拉进一个共享文档&#xff0c;看到标题栏写着“PPT-Master_v2.3_final_真的final.pptx”&#xff0c;点开后发现&#xff1a;母版页有7套配色变体、字体嵌入…

作者头像 李华