恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Java连接MySQL与数据交互:从JDBC到连接池的完整实战指南
首页
资讯中心
/
Java连接MySQL与数据交互:从JDBC到连接池的完整实战指南
Java连接MySQL与数据交互:从JDBC到连接池的完整实战指南
发布时间:2026/10/19 19:05:09
用 Java 连接 MySQL 数据库看起来是个老掉牙的话题但真正自己动手写过一遍的人都知道这里面的坑比想象中多得多。尤其是刚接触 Java 后端的朋友第一次调通 JDBC 代码心情往往不是“终于学会了”而是“总算跑通了”。我记得自己第一份项目里写 Java 连接 MySQL 时被 ClassNotFoundException、CommunicationsException、Access denied 轮番轰炸光一个数据库连接就折腾了一个下午后来才发现不过是时区参数没写。这篇内容就是想把 Java 连接 MySQL 并实现数据交互这件事彻底讲明白。核心关键词就两个Java 连接 MySQL、数据交互。我会从环境准备讲到驱动依赖再到完整的增删改查代码、事务与批量处理最后把生产环境必用的连接池和调试经验也一并打包。适合刚学 Java Web 的在校生、第一次在公司项目里写数据库操作的初级开发也适合那些框架用惯了、想回头补一补 JDBC 底层细节的朋友。把这套东西吃透后面你再去看 MyBatis、JPA会发现那些框架不过是一层漂亮的壳。1. 为什么还要手写 JDBC数据访问大厦的第一块砖很多初学者会问现在写 Java 项目十个里有九个用 Spring Boot MyBatis、JPA直接上手 JDBC 还有必要吗这个问题的答案要看你对自己代码的理解深度。1.1 JDBC 在 Java 数据访问体系里的定位JDBCJava Database Connectivity是 Java 官方定义的一套数据库访问接口规范。它定义了一套接口比如 Connection、Statement、ResultSet数据库厂商负责实现这些接口提供对应的驱动包。Java 代码通过这套统一接口操作不同的数据库不需要关心 MySQL 和 Oracle 之间的底层差异。打个比方JDBC 就像电源插座的标准规格MySQL 驱动、Oracle 驱动都是按照这个规格生产的插头。你的 Java 代码只对着插座写不用管背后接的是哪家发电厂的线。明白了这层关系你就知道 MyBatis、Hibernate 这些框架本质上是帮你把 JDBC 的样板代码封装了起来。你在 MyBatis 里写一条 SQL框架底层还是要创建 Connection、准备 PreparedStatement、执行 executeQuery只不过这些动作被隐藏了。所以框架入门容易遇到问题排查难。一旦 SQL 报错、连接池满、事务回滚不生效不懂 JDBC 的人往往不知从何下手懂的人扫一眼日志就能定位到是哪个环节出了问题。1.2 什么场景适合直接拿起 JDBC现在都推荐用框架动不动就上 Spring Boot但有些场景其实直接撸 JDBC 更香。一是小工具和命令行程序。我之前写过一些内部数据整理的极简工具就数据库连接加查询逻辑总共几百行。这种项目引入一整套 ORM 框架光是配置类和依赖就比你业务代码还长纯粹给自己找麻烦。二是性能敏感的数据批处理。ORM 框架在批量插入、大批量查询时的自动映射、缓存管理都会带来额外开销。JDBC 的 PreparedStatement 配合 addBatch、executeBatch可以发挥数据库驱动的极限性能这一点框架很难超越。三是学习框架原理。你只有亲手写过 JDBC 代码理解了连接从哪来、SQL 怎么执行、结果集怎么映射才能理解 MyBatis 具体做了什么、JPA 又为何这么“重”。我见过不少开发迁移个 Spring Boot 版本MyBatis 报一大堆错焦虑得不行其实就是底层 JDBC 驱动的行为变了不懂底层的就完全抓瞎。所以手写 JDBC 不是过时而是基本功。这篇文章教的这套东西是后续所有数据访问技术的基石。2. 准备环境与驱动依赖光有代码可不够真到了动手阶段第一步不是写代码而是核对你的开发环境。环境不匹配后面写的代码全是空中楼阁。2.1 JDK 与 MySQL 版本怎么配先说 JDK现在写新代码最低也要用 JDK 8能上 JDK 11 或者 17 更好。JDK 8 虽然还能用但 Java 11 之后的 DriverManager 加载机制有些细微变化加上 Spring Boot 3 已经强制要求 JDK 17建议新项目直接上 JDK 17省得以后再迁。MySQL 这边要分两种情况。本地学习、自己调试用 MySQL 5.7 或者 8.0 都可以8.0 的默认认证方式是 caching_sha2_password对驱动版本有要求。公司项目如果是老系统可能还在 5.6、5.7那驱动也要对应调整。我建议你本地直接装 MySQL 8.0 系列毕竟新项目基本都往这个版本走。版本核对具体分三项。第一MySQL 服务要正常启动命令行里能用 mysql -u root -p 登录进去。第二要提前建好数据库和测试表这些准备动作不要太随意因为后边代码要用到。第三记住端口号MySQL 默认 3306如果你用的是 XAMPP、宝塔这类集成环境端口可能被改过到时候连不上别怀疑代码先怀疑端口。2.2 驱动依赖怎么选这里有个大坑Java 连接 MySQL 需要驱动包最常见的叫 mysql-connector-j。这个名字有个历史坑老一点的版本叫 mysql-connector-java从 8.0.31 开始官方把 artifact 的 groupId 改成了 com.mysqlartifactId 改成了 mysql-connector-j。如果你在网上搜到的是老坐标复制到 Maven 里 configure 时可能有点绕。用 Maven 搭建工程直接在 pom.xml 里加依赖。这是最标准的写法dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId version8.0.33/version /dependency如果你是 5.x 的老驱动对应的依赖坐标是dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.49/version /dependency版本怎么选我的经验是用 MySQL 8.0 数据库就配 8.0.x 的驱动用 MySQL 5.7 数据库8.0.x 驱动也能兼容但有时会碰到连接属性不识别的问题。反过来5.1.x 驱动连 MySQL 8.0 数据库会直接报 Unable to load authentication plugin caching_sha2_password。所以最省心的组合是 MySQL 8.0 数据库 8.x 驱动想折腾的可以试试其他组合但别在版本匹配上浪费太多时间。另外需要注意 class 名称的差异。老驱动是 com.mysql.jdbc.Driver新驱动是 com.mysql.cj.jdbc.Driver。虽然 JDBC 4.0 以后驱动可以自动注册不需要手动加载但很多老项目的代码里仍然写了 Class.forName(com.mysql.jdbc.Driver)用新驱动时这一行会报 ClassNotFoundException解决方法是把类名换成新的。3. 完整实操从连接数据库到增删改查环境备齐依赖配好下面写代码。我建议你跟着这个流程走一遍比单纯读代码清楚得多。3.1 第一步建立数据库连接我们先解决从 Java 到 MySQL 的“握手”问题。下面这段代码是最基础的连接方式import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class JdbcConnectionDemo { public static void main(String[] args) { String url jdbc:mysql://localhost:3306/test_db ?useSSLfalse serverTimezoneAsia/Shanghai characterEncodingutf8; String user root; String password 你自己的密码; try (Connection conn DriverManager.getConnection(url, user, password)) { System.out.println(连接成功数据库是: conn.getCatalog()); } catch (SQLException e) { e.printStackTrace(); } } }这里面的 URL 是连接字符串必须严格按照固定格式来写。它由几个部分组成jdbc:mysql:// 是协议头localhost 是数据库主机地址3306 是端口test_db 是数据库名后面跟着的是连接参数。jdbc:mysql://主机地址:端口/数据库名?参数1值1参数2值2我第一次学的时候最容易漏掉的就是参数部分。三个参数里serverTimezone 几乎是必写的因为 MySQL 8.x 驱动默认要求指定时区不写大概率报 The server time zone value Öйú±ê׼ʱ¼ä is unrecognized一串乱码直接把人看懵。useSSLfalse 是为了避免本地连接时做 SSL 握手开发环境加上这个参数能节省连接时间。characterEncodingutf8 是为了保证中文不乱码如果你没写它插入中文数据时经常会出现问号或者乱码。这段代码用了 JDK 7 引入的 try-with-resources 语法Connection 是 AutoCloseable 的子类try 块结束自动关闭连接。这是一个好习惯避免忘记写 conn.close()。很多初学者一开始不爱用这个语法喜欢手工 close最后要么少了一个关闭要么在关闭时又抛出空指针反而不如这样干净。3.2 第二步使用 Statement 还是 PreparedStatement连接建立了接下来执行 SQL。这里有两个选择一个 Statement一个 PreparedStatement。我直接给结论写代码一律用 PreparedStatement除非你的 SQL 字符串里没有一处变量拼接。Statement 的典型写法是把 SQL 字符串拼出来Statement stmt conn.createStatement(); String sql SELECT * FROM user WHERE username username ; ResultSet rs stmt.executeQuery(sql);这种写法在 username 传入一段恶意字符串时会造成什么结果比如传入 OR 11整条 SQL 变成查询全部用户这就是 SQL 注入的经典攻击方式。我见过不少老系统就是这么被爬走数据的所以不要心存侥幸。PreparedStatement 的写法和它完全不同String sql SELECT * FROM user WHERE username ? AND password ?; PreparedStatement ps conn.prepareStatement(sql); ps.setString(1, username); ps.setString(2, password); ResultSet rs ps.executeQuery();参数在 SQL 里用问号占位后续通过 setString、setInt 这些方法把值传进去。驱动会把 SQL 结构和参数值分开处理参数值经过转义恶意字符串只是被当成普通字符串无法改变 SQL 结构。PreparedStatement 还有一个好处同样的 SQL 模板可以预编译重复执行时性能更好尤其适合循环插入数据的场景。3.3 第三步查询数据并处理结果集连接和 SQL 都就绪了下面我们写一个完整的查询流程。假设有一张 user 表包含 id、username、age 三个字段。代码可以这样写String sql SELECT id, username, age FROM user WHERE age ?; try (Connection conn DriverManager.getConnection(url, user, password); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, 20); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { int id rs.getInt(id); String username rs.getString(username); int age rs.getInt(age); System.out.println(ID id , 用户名 username , 年龄 age); } } } catch (SQLException e) { e.printStackTrace(); }ResultSet 是查询结果集可以理解为一个游标式的二维表。rs.next() 让游标移动到下一行如果有数据就返回 true没有数据就返回 false。所以 while (rs.next()) 是遍历结果集的标准循环。取列值的时候可以按下标取 rs.getInt(1)也可以按列名取 rs.getInt(id)我强烈建议用列名因为 SQL 一旦调整按列名还能保持代码可读性按标号很容易取错列。这里有个细节容易被忽略ResultSet 一旦关闭再想获取数据就来不及了。所以处理结果集的逻辑必须在 try 块内完成或者先把数据复制到一个 List 里连接关闭后还能继续使用。我通常这么处理ListUser userList new ArrayList(); while (rs.next()) { User u new User(); u.setId(rs.getInt(id)); u.setUsername(rs.getString(username)); u.setAge(rs.getInt(age)); userList.add(u); } // 出了 try 块连接关闭userList 依然可用3.4 第四步增删改操作与事务管理增删改和查询不同它们不返回结果集而是返回一个影响行数。用 executeUpdate() 执行返回值是 int代表被修改的行数。举个例子// 插入 String insertSql INSERT INTO user(id, username, age) VALUES (?, ?, ?); try (PreparedStatement ps conn.prepareStatement(insertSql)) { ps.setInt(1, 1); ps.setString(2, 张三); ps.setInt(3, 25); int rows ps.executeUpdate(); System.out.println(插入影响行数 rows); } // 更新 String updateSql UPDATE user SET age ? WHERE username ?; try (PreparedStatement ps conn.prepareStatement(updateSql)) { ps.setInt(1, 26); ps.setString(2, 张三); ps.executeUpdate(); } // 删除 String deleteSql DELETE FROM user WHERE username ?; try (PreparedStatement ps conn.prepareStatement(deleteSql)) { ps.setString(1, 张三); ps.executeUpdate(); }如果你只有一条 SQL上面的写法足够了。但实际业务里经常有两个操作必须同时成功或者同时失败。最典型的例子是转账A 账户扣钱、B 账户加钱两条 SQL 只要一条失败另一条就必须回滚否则账就不平了。JDBC 的事务控制有三个核心操作。默认情况下Connection 的 autocommit 是 true也就是每条 SQL 执行后自动提交我们要先关掉。然后执行多条 SQL全部成功后 commit()任何一个环节出问题就 rollback() 回滚最后在 finally 里恢复自动提交状态。代码是这样Connection conn null; try { conn DriverManager.getConnection(url, user, password); conn.setAutoCommit(false); String sql1 UPDATE account SET balance balance - 100 WHERE username A; String sql2 UPDATE account SET balance balance 100 WHERE username B; try (PreparedStatement ps1 conn.prepareStatement(sql1)) { ps1.executeUpdate(); } try (PreparedStatement ps2 conn.prepareStatement(sql2)) { ps2.executeUpdate(); } conn.commit(); } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); } finally { if (conn ! null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }这里注意close() 之前一定要先 setAutoCommit(true)否则连接回到连接池后下一个人拿到这个连接会因为 autocommit 还是 false 而导致行为异常。这种问题极难排查日志里看不到任何报错但到处是事务不生效、数据时有时无的诡异现象。批量操作也是生产环境的高频需求。如果要在循环里插入一万条数据每条都用 executeUpdate()性能极差。正确姿势是使用 addBatch executeBatchString sql INSERT INTO user(username, age) VALUES (?, ?); try (Connection conn DriverManager.getConnection(url, user, password); PreparedStatement ps conn.prepareStatement(sql)) { conn.setAutoCommit(false); for (int i 0; i 10000; i) { ps.setString(1, 用户 i); ps.setInt(2, 20 i % 50); ps.addBatch(); if (i % 1000 0) { ps.executeBatch(); } } ps.executeBatch(); conn.commit(); }每凑够一批就 executeBatch 一次而不是等全部积累完是在内存与速度之间取一个平衡。最终数量少时一次性提交没问题量大的时候分批是必须的。4. 从能跑到跑好生产环境必须直面的性能与安全问题把上面的代码跑通了你只是完成了“能连上、能增删改查”这一步。实际项目上线后还会面临连接管理、SQL 注入、连接耗尽这些新问题。4.1 SQL 注入到底是怎么回事上面提到用 PreparedStatement 代替 Statement这里展开说说原理。SQL 注入的本质是用户输入被拼进了 SQL 语句里变成了 SQL 的一部分。看一个例子String sql SELECT * FROM user WHERE username input ;如果用户输入的是admin --拼接之后变成SELECT * FROM user WHERE username admin -- 在 MySQL 里-- 后面是注释所以这条语句完全忽略了后面的单引号直接以 username admin 作为条件执行。如果你运气好数据库里恰好有 admin 用户数据就泄露了。更狠的攻击是 OR 11 --拼接后变成查询所有用户。PreparedStatement 之所以安全是因为它把 SQL 语句的结构和执行参数分开了。结构在 prepareStatement 时已经确定参数被当作了纯数据传给驱动驱动会做转义处理单引号变成普通字符无论如何都成不了 SQL 结构的一部分。这是官方推荐的第一道防线不需要任何额外配置只要你在写代码时养成用 PreparedStatement 的习惯就够了。4.2 连接池高并发下的生命线新手写 JDBC 代码每执行一次操作就新开一个连接用完就关在低并发本地测试没问题。但到了线上一个请求开一次连接连接建立和断开的开销非常大数据库连接数很快就撑不住了。数据库连接池可以简单理解为提前建好了一批连接放在池子里用的时候从池里取出一个用完关掉其实是归还到池子而不是真的断开。这样数据库服务端不用频繁创建和销毁线程应用端也能快速拿到可用连接。Java 生态里最常见的连接池是 HikariCPSpring Boot 2.x 之后默认使用它。HikariCP 为什么快它的字节码被高度优化过并发获取连接时锁的粒度更细连接有效性检测也更聪明。如果自己管理连接池可以这样配dependency groupIdcom.zaxxer/groupId artifactIdHikariCP/artifactId version5.0.1/version /dependencyHikariConfig config new HikariConfig(); config.setJdbcUrl(url); config.setUsername(user); config.setPassword(password); config.setMaximumPoolSize(20); config.setMinimumIdle(5); config.setConnectionTimeout(30000); config.setIdleTimeout(600000); HikariDataSource dataSource new HikariDataSource(config);配置参数里maximumPoolSize 不是越大越好通常和数据库实例的 CPU 核数相关。我见过有人直接配 200结果数据库连接被打满应用反而变慢。连接池的大小应该结合数据库性能和业务并发量来调一开始 10 到 20 个是合理的起点之后通过压测慢慢往上调。拿到 DataSource 之后原来的代码要改成从 dataSource.getConnection() 获取连接这样连接就由连接池来管理。如果你继续 new DriverManager.getConnection()等于完全绕过了连接池配置白做了。4.3 关于驱动自动加载的常见误解Java 6 之后JDBC 驱动支持通过 ServiceProvider 机制自动注册也就是 DriverManager 会在启动时扫描 classpath 下的 META-INF/services/java.sql.Driver 文件自动加载驱动类。所以新项目里写不写 Class.forName(com.mysql.cj.jdbc.Driver) 其实都可以。但老一点的项目里这行代码还是很普遍。为什么因为旧驱动不是自动注册的必须手动加载。而且有些容器环境下DriverManager 的自动发现机制可能失效写了 Class.forName 反而更稳妥。我的建议是写的时候不写无所谓但看到老代码里这行时不要惊讶更不要删了之后报错又怪别人。5. 常见问题排查与避坑实录跑一次就会遇到的意外代码写完了运行报错才是真正的开始。下面这些问题是初学者最容易碰到的我把它们整理成了一张速查表方便复盘时对照。报错信息常见原因解决方法Communications link failureMySQL 服务未启动、端口不对、防火墙拦截先确认 MySQL 是否启动telnet 3306 测试端口连通性Access denied for user rootlocalhost用户名或密码错误或者用户没有远程访问权限核对密码确认用户是否有对应库的权限Unknown database test_db数据库不存在先通过命令行 CREATE DATABASE 创建库ClassNotFoundException: com.mysql.cj.jdbc.Driver驱动依赖没加载或者类名写错检查 Maven 依赖是否生效确认类名是 cj 开头The server time zone value is unrecognizedURL 里没有指定 serverTimezone增加 serverTimezoneAsia/ShanghaiSSL connection errorURL 里没有禁用 SSL或本地 MySQL 不支持 SSL增加 useSSLfalse 仅限开发环境中文数据乱码连接字符集未设置或表本身字符集不是 utf8mb4URL 增加 characterEncodingutf8确认表字符集No operations allowed after connection closed连接已关闭但代码还在使用 Connection 或 ResultSet检查资源释放逻辑把取数放在 try 内完成第一类容易踩的坑是连接失败。Communications link failure 是新手最常看到的报错原因很多但排查思路是有迹可循的。先在命令行用 mysql -u root -p 连一下本机 MySQL如果命令行都连不上说明 MySQL 服务没起来跟 Java 代码没有任何关系。如果命令行能连上再用 telnet 127.0.0.1 3306 试一下端口通说明网络和服务都正常问题大概率出在代码的 URL、用户名或密码上。第二类坑是驱动版本不匹配。我之前就遇到过MySQL 8.0 数据库配了 5.1.x 的驱动项目启动时一直报 Unable to load authentication plugin其实就是因为新版本 MySQL 默认的加密插件变了老驱动不认。解决办法有两个要么把驱动升到 8.x要么给数据库新建一个 mysql_native_password 的用户但明显升驱动更简单。第三类坑是连接池配置不当。我见到过把 maximumPoolSize 配得异常大的结果应用启动时数据库 CPU 直接飙高因为连接池预热创建了大量连接。也有配置了连接池但代码里还是用 DriverManager 拿连接的白白空跑连接池。还有前面提到的 autocommit 没恢复这是最阴间的坑某条连接执行了事务没提交归还给连接池后下一个人拿这个连接继续做操作数据一直不落库排查半天才发现是连接状态被污染了。排查这类问题我的经验是关掉连接池直接用 DriverManager 裸连能跑通说明业务代码没问题剩下的锅基本在连接池配置上。反过来也一样如果裸连报错先排除驱动、URL、权限这些基础项。两层分开排查问题范围立刻缩小一半。6. 封个简易工具类JDBC 的第二次升华写到这里你已经有能力完成一次完整的 Java 连接 MySQL 数据交互了。但如果每个业务方法里都重复写获取连接、创建 PreparedStatement、遍历 ResultSet 的代码你会很快感到繁琐。实际项目中我们通常会封装一个 JDBC 工具类把连接的获取和资源的释放统一管理。我提供一个极简版的工具类思路你可以根据自己的项目再调整public class JdbcUtil { private static final String URL jdbc:mysql://localhost:3306/test_db ?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USER root; private static final String PASSWORD 你的密码; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, Statement stmt, ResultSet rs) { if (rs ! null) { try { rs.close(); } catch (SQLException e) { /* ignore */ } } if (stmt ! null) { try { stmt.close(); } catch (SQLException e) { /* ignore */ } } if (conn ! null) { try { conn.close(); } catch (SQLException e) { /* ignore */ } } } }注意工具类里我用 try 块包裹了 close 方法但忽略了异常。这不是不在乎异常而是关闭资源时的异常通常不会影响主要业务流程而且如果在 finally 里再抛出异常反而会掩盖前面的业务异常。这是一种权衡但也是线上代码常见的容错方式。有了工具类之后业务代码可以写成Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn JdbcUtil.getConnection(); ps conn.prepareStatement(SELECT * FROM user WHERE age ?); ps.setInt(1, 20); rs ps.executeQuery(); while (rs.next()) { System.out.println(rs.getString(username)); } } finally { JdbcUtil.close(conn, ps, rs); }这套代码已经比较接近公司里真正在用的风格了。如果再进一步你可以在这个基础上做一个简单的 BaseDao、泛型查询封装或者直接引入 Spring 的 JdbcTemplate。你会发现JdbcTemplate 的核心思想就是这个工具类的增强版连接统一管理、重复代码消除、异常转换。理解了 JDBC 这层你再看 Spring 的数据访问体系真的会有一层窗户纸被捅破的感觉。我记得自己第一次完整封装出这个工具类的时候心里特别踏实因为我知道哪怕离开框架我也能独立完成数据库操作。这种底层能力是面试时聊到数据库性能调优、事务边界、连接管理时能够深入对话的底气。这篇文章的内容到这里就完整了。如果你还在因为框架报错而一头雾水不妨像我当初一样找个周末从 DriverManager 开始不依赖任何框架手写一个 Java 连接 MySQL 的小项目把增删改查、事务、批量操作都过一遍你会收获一种“尽在掌握”的感觉。过程中遇到任何问题欢迎回来对照这篇文章一步一步排查。毕竟JDBC 踩过的每一个坑都是数据库编程这条路上的垫脚石。