
从一开始接触Java Web前面几篇讲的都是Servlet、JSP这些偏展示层的东西数据基本都是写死在代码里或者临时塞进session页面一刷新啥都没了。从这一篇开始我们要进入整个Web开发最核心、也最绕不开的环节——web数据库开发。这篇的主角是JDBC和MySQL。先给没接触过的朋友说清楚这是干嘛的。JDBC是Java官方提供的一套操作数据库的标准接口你可以把它理解成一个翻译官负责把Java代码里的指令翻译成MySQL听得懂的SQL语句再把查出来的结果翻译回Java对象。而MySQL是目前中小型Web项目里占有率最高的开源关系型数据库免费、上手快、资料多和JDBC搭配几乎是Java Web入门的必修课。这篇文章会从为什么需要JDBC讲起一路带到环境搭建、第一个连接、增删改查、事务控制最后把我在实际项目里踩过的坑和排查思路一并整理出来。适合刚学完Servlet准备进入数据操作阶段的同学也适合那些用了很久MyBatis却没正经写过原生JDBC、想补一补地基的同行。1. 整体设计与思路拆解1.1 JDBC到底解决了什么问题在没有JDBC之前Java程序每操作一种数据库就得学习这一家厂商提供的专用API。今天用MySQL要学一套明天项目切到PostgreSQL又得学另一套程序员的时间全耗在适配不同数据库上了。JDBC的出现就是把这块统一了Sun公司定义好一套标准接口数据库厂商按这套规范去实现各自的驱动Java程序员只需要面向JDBC接口写代码底层连的是哪家数据库对业务代码来说基本是透明的。这个设计思路对我们做Web开发的启发很大。你想想今天项目里用MySQL明天客户要求换成别的数据库理论上只要换驱动和连接URL业务层的JDBC代码一行都不用动。当然实际项目里很少有人真这么干因为不同数据库的SQL方言多少有差异但接口统一的思路让Java生态的数据库访问层有了一个稳定的地基。后面学MyBatis、Hibernate这些ORM框架的时候你会发现它们底层干的事本质上还是JDBC那一套只不过把重复代码封装掉了。地基打不牢后面学框架容易越学越虚。1.2 方案选型背后的取舍有人可能会问既然ORM框架这么流行为什么不直接学MyBatis还要先啃一遍原生JDBC我的看法是跳过JDBC直接学框架就像没学过手动挡直接开自动挡车是能跑但你永远不知道变速箱里发生了什么。MyBatis帮你封装了结果集映射、参数绑定、连接管理但一旦遇到SQL性能问题、连接池耗尽、批量插入太慢这类线上故障如果不懂底层原理你连排查的方向都没有。从技术选型的角度说JDBC这一层提供的核心能力——建立连接、执行SQL、处理结果集、控制事务、管理批处理——是任何数据库框架都无法绕开的基础。所以这篇的主要内容就围绕这几个核心能力展开不算多但每一样都是后面所有数据库开发技能的地基。数据库选型上选了MySQL而不是别的一方面是因为它对学习者最友好安装维护成本低社区问答资料铺天盖地另一方面是Java Web的岗位需求里MySQL的使用率确实排在最前面学了立刻能用上。1.3 JDBC在整个Java Web服务链路中的位置一个典型的Java Web请求从浏览器发出到最终把数据存进数据库中间经过了这样一条链路浏览器发出HTTP请求Servlet容器比如Tomcat把请求交给Servlet处理Servlet调用Service层业务类Service层调用DAO层数据访问对象DAO层内部通过JDBC去操作MySQL数据库。JDBC处在整个链路最底部它向上给DAO层提供稳定的数据访问能力向下屏蔽了数据库的差异。把这条链路画在脑子里非常重要因为你会发现一个问题每次请求都从零开始创建数据库连接是一件开销很大且很低效的事情。这也是为什么后来会出现连接池技术HikariCP、Druid这些工具本质上就是提前把一批已经创建好的连接放在池子里反复使用避免反复握手和认证的消耗。理解了这条链路和连接池存在的理由你再去看Spring框架里DataSource、MyBatis的SqlSessionFactory思路就顺了它们无非就是在JDBC这层地基上不断做上层的工程化优化。2. 工具选型解析版本、驱动与连接池2.1 MySQL版本怎么选5.7还是8.0做Java Web开发选数据库版本是个很实际的问题。现在主流有两种选择老牌稳定的MySQL 5.7和功能更强但配置更讲究的MySQL 8.0。我的建议是新项目直接上8.0没人拦着但如果你的目标环境是某些老系统或者公司还在用5.7那你最好对两者差异心里有数。差异主要体现在几个地方。首先MySQL 8.0默认的认证插件是caching_sha2_password而5.7用的是mysql_native_password。JDBC连接的时候驱动的版本和认证方式必须匹配否则报错都是很奇怪的类型。我见过有人在8.0数据库上用了5.1系列的老驱动结果连接直接失败报错信息还不直白排查半天才知道是认证插件不兼容。其次8.0对时区处理和字符集的理解更严格连接URL里必须配置serverTimezone5.7在多数情况下不配置也能连上。第三8.0内置了窗口函数、CTE这些现代SQL特性写复杂统计查询会舒服很多。对比项MySQL 5.7MySQL 8.0默认认证插件mysql_native_passwordcaching_sha2_passwordJDBC驱动要求5.1.x以上即可推荐8.0.x驱动时区参数一般可不写必须写serverTimezoneSQL现代特性基础窗口函数、CTE运维资料量极多较多且仍在增长2.2 驱动包版本对应关系JDBC要想连上MySQL必须引入对应的驱动包也就是mysql-connector-java这个jar。这里最容易踩的坑就是驱动版本和数据库版本对不上。驱动包的命名也有讲究老版本叫com.mysql.jdbc.Driver从8.0开始改成了com.mysql.cj.jdbc.Driver如果你在网上抄了一段老代码把旧的类名搬到新驱动上会直接报ClassNotFoundException。我给你的建议是别在这上面纠结就用当前官网最新的8.0.x版驱动。老版本驱动能兼容新数据库吗运气好能连上但认证、时区、字符集这些细节会有很多隐患。反过来新版驱动连老版本数据库一般没问题因为MySQL做了向下兼容。Maven项目里加依赖很方便直接引入mysql-connector-j就好了。如果你还在用老项目里的5.1.x驱动我强烈建议换掉不只是兼容性问题老驱动在安全性上也有已知漏洞而且对utf8mb4的支持不够完善遇到表情符号存不进数据库就懵了。2.3 连接池从原生JDBC到HikariCP说到工具选型必须聊一聊连接池。很多初学者打开连接、执行SQL、关闭连接一套写完觉得挺顺的没什么问题。但等你上线一个稍有流量的系统就会发现频繁创建和销毁数据库连接的成本高到难以接受。一次连接的建立要经过TCP握手、身份认证、权限检查快的十几毫秒慢的时候几十毫秒高并发下每一秒都在重复做这些事数据库不卡才怪。解决方式就是连接池。它维护了一个连接缓存用的时候从池子里借用完还回去而不是销毁重建。常见的连接池有DBCP、C3P0、Druid、HikariCP目前我推荐HikariCPSpring Boot 2.x默认集成的就是它性能好配置简洁。Druid在中文社区里也很流行自带监控页面做运维排查的时候很有用。这篇文章里我先把原生JDBC讲透让你自己体会到连接不够用、反复创建很肉疼的问题然后再去接连接池思路自然就通了。3. 环境准备与数据库初始化3.1 MySQL解压版安装全流程Windows案例正式写代码之前先把MySQL装起来。这里我不推荐用安装包装而是用zip解压版更干净能清楚看到MySQL的初始化过程排查问题也方便。第一步从官网下载MySQL Community Server的zip包8.0.x就行系统架构选对64位就选x64。下载完解压到一个没有空格和中文字符的目录比如D:\mysql-8.0.40-winx64。然后在该目录下新建一个文本文件命名为my.ini。文件内容我直接给你一份基础配置[mysqld] basedirD:/mysql-8.0.40-winx64 datadirD:/mysql-8.0.40-winx64/data port3306 character-set-serverutf8mb4 collation-serverutf8mb4_general_ci default-authentication-pluginmysql_native_password [client] port3306 default-character-setutf8mb4注意basedir和datadir的路径换成你自己的实际路径而且要用正斜杠。这里的datadir指向的data目录不需要你自己创建初始化命令会生成。然后以管理员身份打开命令提示符进入bin目录执行初始化命令mysqld --initialize --console这个命令会创建一个默认的root账号并且生成一个临时密码输出在控制台里一定先复制下来后面的修改步骤要用。之后执行mysqld --install把MySQL注册为Windows服务再用net start mysql启动服务。启动成功后用刚才记下的临时密码登录mysql -u root -p登录进去之后立刻修改root密码顺便创建一个项目专用账号这个习惯一定要养成别拿root账号去跑Web应用权限太大了。3.2 my.ini配置与初始化参数配置这块我把几个关键参数单独拎出来讲一下因为很多新手不知道每个配置是干嘛用的出了问题也不知道该调哪。character-set-serverutf8mb4是全局字符集utf8mb4是真正的四字节UTF-8能存emoji和生僻字比老式的utf8更完善现在做新项目基本是标配。collation-server指定了排序规则utf8mb4_general_ci适合大多数场景排序比较快。如果你的业务涉及多种语言的精确排序可以考虑utf8mb4_unicode_ci。这个细节平时没感觉一旦遇到查询结果排序不对的诡异问题回来看这里就能找到原因。port3306是数据库监听端口默认即可除非你的机器上3306被其他服务占了才需要改。如果要改记得后面JDBC的URL里也要同步修改。另外init命令里的--initialize是MySQL 5.7之后的标准初始化方式它会在datadir下生成系统库和系统表同时创建超级管理员账号。如果你用的是老教程里的mysql_install_db脚本在新版本上已经不好使了。3.3 建库建表与账号授权数据库服务跑起来接下来就该建库建表了。遵循最小权限原则我给Web应用创建一个专门的数据库用户账号叫study配上密码只授予它对指定数据库的操作权限。用root登录后执行CREATE DATABASE IF NOT EXISTS web_demo DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci; CREATE USER studylocalhost IDENTIFIED BY Study123456; GRANT ALL PRIVILEGES ON web_demo.* TO studylocalhost; FLUSH PRIVILEGES;这一段里localhost表示这个账号只允许从本机连接。如果应用和数据库在不同的机器上这个位置要换成应用服务器的IP或者用通配符%但通配符在公网环境有安全风险要注意。然后切到study账号创建一张用户表USE web_demo; CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, username VARCHAR(50) NOT NULL COMMENT 用户名, password VARCHAR(100) NOT NULL COMMENT 密码, email VARCHAR(100) DEFAULT NULL COMMENT 邮箱, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;建表的时候有几个习惯值得坚持。主键用BIGINT自增不要用UUID当主键因为InnoDB是索引组织表UUID的随机性会导致频繁的页分裂数据量大之后写入性能会很难看。varchar的宽度够用就行不要一上来就255。时间字段用DATETIME而不是VARCHAR否则连时间比较这类基础操作都得在应用层做类型转换。4. JDBC核心操作实现4.1 第一个JDBC程序从建立连接到关闭连接环境准备好进入重头戏。先写一个最原始的JDBC连接代码控制在最小可运行范围。在项目里添加驱动依赖Maven就这样加dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId version8.0.33/version /dependency然后写连接代码import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class JdbcDemo { public static void main(String[] args) { String url jdbc:mysql://localhost:3306/web_demo ?useSSLfalseserverTimezoneAsia/Shanghai useUnicodetruecharacterEncodingutf8; String user study; String password Study123456; try { Connection conn DriverManager.getConnection(url, user, password); System.out.println(连接成功 conn.getClass().getName()); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }这段代码里最值得分析的就是那个URL。jdbc:mysql://是协议头localhost:3306是地址和端口web_demo是数据库名。后面三个参数的用意分别是useSSLfalse表示关闭SSL加密连接本地开发开着反而会遇到证书警告serverTimezoneAsia/Shanghai告诉驱动使用东八区时间避免服务器和客户端时区不一致导致日期错乱useUnicodetrue和characterEncodingutf8则确保传输过程中中文不乱码。如果你用的是8.0驱动连接前其实不需要手动加载Class.forName(com.mysql.cj.jdbc.Driver)因为JDBC 4.0以后驱动jar包里的META-INF/services服务描述文件会自动完成注册。但很多老教程里还保留着加载驱动的写法写上也不算错老项目的代码里你也经常能看到知道这回事就行。4.2 Statement与PreparedStatement为什么必须用后者新手很容易在代码里直接写Statement然后用字符串拼接SQL比如这样Statement stmt conn.createStatement(); String sql SELECT * FROM sys_user WHERE username name ; ResultSet rs stmt.executeQuery(sql);这个写法在功能上是能跑的但它同时踩了三个坑。第一是SQL注入风险name如果传进来一段包含单引号和OR条件的参数你的查询逻辑就会被改写比如传入admin OR 11那这条SQL就变成了无条件查询直接绕过认证这个安全问题在Web开发里是致命的。第二是每次执行都要重新解析SQL语句执行效率低。第三是代码难读拼接一长串SQL维护起来谁看谁头疼。正确做法是用PreparedStatementString sql SELECT * FROM sys_user WHERE username ?; PreparedStatement ps conn.prepareStatement(sql); ps.setString(1, name); ResultSet rs ps.executeQuery();先编译SQL骨架再填充参数驱动的内部处理天然规避了单引号转义问题同时也让SQL模板与参数分离兼顾了安全和效率。上面那个只写了一个参数的例子实际查询可以写很多个?再按顺序setString、setInt、setDate代码直观很多。这个习惯从今天开始就应该刻进肌肉记忆里不光是做工程师面试要考线上系统被注入攻击的案例这几年我看得太多了。4.3 CRUD实操增删改查完整示例不用框架直接用原生JDBC把一张表的增删改查完整写一遍你会对数据访问层的工作量有一定体感。我以sys_user表为例写一个简单的DAO方法。新增记录public int insertUser(User user) throws SQLException { String sql INSERT INTO sys_user(username, password, email) VALUES(?, ?, ?); try (Connection conn DriverManager.getConnection(url, user, password); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, user.getUsername()); ps.setString(2, user.getPassword()); ps.setString(3, user.getEmail()); return ps.executeUpdate(); } }注意我用了try-with-resources语法Java 7之后这个写法非常值得养成习惯它能保证Connection、PreparedStatement这些资源在使用完后自动关闭哪怕中途抛异常也不会漏关。以前老代码里到处是finally块里判断null然后close代码又长又容易漏这里一次搞定。查询走executeQuery返回ResultSet再把数据手动塞进Java对象public User findById(Long id) throws SQLException { String sql SELECT * FROM sys_user WHERE id ?; try (Connection conn DriverManager.getConnection(url, user, password); PreparedStatement ps conn.prepareStatement(sql)) { ps.setLong(1, id); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { User u new User(); u.setId(rs.getLong(id)); u.setUsername(rs.getString(username)); u.setEmail(rs.getString(email)); return u; } return null; } } }ResultSet刚开始用会有点别扭它维护了一个游标初始化时指向第一行之前必须调用next()才能移动到第一行每调一次就下移一行。如果返回的是多行结果就得while (rs.next())循环。等之后用了MyBatis你会发现这些代码全被框架隐藏了现在亲手写一遍能加深对ORM本质是把结果集弄进对象的理解。4.4 批处理与事务控制前面说的都是单条SQL日常开发里还有两类场景需要特殊处理批量插入多条数据以及一个业务流程里需要执行多条SQL并且要么全成功要么全失败。批量插入最简单的想法是for循环里一条条executeUpdate但这样每执行一条都有一次网络往返和SQL解析上万条数据跑起来会慢得让人怀疑人生。用addBatch和executeBatch可以把SQL攒起来一次性发给数据库public void batchInsert(ListUser users) throws SQLException { String sql INSERT INTO sys_user(username, password, email) VALUES(?, ?, ?); try (Connection conn DriverManager.getConnection(url, user, password); PreparedStatement ps conn.prepareStatement(sql)) { for (User u : users) { ps.setString(1, u.getUsername()); ps.setString(2, u.getPassword()); ps.setString(3, u.getEmail()); ps.addBatch(); } ps.executeBatch(); } }事务控制的场景更常见。比如创建用户并初始化一个工作空间插入用户表成功后就该插入工作空间表。如果第二步失败而第一步已经提交数据就脏了。解决办法是把两步包进同一个事务Connection conn DriverManager.getConnection(url, user, password); try { conn.setAutoCommit(false); // 执行插入用户 // 执行插入工作空间 conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); conn.close(); }setAutoCommit(false)就是告诉数据库不要立刻把每条SQL落盘到commit的时候才一次性生效。中途出异常就回滚已经执行过的SQL全部撤销。这个原子性是数据库事务的核心价值也是连接池、框架和高阶数据库知识反复登场的基础概念现在亲手体会一次比背一百遍定义都管用。5. 高频问题排查实录5.1 连接报错速查表根据我这些年不断在答疑群里看到的情况大家初次连MySQL时踩的坑高度集中我整理成一张速查表直接对照定位就行。报错关键字代表含义解决方案ClassNotFoundException: com.mysql.jdbc.Driver驱动类找不到确认驱动依赖是否引入8.0驱动要换成com.mysql.cj.jdbc.DriverAccess denied for user用户名密码或权限不对核对账号密码确认账号允许来源IP是否匹配Unknown database库不存在确认URL里的库名用root登录执行show databases没有就用CREATE DATABASE创建Server timezone value时区配置缺失URL上拼接serverTimezoneAsia/ShanghaiPublic Key Retrieval is not allowed8.0加密认证密钥问题URL拼接allowPublicKeyRetrievaltrueCommunications link failure服务未启动或网络不通检查MySQL服务状态尝试在命令行直接连一次确认端口没被占用5.2 时区、编码与安全参数配置经验时区问题值得单独拿出来说一说。很多新人第一天连接就栽在这里报错里带着Server timezone value CST is not recognized这段。原因是MySQL 8.0默认的时区设置和JDBC驱动的时区解析机制不对付解决方案就是上面表格里说的在URL后面加上serverTimezoneAsia/Shanghai。更稳妥的改法是把时区配置写到my.ini里设置default-time-zone08:00这样无论是用命令行、连接池还是可视化工具都不会再存在时区不匹配的问题。编码问题也很普遍。如果你发现中文存进数据库后变成问号要先确认三处统一my.ini里character-set-server的设置在入库前就固定了编码建库建表语句里指定了CHARSETutf8mb4JDBC的URL拼接了characterEncodingutf8。这三处只要有一处不一致就可能出现乱码或者部分字符丢失。另外强调一个容易被忽视的点旧数据库里有些表还停留在latin1编码你要用新代码去读写中文多半会出乱码这种情况需要单独处理表编码而不是只改连接参数。5.3 连接泄漏与性能隐患最后一个要讲的问题不是报错而是那些不会立刻报错、但会慢慢拖垮系统的隐患。最常见的就是连接泄漏——你打开了一个Connection但忘记关闭。最开始系统一切正常随着请求量增长数据库的max_connections逐渐被打满后续请求全部抛Too many connections异常应用直接卡死。这个问题尤其容易出现在正常路径有close但异常路径没有close的代码里比如查询抛了SQLException后面的关闭语句根本没机会执行。所以我在上面所有的示例代码里都用了try-with-resources这不仅仅是为了少写几行代码它编码强制保证资源在finally阶段一定被关闭从根上规避了连接泄漏。如果你在使用过程中被迫在finally里手动close记住两个原则第一close放在finally里而不是try块的末尾第二处理ResultSet的循环里如果提前return也要确保close能执行到。另外项目一旦上了连接池获取连接的API变成dataSource.getConnection()用完同样要close但此时close不是真关闭而是归还给池子所以规则是一样的谁借走、谁归还。个人体会这一套JDBC加MySQL的组合看起来是入门内容但我在实际写代码这些年的感受是它恰恰是每一位Java Web开发者的任督二脉。你之后用MyBatis、用Spring Data JPA甚至去做大数据方向的同步任务底层绕来绕去都绕不开驱动、连接、ResultSet、事务这几个概念。早年我写过一段时间只会上层框架、完全不会原生JDBC的代码后来遇到一次线上批量数据性能问题真是两眼一抹黑被迫回头补了JDBC的课才把问题定位清楚。所以别觉得这章内容简单或者过时把这些基础磨扎实了后面学任何数据库相关的框架都会顺很多。别偷懒照着代码本地跑一遍数据库多试两遍你会回来感谢那个愿意花半天时间敲JDBC的自己。