news 2026/11/2 11:24:46

SpringBoot JDBC连接MySQL实战:驱动、连接池与排错指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot JDBC连接MySQL实战:驱动、连接池与排错指南

简介:面向Java后端初学者与Spring Boot入门开发者的JDBC连接MySQL实战资料包,围绕在Spring Boot项目中通过JDBC访问数据库这一常见需求,提供可运行的示例源码、SQL脚本与配套环境文件,适合课程实验、毕业设计原型及小型项目起步时对照学习。压缩包共136个文件,约637.42MB,包含3个java源码、5个class编译文件、3个sql脚本,以及大量dll运行库、exe与msi安装程序、xml和properties配置等,兼顾代码示例与依赖环境准备。已有601人学习下载,说明其具备一定的参考与复用价值。读者可据此梳理数据源配置、实体类与控制器调用、建表语句和依赖加载流程,并借助现成目录快速定位源码、脚本与安装文件,减少搭建环境和排查连接问题的时间。

1. 一套能直接跑的 SpringBoot JDBC 连接 MySQL 方案,解决的是哪一段

很多人接手 SpringBoot 项目,业务代码写得挺齐,一到连 MySQL 就卡在Communications link failure或者Public Key Retrieval is not allowed,翻半天文档也说不清是驱动、连接串还是权限的问题。这套带源码和安装包的 JDBC 连接方案,解决的就是从装库、配依赖到跑通增删改查这一整段:驱动坐标怎么选、连接串每个参数什么含义、连接池怎么配、事务和批量写入怎么写、报错怎么定位。它不适合已经天天写 ORM 框架、想找新轮子的人,适合刚接手 SpringBoot 项目、要把老 JDBC 代码搬进新工程、或者第一次独立搭数据访问层的开发者。下面按我拆包的顺序讲。

2. 环境对齐:JDK、驱动坐标、MySQL 三处版本怎么咬合

2.1 版本错配是启动失败的第一大来源

Spring Boot 2.x 和 3.x 在 JDBC 这一层的差别,不在JdbcTemplate的 API,而在两个地方:JDK 基线和 MySQL 驱动的 Maven 坐标。2.7.x 系列以 JDK 8 为基线,驱动坐标是mysql:mysql-connector-java;3.x 系列要求 JDK 17,坐标换成了com.mysql:mysql-connector-j。这两处不统一,Maven 照样能编译通过,运行期直接抛ClassNotFoundException: com.mysql.cj.jdbc.Driver或者No suitable driver found,报错信息还指向连接串,很容易误判。

我一般的做法是先在pom.xml里把spring-boot-starter-parent的版本钉死,再看本地 JDK 的java -version是否满足,最后才去写连接串。顺序反了就是白折腾。

Spring BootJDK 基线驱动坐标驱动类名
2.7.x8 / 11mysql:mysql-connector-javacom.mysql.cj.jdbc.Driver
3.x17com.mysql:mysql-connector-jcom.mysql.cj.jdbc.Driver

表里驱动版本统一写 8.0.x,是因为 MySQL 5.7 和 8.0 都能被 8.0 系列驱动连上,而 5.1.x 老驱动连 MySQL 8 会碰到认证插件不兼容。源码包里带的是 8.0.x 驱动,如果你的库是 5.7,不用降驱动,改连接串里的时区和 SSL 参数就行。

2.2 装库、建库、建账号:三行 SQL 定生死

安装包按默认选项装完,root密码设一个自己记得住的,然后打开命令行执行。别用图形工具点,第一次建库建号用命令行最不容易漏参数。

# 登录(-p 后不要直接跟密码,回车后再输入,避免进历史命令) mysql -u root -p # 建库:字符集必须是 utf8mb4,排序规则 8.0 用 0900_ai_ci CREATE DATABASE demo_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci; # 建业务账号,别让应用直接用 root CREATE USER 'app_user'@'%' IDENTIFIED BY 'App@123456'; # 只给这一个库的权限,够用即可 GRANT SELECT, INSERT, UPDATE, DELETE ON demo_db.* TO 'app_user'@'%'; FLUSH PRIVILEGES;

这几行的逻辑很直白:utf8mb4是为了存得下 Emoji 和生僻字,utf8在三字节上会截断;排序规则utf8mb4_0900_ai_ci只在 MySQL 8.0 存在,5.7 上要用utf8mb4_general_ci,否则建库语句直接报错。'app_user'@'%'里的%表示允许从任意主机连,本地开发图省事可以这么写,真要上生产就得收窄成具体网段。GRANT里我故意没给DROP,是因为见过测试环境一条误执行的DROP TABLE把演示数据清空的场面,权限收窄比事后恢复便宜得多。

建完库立刻验证一次:mysql -u app_user -p -h 127.0.0.1 -P 3306 demo_db。这一步能连上,后面 Java 里连不上就一定是应用侧的问题,排查范围直接砍一半。

2.3 pom.xml 依赖:只引必要的两个

源码包的pom.xml里 JDBC 相关的依赖只有两块:spring-boot-starter-jdbc和 MySQL 驱动。不需要spring-boot-starter-data-jpa,也不需要额外引连接池——starter-jdbc里已经带了 HikariCP。

<dependencies> <!-- 提供 JdbcTemplate、DataSource 自动装配、事务管理器 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!-- 驱动:Boot 2.x 用下面这个坐标;Boot 3.x 换成 com.mysql:mysql-connector-j --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> </dependencies>

scope写runtime是有讲究的:业务代码里不该出现任何com.mysql.*的 import,驱动只在运行期通过 SPI 加载。一旦你在代码里Class.forName("com.mysql.cj.jdbc.Driver"),就等于把驱动实现焊死在业务里,将来换库或升驱动都得改代码。常见做法是交给 Spring Boot 的自动装配,它在检测到 classpath 上有驱动、且配置文件里有spring.datasource.url时,自动建好DataSource和JdbcTemplate。

3. 四种连库写法对比:从 DriverManager 到 JdbcTemplate 的取舍与代码

3.1 DriverManager 和 DataSource 到底差在哪

DriverManager.getConnection()每次调用都会新建一条物理连接,用完close()就真断掉。写个定时任务循环一千次,就是一千次 TCP 握手加 MySQL 认证握手,本地测不出来,上了有网络延迟的环境就是灾难。DataSource是连接池的抽象,getConnection()是从池里借一条,close()是还回去,物理连接复用。

这也是为什么源码包里那条“原生 JDBC 演示”只作为对照存在,真正对外的是走DataSource的JdbcTemplate。选型上没有悬念:只要有并发,就必须用池化数据源。唯一适合DriverManager的场景是一次性数据迁移脚本,跑完就退出的那种。

3.2 连接串参数逐项拆开看

application.yml里那一行长 URL,是坑最密集的地方。逐项拆:

参数建议值作用与不写会怎样
serverTimezoneAsia/Shanghai不写时,驱动会拿服务端时区,遇到CST这类有歧义的缩写直接抛The server time zone value is unrecognized
characterEncodingutf8配合useUnicode=true,保证中文不乱码
useSSLfalse本地无证书必须关,否则启动要握证书,日志里刷一堆警告
allowPublicKeyRetrievaltrueMySQL 8 默认caching_sha2_password,不开这个本地连不上
rewriteBatchedStatementstrue批量插入时把多条 INSERT 合成一条多值语句,性能差一个量级
connectTimeout/socketTimeout3000/60000不设会一直挂着,故障时线程池被拖死
zeroDateTimeBehaviorconvertToNull遇到0000-00-00不设会抛Zero date value prohibited
spring: datasource: url: jdbc:mysql://127.0.0.1:3306/demo_db?useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true&connectTimeout=3000&socketTimeout=60000 username: app_user password: App@123456 driver-class-name: com.mysql.cj.jdbc.Driver

注意&在 YAML 里不需要转义,但如果你把这段放进.properties,就得原样写,别加反斜杠。另外driver-class-name在 Boot 2.x 之后其实可以省略,自动装配能推断出来;写上的好处是启动日志里能一眼确认驱动是否被正确加载。

3.3 JdbcTemplate 的增删改查:把主键和映射写对

JdbcTemplate的核心就三类方法:update管写,queryForObject管单条,query管列表。真正容易写错的是自增主键回填和字段映射。

@Repository public class OrderRepository { private final JdbcTemplate jdbcTemplate; // 构造器注入,Spring 会自动把容器里的 JdbcTemplate 传进来 public OrderRepository(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } /** 插入并回填自增主键,keyHolder 必须配合 RETURN_GENERATED_KEYS */ public Long insert(Order order) { String sql = "INSERT INTO t_order(order_no, amount, status) VALUES (?, ?, ?)"; KeyHolder keyHolder = new GeneratedKeyHolder(); jdbcTemplate.update(conn -> { PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); ps.setString(1, order.getOrderNo()); ps.setBigDecimal(2, order.getAmount()); ps.setInt(3, order.getStatus()); return ps; }, keyHolder); Number key = keyHolder.getKey(); return key == null ? null : key.longValue(); } /** 显式 RowMapper,按列名取值,不依赖 SELECT 的字段顺序 */ public Order findByNo(String orderNo) { String sql = "SELECT id, order_no, amount, status, created_at FROM t_order WHERE order_no = ?"; return jdbcTemplate.queryForObject(sql, ROW_MAPPER, orderNo); } /** 更新,返回受影响行数,业务层据此判断是否真的改到了 */ public int updateStatus(Long id, int status) { return jdbcTemplate.update("UPDATE t_order SET status = ? WHERE id = ?", status, id); } private static final RowMapper<Order> ROW_MAPPER = (rs, rowNum) -> { Order o = new Order(); o.setId(rs.getLong("id")); o.setOrderNo(rs.getString("order_no")); o.setAmount(rs.getBigDecimal("amount")); o.setStatus(rs.getInt("status")); Timestamp ts = rs.getTimestamp("created_at"); o.setCreatedAt(ts == null ? null : ts.toLocalDateTime()); return o; }; }

逻辑上说清楚三件事。第一,update(PreparedStatementCreator, KeyHolder)这个重载才是回填主键的正确姿势,很多示例用SimpleJdbcInsert,功能没问题但多引一个类,不如这个直接。第二,RowMapper按列名取值,rs.getLong("id")里的字符串必须和 SELECT 里的别名一致,写成SELECT id AS order_id就得改成getLong("order_id"),这一点和按索引取值不同。第三,金额字段用BigDecimal接,用double接会丢精度,这是财务类字段的老坑。

参数说明上再补一句:queryForObject在查不到数据时抛EmptyResultDataAccessException,不是返回 null。业务层要么 try-catch,要么改用query(...)拿 List 再判断isEmpty()。我一般后者,代码干净。

3.4 事务边界:@Transactional 失灵的三种典型情况

@Transactional挂在方法上就生效,这是错觉,它依赖 Spring 的 AOP 代理。三种情况必失效:方法不是public;同类内部this.xxx()自调用,不走代理;异常被自己 catch 掉没往外抛。默认情况下只有RuntimeException和Error触发回滚,抛Exception得显式写@Transactional(rollbackFor = Exception.class)。

@Service public class OrderService { private final OrderRepository repository; public OrderService(OrderRepository repository) { this.repository = repository; } // rollbackFor 必须显式写,否则受检异常不回滚 @Transactional(rollbackFor = Exception.class) public void createAndPay(Order order) throws Exception { Long id = repository.insert(order); repository.updateStatus(id, 1); if (order.getAmount().signum() < 0) { throw new Exception("金额非法"); // 触发回滚,两条 SQL 一起撤销 } } }

还有个细节:事务方法里如果调了远程接口,长事务会一直占着连接,池子很快见底。我的习惯是把远程调用挪到事务外,事务里只包数据库操作。

4. 排查与避坑:五条连不上库的真实现场和处理顺序

4.1 报错信息到根因的对照表

先给一张对照表,绝大多数启动失败都能在里面找到定位方向。看报错不要看第一行,要看Caused by最下面那一层,那才是根因。

报错关键字大概率根因先查什么
Communications link failure服务没起、端口不通、防火墙telnet 127.0.0.1 3306通不通
Access denied for user账号密码错、Host 不匹配账号是不是'user'@'%',密码有没有特殊字符
Public Key Retrieval is not allowedMySQL 8 认证插件URL 有没有allowPublicKeyRetrieval=true
Unknown database库名拼错、库没建SHOW DATABASES;对一遍
The server time zone value ... is unrecognized时区参数缺失加serverTimezone=Asia/Shanghai
No suitable driver found驱动坐标错、JDK 版本不匹配看mvn dependency:tree里有没有驱动

4.2 五条踩坑记录,每条都是真金白银

现象一:本地 IDEA 里跑得好好的,打成 jar 扔到服务器上启动就Communications link failure。原因:配置文件里写的是localhost,容器或服务器上localhost解析到的地址和 MySQL 实际监听的不是一回事,加上 MySQL 默认只监听127.0.0.1。解决:URL 改成明确的127.0.0.1:3306或实际内网地址,同时确认my.cnf里bind-address的值,两边对齐再重启。

现象二:批量插入一万条数据,耗时接近 40 秒,慢得离谱。原因:URL 里没加rewriteBatchedStatements=true,驱动把batchUpdate拆成一条条发出去,等于一万次网络往返。解决:加上这个参数,注意它只对INSERT/UPDATE的预编译语句生效,且 SQL 里不能有;结尾,否则重写会失败。

现象三:应用跑了一夜,第二天所有请求超时,日志刷HikariPool - Connection is not available, request timed out。原因:连接池maxLifetime默认 30 分钟,而 MySQL 服务端wait_timeout是 8 小时,本来不该冲突;但中间挂了代理或防火墙会静默掐掉空闲连接,池子拿到的是死连接。解决:把maxLifetime调到比中间设备空闲超时更短(比如 10 分钟),并开启connection-test-query或用 JDBC4 的isValid探活。

现象四:中文写进去变成问号。原因:建库时用了utf8,或者连接串漏了characterEncoding=utf8,也可能是表级字符集没跟着库走。解决:库、表、连接串三处统一utf8mb4,SHOW CREATE TABLE逐张确认,历史表用ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4补。

现象五:@Transactional加了但异常后数据没回滚。原因:方法内部try-catch把异常吞了,或者同类内部调用绕过了代理。解决:异常往上抛,或者手动TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();自调用改成注入自身代理或拆到另一个 Bean。

4.3 日志和诊断开关怎么开

排查连库问题,把这两行加到application.yml,日志里能看到池子的创建、借出、归还全过程:

logging: level: org.springframework.jdbc.core: DEBUG # 打印实际执行的 SQL 和参数 com.zaxxer.hikari: DEBUG # 打印连接池状态 com.mysql.cj.jdbc: DEBUG # 驱动层握手细节

JdbcTemplate的 DEBUG 日志会把参数值原样打出来,开发期很好用,上线前必须调回 INFO,否则等于把手机号、金额这些字段全写进日志文件。还要注意,日志级别调太高会明显拖慢批量操作,压测时关掉。

5. 连接池与批量写入:HikariCP 参数和 rewriteBatchedStatements 的边界

5.1 HikariCP 的关键参数怎么定

HikariCP 是starter-jdbc的默认池,默认值对演示够用,对生产往往不合适。最常被误解的是maximumPoolSize:调大不等于变快。数据库的连接数是有限资源,单实例 MySQL 常见配置也就几百,应用侧每个实例开 20 个连接已经不算少。合理值大致是「数据库最大连接数 ÷ 应用实例数,再留 20% 余量」。

spring: datasource: hikari: pool-name: demo-pool maximum-pool-size: 20 # 上限,超过就排队 minimum-idle: 5 # 常驻空闲连接,避免突发时现建 connection-timeout: 3000 # 借连接等待上限,超时抛异常而不是无限等 idle-timeout: 600000 # 空闲 10 分钟回收 max-lifetime: 600000 # 必须小于 MySQL wait_timeout connection-test-query: SELECT 1 # 老驱动探活用;JDBC4 驱动可省

connection-timeout我一般设 3 秒而不是默认的 30 秒。业务请求等 30 秒才报错,用户体验和线程占用都是双输,早点失败早点降级。max-lifetime设为 10 分钟,是给中间网络设备留出安全边界,同时也别忘了 MySQL 侧wait_timeout的对照。

5.2 批量写入:batchUpdate 的正确打开方式

单条update循环一万次,和batchUpdate一次发一万条,差距在rewriteBatchedStatements=true的前提下能到十倍以上。前提是这个参数真的生效了,判断办法就是看驱动 DEBUG 日志里是否出现合并后的多值 INSERT。

public int[] batchInsert(List<Order> orders) { String sql = "INSERT INTO t_order(order_no, amount, status) VALUES (?, ?, ?)"; return jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { @Override public void setValues(PreparedStatement ps, int i) throws SQLException { Order o = orders.get(i); ps.setString(1, o.getOrderNo()); ps.setBigDecimal(2, o.getAmount()); ps.setInt(3, o.getStatus()); } @Override public int getBatchSize() { return orders.size(); } }); }

batchUpdate返回的int[]是每行受影响行数,注意 MySQL 在开启重写后这个数组可能被合并成一个元素,别拿它跟入参条数强行对应。另外批量不要一次塞十万条,常见做法是每 1000 条切一批,事务也按批提交,既控内存也控锁持有时间。

5.3 大结果集:别把整张表读进内存

导出类需求最容易翻车。query一次性把结果集映射成对象列表,几十万行直接 OOM。要用RowCallbackHandler流式处理:

public void exportAll(Consumer<Order> consumer) { String sql = "SELECT id, order_no, amount, status FROM t_order"; jdbcTemplate.query(sql, rs -> { // 不返回 List,逐行回调 Order o = new Order(); o.setId(rs.getLong("id")); o.setOrderNo(rs.getString("order_no")); o.setAmount(rs.getBigDecimal("amount")); o.setStatus(rs.getInt("status")); consumer.accept(o); }); }

回调式查询本身不会构建全量列表,但 MySQL 驱动默认仍会把整个结果集拉到客户端。要真正边读边处理,得在连接串加useCursorFetch=true并在 Statement 上设fetchSize,或者用setFetchSize(Integer.MIN_VALUE)走流式模式。流式期间这条连接不能做别的操作,所以别放在事务里长时间持有,导出任务最好单独配一个数据源。

6. 换成自己的库:三步验证法与一个自检接口

拿到源码包先别动业务代码,我一般按三步验证。第一步,把application.yml里的url、username、password换成自己的,启动后盯这行日志:HikariPool-1 - Start completed.,出现说明池子建起来了,没出现就是连接串或权限的问题,这时候翻第 4 章那张对照表最快。第二步,确认能读:写个临时接口调一次SELECT 1,顺便验证JdbcTemplate是否被正确注入。第三步,确认能写且能回滚:手动抛一次异常,看数据是否真的没落库。

源码包里带了个自检接口,思路很简单,用CommandLineRunner在启动时跑一次轻量探测:

@Component public class DbHealthCheck implements CommandLineRunner { private final JdbcTemplate jdbcTemplate; public DbHealthCheck(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } @Override public void run(String... args) { Integer one = jdbcTemplate.queryForObject("SELECT 1", Integer.class); String version = jdbcTemplate.queryForObject("SELECT VERSION()", String.class); System.out.println("db ok=" + one + ", server=" + version); } }

这段代码只做两件事:验证连接可用、确认连到的是哪个服务端版本。别小看第二项,本地 MySQL 5.7 和测试环境 8.0 的时区、认证插件行为都不一样,先把版本打出来能省掉一大半「本地好好的」类问题。验证通过后再把这段删掉或加开关,别让它跟着上生产。

从那以后我每次接入一套新库,都强制走这三步再动业务代码,顺序反过来的次数多了,吃过亏就记住了。希望帮到你。

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

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

SolidWorks二次开发模板:COM连接、批量出图与避坑骨架

简介&#xff1a;SolidWorks二次开发模板是一套面向机械设计人员与C#开发者的SDK入门工程&#xff0c;帮助读者在SolidWorks环境中快速搭建COM Add-in插件、定制菜单工具栏并实现命令与事件响应。压缩包共99个文件&#xff0c;约10.24MB&#xff0c;以cs源码、dll类库、csproj与…

作者头像 李华
网站建设 2026/11/2 11:22:40

JSP+JavaBean简单购物车:Session状态管理与Tomcat实战

简介&#xff1a;这份压缩包是一套基于 JSP、Servlet、Tomcat 与 Session 的简易购物车实战案例&#xff0c;面向刚接触 Java Web 开发、希望打通“页面—控制层—业务层—数据层”链路的初学者与课程设计者。项目围绕商品列表展示、加入购物车、查看与删除购物车条目等基础流程…

作者头像 李华
网站建设 2026/11/2 11:21:42

机器人基础模型开始按小时对外开放

【具身AGI导读】把模型装进接口按使用时长卖&#xff0c;看上去只是交付形式的改动&#xff1b;真正被抬上台面的&#xff0c;是「跨本体」从论文里的实验设置变成了一项对外承诺。2026 年 10 月 20 日&#xff0c;由英伟达 DreamZero 团队创立的创业公司 DreamLabs 结束隐身&a…

作者头像 李华
网站建设 2026/11/2 11:21:41

C++实现LeetCode(99.复原二叉搜索树)

前言 LeetCode 第 99 题「恢复二叉搜索树」的描述是&#xff1a;一棵二叉搜索树的其中两个节点被错误地交换了&#xff0c;在不改变树结构的前提下恢复这棵树。题目通常要求给出原地算法&#xff0c;进阶要求使用 O(1) 额外空间。 解题的钥匙是中序遍历。二叉搜索树的中序序列严…

作者头像 李华
网站建设 2026/11/2 11:20:22

Kubernetes权威指南——深入掌握Pod(七)

深入掌握Pod11、Pod的升级和回滚11.1、Deployment的升级11.2、Deployment的回滚11.3、暂停和恢复Deployment的部署操作&#xff0c;以完成复杂的修改11.4、使用kubectl rolling-update命令完成RC的滚动升级11.5、其他管理对象的更新策略11.5.1、DaemonSet的更新策略11.5.2、Sta…

作者头像 李华