简介:面向 Java 后端初学者与需要快速跑通数据库连接的开发者,内容围绕 Spring Boot 整合 JDBC 访问 MySQL 的完整链路展开,涉及依赖引入、连接参数配置、Controller 与实体类分层写法及建库建表脚本,可对照复现一套可运行的示例工程,也适合课程实验或小型项目脚手架使用。压缩包共 136 个文件、约 637MB,3 个 java 源文件与 5 个 class 对应核心示例代码,3 个 sql 脚本提供建表与初始化数据,properties、xml 等文件说明连接与工程结构,另有 92 个 dll、4 个 exe 以及 zip、msi 安装包,便于在 Windows 下一次性备齐 JDK、数据库与驱动。目前已有 601 人学习下载;源码、SQL 脚本与安装包相互配合,可省去逐个查找驱动、核对版本的时间,把重点放在连接串、数据源参数与增删改查实现上,遇到驱动缺失或连接失败也有现成环境可对照排查。
1. 版本先对齐:JDK 17、Spring Boot 3.2 与 MySQL 8.0 的组合
上周被人拉去看一个跑不起来的工程,pom 里 Spring Boot 写的是 3.2,机器上 JAVA_HOME 还指着 JDK 8,MySQL 装的是 5.7。把 JDK 换掉之后,连接串里的驱动类又还写着com.mysql.jdbc.Driver,问题一层一层往外冒。用 Java 通过 JDBC 连 MySQL 这件事本身不复杂,复杂的是 Spring Boot 版本、JDK 版本、MySQL 版本、驱动坐标这四样必须互相匹配,只要有一个错位,报错信息就会指向别的地方。
这套环境几乎是每个 Java 后端都要接触的第一条数据库链路,读写、连接池、字符集、时区全都压在它上面。装对了十分钟能跑通,装不对能折腾一整天。下面按我实际的顺序来:先把 JDK 和 Maven 配干净,再把 MySQL 8.0 初始化好并开出最小权限账号,然后回到工程里配连接串和 HikariCP,最后用三层自检确认它真的能读写。
2. 装 JDK 与 Maven:从 JAVA_HOME 到镜像源一次配干净
本章做的事情只有两件:让命令行认出唯一的那个 JDK,让 Maven 能稳定地把依赖拉下来。听起来像废话,但后面百分之六十的诡异报错,根都在这两步。
2.1 JDK 版本怎么选,装完怎么验
Spring Boot 3.x 的最低要求是 JDK 17,Spring Boot 2.7 那一代还能在 JDK 8 上跑。选哪个不是喜好问题,是匹配问题:
| Spring Boot | 最低 JDK | JDBC 驱动坐标 | 命名空间 |
|---|---|---|---|
| 2.7.x | JDK 8 | mysql:mysql-connector-java | javax.* |
| 3.0.x - 3.2.x | JDK 17 | com.mysql:mysql-connector-j | jakarta.* |
| 3.3.x 及以上 | JDK 17 | com.mysql:mysql-connector-j | jakarta.* |
我一般直接下 Temurin 或 Zulu 的 JDK 17 免安装压缩包,解到一个不带空格、不带中文的目录,比如D:\env\jdk17或/opt/jdk17。带空格的路径在某些脚本里会被拆成两个参数,这类坑排查起来非常不直观。
# 解压后先看目录结构,确认 bin 目录直接在解压目录下 ls /opt/jdk17 # 期望看到 bin conf include jmods legal lib release 这一层解压出来如果多套了一层jdk-17.0.x目录,JAVA_HOME 就要指到那一层,指错一层是最常见的翻车点。确认结构没问题再设环境变量。
# Linux / macOS,写进 ~/.bashrc 或 ~/.zshrc export JAVA_HOME=/opt/jdk17 export PATH=$JAVA_HOME/bin:$PATHREM Windows,系统环境变量里配,不要只在当前终端 set setx JAVA_HOME "D:\env\jdk17" setx PATH "%JAVA_HOME%\bin;%PATH%"JAVA_HOME指的是 JDK 根目录,不带\bin;PATH里加的才是%JAVA_HOME%\bin。这两个混着写,命令行能跑java,但 Maven 和 IDE 会各自报不同的错,属于典型的玄学现场。配完一定要新开一个终端验证:
java -version # 应输出 17.x javac -version # 应输出 17.x,和上面一致 echo $JAVA_HOME # Windows 用 echo %JAVA_HOME%java -version和javac -version版本不一致,说明 PATH 里排在前面的还有另一个 JDK,把它挪走或者删掉。这一步不要偷懒跳过,后面所有问题都建立在这个前提上。
2.2 Maven 安装与 settings.xml 配置
Maven 用 3.9.x 这一代就够了,它自带的依赖解析对 Spring Boot 3 的 BOM 支持没问题。解压到D:\env\maven或/opt/maven,同样把MAVEN_HOME和%MAVEN_HOME%\bin写进环境变量。
真正要改的是conf/settings.xml,重点三处:本地仓库位置、镜像源、JDK 编译配置。
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"> <!-- 本地仓库挪出 C 盘/系统盘,几百兆依赖堆在用户目录里迟早出事 --> <localRepository>D:/env/maven-repo</localRepository> <mirrors> <mirror> <id>aliyun-central</id> <mirrorOf>central</mirrorOf> <name>central mirror</name> <url>https://maven.aliyun.com/repository/central</url> </mirror> </mirrors> <profiles> <profile> <id>jdk17</id> <activation> <activeByDefault>true</activeByDefault> <jdk>17</jdk> </activation> <properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <maven.compiler.release>17</maven.compiler.release> </properties> </profile> </profiles> </settings>localRepository里的路径用正斜杠,Windows 下也建议用/,反斜杠在 XML 里会被当转义符处理。mirrorOf写central只拦中央仓库,如果写*会把所有仓库都拦下来,某些公司私服就会被一起劫持,表现是依赖莫名其妙 404。
maven.compiler.release比source/target更严格,它会让编译器对着 JDK 17 的 API 表校验,避免你在 JDK 17 上用了 JDK 21 才有的方法却编译通过。配完验证:
mvn -v # 输出里要盯两行:Maven 3.9.x 和 Java version: 17.xmvn -v里那行 Java version 是很多人忽略的黑匣子。它和java -version不一致,说明 Maven 找到了另一个 JDK(比如 IDE 内置的),此时mvn clean package的编译结果和 IDE 里的编译结果会不一样,症状是「IDE 能跑,打包就报错」。
2.3 三个最容易写错的路径
第一条:JAVA_HOME带上了\bin。症状是mvn -v报JAVA_HOME is set to an invalid directory。
第二条:PATH里同时存在新旧两个 JDK 的 bin,且顺序不对。症状是java -version是 17,javac -version是 8,或者反过来。解决办法不是反复改,而是把不用的那个 JDK 从 PATH 里彻底删掉,多版本共存靠脚本切换,别指望 PATH 顺序。
第三条:改完环境变量没有重启终端,或者 IDE 是从旧终端拉起来的。Windows 上尤其明显,setx只对之后新开的进程生效。我习惯的做法是改完环境变量后直接关掉所有终端和 IDE,重新开一个,再验证一遍java -version、mvn -v、IDE 里的 Project SDK 三处指向同一个 JDK 17。
3. 装 MySQL 8.0:初始化、建库与最小权限账号
MySQL 8.0 和 5.7 之间有几处会直接影响 JDBC 连接的变化:默认认证插件改成了caching_sha2_password,默认字符集改成了utf8mb4,utf8这个词的语义也被收紧了。用 5.7 时代的经验去装 8.0,大概率会在连接阶段卡住。
3.1 免安装包初始化与注册服务
Windows 上我一般用 ZIP 包,可控性最强。解压到D:\env\mysql-8.0.36,在同级建一个my.ini:
[mysqld] port=3306 basedir=D:/env/mysql-8.0.36 datadir=D:/env/mysql-8.0.36/data character-set-server=utf8mb4 collation-server=utf8mb4_0900_ai_ci default-time-zone='+08:00' max_connections=200 # 8.0 用这个参数,8.4 起换成了 authentication_policy default-authentication-plugin=mysql_native_password [client] default-character-set=utf8mb4datadir必须指向一个空目录或者不存在的目录,初始化时 MySQL 会往里写系统表。目录里有残留文件,--initialize会直接失败。default-time-zone设成固定偏移量而不是时区名,是为了避免服务器本身没装时区表时启动报错。default-authentication-plugin这一行在 8.0 上还认,8.4 之后会被废弃,如果你的小版本是 8.4,把它换成authentication_policy=mysql_native_password。
初始化并注册服务:
# 用 --console 把初始化日志打到屏幕上,不要让它进 error log 里找不着 mysqld --defaults-file=D:/env/mysql-8.0.36/my.ini --initialize-insecure --console # 注册为系统服务,服务名自己起,别和已有的 MySQL 冲突 mysqld --install MySQL80 --defaults-file=D:/env/mysql-8.0.36/my.ini # 启动 net start MySQL80--initialize-insecure生成的是空密码的 root,好处是能在本地直接登进去改,坏处是这中间有一段时间数据库是完全敞开的。我的习惯是初始化完立刻登录改密码,中间不接入任何网络。
ALTER USER 'root'@'localhost' IDENTIFIED BY '换成你自己的强密码'; FLUSH PRIVILEGES;Linux 上如果用 tar.gz 包,流程一样,只是要多建一个mysql系统用户并把datadir的属主改过去,否则--initialize会因为权限不足中途退出。启动失败时先看datadir下的.err文件,里面会写清楚是端口占用、权限问题还是配置项不认识。
3.2 建库建表:字符集和排序规则一次定死
字符集这件事没有后悔药,表建完再改要重建数据。8.0 的默认已经是utf8mb4,但我还是习惯显式写出来,因为配置文件被覆盖、或者从别处导入的 SQL 脚本里写着utf8,最后都会落回三字节的utf8mb3,存 emoji 或者部分生僻字就报Incorrect string value。
CREATE DATABASE IF NOT EXISTS demo_db DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_0900_ai_ci; USE demo_db; CREATE TABLE t_order ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', order_no VARCHAR(32) NOT NULL COMMENT '订单号', amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '金额', remark VARCHAR(255) DEFAULT NULL COMMENT '备注', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_amount (amount) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;几个取舍说一下。金额用DECIMAL不用DOUBLE,浮点在累加时会有尾差,对账时对不上是非常难受的事。order_no上建唯一索引,业务层的幂等就多一道保险,插入重复单号会直接抛异常而不是静默写脏数据。created_at用DATETIME配DEFAULT CURRENT_TIMESTAMP,比TIMESTAMP少一层 2038 年的隐患,代价是不带时区信息,应用侧要保证写入的时间是同一个时区。
utf8mb4_0900_ai_ci里的ai是重音不敏感、ci是大小写不敏感。要做严格区分大小写的用户名之类的字段,单列指定utf8mb4_0900_as_cs,别为了一个字段把整库的排序规则改掉。
3.3 应用账号:不给 root,也不给 DDL
应用连接数据库用的账号和你运维用的账号必须是两个。应用账号只给 DML 权限,建表、改表这类操作走单独的迁移流程。
-- 应用账号,host 用 % 还是 localhost 取决于应用和库是否同机 CREATE USER 'app_demo'@'%' IDENTIFIED BY 'App_Str0ng_Pwd'; -- 只给增删改查,不给 DROP/ALTER/CREATE GRANT SELECT, INSERT, UPDATE, DELETE ON demo_db.* TO 'app_demo'@'%'; FLUSH PRIVILEGES;'app_demo'@'%'和'app_demo'@'localhost'在 MySQL 里是两个不同的账号,只建了%的账号,从本机连不一定能登进去,反过来也一样。本地开发时两个都建,或者明确用127.0.0.1走 TCP 而不是 socket。
另外 8.0 默认的caching_sha2_password在 JDBC 侧需要额外参数配合,如果你暂时不想纠结,可以把账号指定成mysql_native_password:
CREATE USER 'app_demo'@'%' IDENTIFIED WITH mysql_native_password BY 'App_Str0ng_Pwd';但更推荐的做法是保留默认插件,在连接串里加allowPublicKeyRetrieval=true,这一点下一章会展开。
4. 工程接 JDBC:pom 坐标、连接串参数与 HikariCP 调参
到这一步环境已经就绪,剩下的全在工程里。Spring Boot 用spring-boot-starter-jdbc这一套,没有 ORM,纯JdbcTemplate,好处是 SQL 可控、启动快、排查时链路短。
4.1 依赖坐标:两个 starter 加一个驱动
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.5</version> <relativePath/> </parent> <properties> <java.version>17</java.version> </properties> <dependencies> <!-- 带 HikariCP 和 JdbcTemplate 的自动配置 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!-- 8.0.31 之后坐标从 mysql:mysql-connector-java 改成 com.mysql:mysql-connector-j --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>驱动这块不要自己写<version>。spring-boot-starter-parent的依赖管理里已经锁好了和当前 Boot 版本匹配的驱动版本,手写版本号会破坏这层对应关系,常见后果是驱动版本过高导致Unsupported class file major version,或者过低不认 8.0 的认证插件。
com.mysql:mysql-connector-j这个坐标是 8.0.31 之后才有的。老教程里的mysql:mysql-connector-java在 Boot 3 的 BOM 里已经不再管理,写了也不报错,但版本会变成你自己指定的那个,属于埋雷。
4.2 连接串逐项拆解
spring: datasource: url: jdbc:mysql://127.0.0.1:3306/demo_db?useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&connectTimeout=3000&socketTimeout=60000 username: app_demo password: App_Str0ng_Pwd driver-class-name: com.mysql.cj.jdbc.Driver hikari: pool-name: demo-pool maximum-pool-size: 10 minimum-idle: 2 connection-timeout: 3000 idle-timeout: 600000 max-lifetime: 1740000 connection-init-sql: SET NAMES utf8mb4逐项说清楚,改的时候心里有数:
| 参数 | 作用 | 取值建议 |
|---|---|---|
useUnicode+characterEncoding | 客户端与服务端的字符编码协商 | 固定true+utf8,对应服务端 utf8mb4 |
useSSL | 是否走 SSL 握手 | 内网自建库false,云上实例按服务商要求开 |
allowPublicKeyRetrieval | 允许客户端向服务端索取公钥 | 配合caching_sha2_password且不开 SSL 时必须有 |
serverTimezone | 驱动解析时间用的时区 | 写固定偏移GMT%2B8,+必须编码成%2B |
rewriteBatchedStatements | 把批量 insert 改写成多值语句 | 批量写入场景必须开,否则批处理是逐条发 |
connectTimeout | 建连超时(毫秒) | 3000 足够,别用默认值等半天 |
socketTimeout | 单次读超时(毫秒) | 60000,长事务场景按最慢 SQL 调 |
driver-class-name在 Boot 3 + 8.x 驱动下其实可以省略,驱动会自动从 URL 推断。但省略之后如果哪天换成了连接池里的多数据源,排查时少一个抓手,所以我一般还是显式写上,而且一定写com.mysql.cj.jdbc.Driver,不是老版的com.mysql.jdbc.Driver。
connection-init-sql设SET NAMES utf8mb4是双保险,防止服务端character_set_client被别处改掉。如果服务端配置已经对了,这条语句是幂等的,不产生额外开销。
4.3 HikariCP 参数:max-lifetime 必须小于 wait_timeout
Spring Boot 默认用的连接池就是 HikariCP,不引额外依赖。参数里最容易出事的是max-lifetime。
MySQL 服务端有个wait_timeout,默认 28800 秒(8 小时),空闲连接超过这个时间会被服务端单方面踢掉。而连接池并不知道对面已经断了,仍然把这条连接当可用连接发出去,症状是「应用闲置一夜之后,第一批请求全部报Communications link failure」。
解决办法就是让连接池主动先于服务端回收:
hikari: max-lifetime: 1740000 # 29 分钟,必须小于 MySQL wait_timeout idle-timeout: 600000 # 10 分钟,空闲多久后允许被回收 keepalive-time: 300000 # 5 分钟,定期探活,防止被中间网络设备静默断连max-lifetime要比wait_timeout小,留出至少几分钟的余量。反过来,max-lifetime设得过小(比如 30 秒)会让连接频繁重建,建连本身就是一次 TCP 加认证握手,高并发下这笔开销很可观。
maximum-pool-size不要拍脑袋设大。池子大小的瓶颈通常不在数据库连接数,而在后端能扛住的并发 SQL 数量。10 到 20 是大部分中小业务合适的区间,配上connection-timeout: 3000,拿不到连接就快速失败,比无限等待更容易定位问题。
4.4 JdbcTemplate 的读写写法
配置好了不代表会用。JdbcTemplate的坑集中在参数传递和行映射上:
@Repository public class OrderDao { private final JdbcTemplate jdbcTemplate; public OrderDao(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } // 写入:占位符按顺序传参,返回影响行数 public int insert(String orderNo, BigDecimal amount, String remark) { String sql = "INSERT INTO t_order(order_no, amount, remark) VALUES (?, ?, ?)"; return jdbcTemplate.update(sql, orderNo, amount, remark); } // 查询:RowMapper 放中间,参数跟在后面,别把顺序写反 public List<Order> findTop50(BigDecimal minAmount) { String sql = "SELECT id, order_no, amount, remark, created_at " + "FROM t_order WHERE amount >= ? ORDER BY id DESC LIMIT 50"; return jdbcTemplate.query(sql, (rs, rowNum) -> { Order o = new Order(); o.setId(rs.getLong("id")); o.setOrderNo(rs.getString("order_no")); o.setAmount(rs.getBigDecimal("amount")); o.setRemark(rs.getString("remark")); o.setCreatedAt(rs.getTimestamp("created_at").toLocalDateTime()); return o; }, minAmount); } // 批量写:必须配合 rewriteBatchedStatements=true 才能合成多值语句 public int[] batchInsert(List<Order> orders) { String sql = "INSERT INTO t_order(order_no, amount, remark) VALUES (?, ?, ?)"; return jdbcTemplate.batchUpdate(sql, orders, 500, (ps, o) -> { ps.setString(1, o.getOrderNo()); ps.setBigDecimal(2, o.getAmount()); ps.setString(3, o.getRemark()); }); } }update和query的参数顺序不一样:update(sql, args...)参数直接跟在 SQL 后面,query(sql, rowMapper, args...)中间夹了一个RowMapper。写反了不会编译报错(都是 Object),运行时报参数个数不匹配,这类错误看着很莫名其妙。
batchUpdate(sql, collection, batchSize, setter)里的batchSize是每多少条刷一次,500 是个比较稳的值。批处理真正的性能开关在连接串的rewriteBatchedStatements=true上,不开这个参数,驱动会老老实实一条一条发,看着用了批量接口,实际毫无收益。
5. 排查手册:连不上、连上查不出、查出来乱码的五种翻车
连接类的报错有个共同点:错误信息本身往往不指向真正的原因。下面五条是我踩过并且反复在别人机器上见到的,按「现象 → 原因 → 解决」记下来。
5.1 Communications link failure
现象:应用启动时报com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure,堆栈里没有更具体的信息。
原因:TCP 层就没连上,可能是服务没起、端口不对、防火墙拦了,也可能是 URL 里的主机名解析到了别处。还有一种隐蔽情况是服务端bind-address只监听127.0.0.1,从别的机器连必然失败。
解决:先绕开 Java 验证网络层,用命令行客户端直连同一个地址端口。
# 先看端口通不通 telnet 127.0.0.1 3306 # 再用客户端登一次,确认账号密码和库名都没问题 mysql -h 127.0.0.1 -P 3306 -u app_demo -p demo_db客户端能连、Java 连不上,问题就在 URL 或驱动;两边都连不上,去查服务和防火墙。不要一上来就改代码。
5.2 Access denied for user 'app_demo'@'localhost'
现象:Access denied for user 'app_demo'@'localhost' (using password: YES),但客户端用同样的账号密码能登进去。
原因:报错里的 host 部分是 MySQL 对来源的判定结果,不是你写在 URL 里的那个。用localhost连会走 socket,MySQL 认为来源是localhost;用127.0.0.1连走 TCP,认为来源是127.0.0.1。只建了'app_demo'@'%'的账号,本机 socket 连接就可能匹配不上。
解决:查一下实际有哪些账号,再按需补齐。
SELECT user, host, plugin FROM mysql.user WHERE user = 'app_demo';缺哪个 host 补哪个,本地开发把localhost和%都建上,最省事。
5.3 Public Key Retrieval is not allowed
现象:java.sql.SQLException: Public Key Retrieval is not allowed。
原因:MySQL 8.0 默认认证插件是caching_sha2_password,在不开 SSL 的连接上,客户端需要向服务端索取 RSA 公钥来加密密码,驱动默认禁止这个行为。
解决:连接串加allowPublicKeyRetrieval=true。这相当于信任服务端返回的公钥,所以只在内网或者已经确认链路安全的环境里开;公网实例应该走 SSL 而不是开这个开关。
5.4 中文乱码与 Incorrect string value
现象:写入中文报Incorrect string value: '\xE4\xBD\xA0...' for column 'remark',或者读出来是一串问号。
原因:表或列的字符集是utf8mb3(三字节),存不下四字节字符;或者连接串没声明编码,服务端按默认的latin1处理。
解决:三层一起查,缺一层都不行。
-- 库、表、列三层的字符集都要是 utf8mb4 SELECT DEFAULT_CHARACTER_SET_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = 'demo_db'; SELECT TABLE_NAME, TABLE_COLLATION FROM information_schema.TABLES WHERE TABLE_SCHEMA = 'demo_db'; SHOW FULL COLUMNS FROM t_order;发现某一层是utf8,用ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4改掉,同时确认连接串带了characterEncoding=utf8。改表的时候注意,CONVERT TO会重建表,大表要挑低峰期做。
5.5 Connection is not available, request timed out
现象:跑一段时间后开始报HikariPool-1 - Connection is not available, request timed out after 3000ms,重启应用就恢复。
原因:连接泄漏。某段代码通过DataSource.getConnection()手动拿了连接却没关,池子里的连接被一点点耗光。用JdbcTemplate一般不会有这问题,但混合了手写 JDBC 的代码就难说。
解决:把池子的运行状态暴露出来看,不要凭猜。
# 打开 Hikari 的指标和 Actuator 的数据库健康检查 management: endpoints: web: exposure: include: health,metrics health: db: enabled: true访问/actuator/metrics/hikaricp.connections.active和hikaricp.connections.pending,如果 active 长期顶在maximum-pool-size而 pending 一直在涨,基本可以确认是泄漏。再把手动拿连接的地方全部改成 try-with-resources,或者统一收口到JdbcTemplate。短期止血的办法是把leak-detection-threshold设成 20000(毫秒),超过 20 秒没归还的连接会在日志里打出堆栈,直接指出是哪一行代码拿的。
6. 验证跑通:三层自检把「能启动」变成「能读写」
应用能启动不代表链路通了。Spring Boot 启动时默认是懒加载数据源的,很多时候「启动成功」只是意味着配置解析没报错,真正建连要等到第一次请求。我一般按三层往下验。
第一层,网络与账号。用命令行客户端从应用所在的机器直连一次,确认主机、端口、账号、库名四个信息全部正确。这一层不过,后面所有验证都是白费。
第二层,数据源。在启动类旁边加一个ApplicationRunner,让它在容器就绪后立刻拿一条连接出来,把元数据打印出来:
@Component public class DataSourceSelfCheck implements ApplicationRunner { private final DataSource dataSource; public DataSourceSelfCheck(DataSource dataSource) { this.dataSource = dataSource; } @Override public void run(ApplicationArguments args) throws Exception { try (Connection conn = dataSource.getConnection()) { DatabaseMetaData meta = conn.getMetaData(); // 三行输出能同时证明:连接可用、驱动版本正确、服务端版本符合预期 System.out.println("url = " + meta.getURL()); System.out.println("driver = " + meta.getDriverName() + " " + meta.getDriverVersion()); System.out.println("product = " + meta.getDatabaseProductVersion()); System.out.println("readOnly = " + conn.isReadOnly()); } } }这段代码的价值在于把问题暴露在启动阶段。meta.getURL()会回显实际生效的连接串,能立刻看出时区、SSL 这些参数有没有被覆盖;getDriverVersion()和getDatabaseProductVersion()对上你预期的版本,就排除了驱动错配。用 try-with-resources 包住,验证完连接立刻归还,不占用池子。
第三层,业务读写。跑一个最小的闭环:插入一条、按条件查出来、更新一次、再删掉。用测试类做最方便,@SpringBootTest起完整容器,断言影响行数和查询结果。这一步能抓到字符集、字段类型映射、事务边界的问题,比如DECIMAL映射成double造成的精度丢失,只有真的写进去再读出来才会暴露。
三层都过之后,再补一个日常习惯:把/actuator/health挂到监控上,db那一项是 DOWN 就告警。它比任何日志都直接,因为它每隔一段时间真的去拿一次连接。
血泪经验是,我以前图快,装完直接mvn spring-boot:run看到 Started 就宣布搞定,结果第二天同事拉代码,在他机器上因为max-lifetime没配、连接串时区写成了UTC,批量插入的时间全部偏了八小时,查了半天以为是业务逻辑问题。从那以后我每次配完这套环境,都会强制走一遍上面三层验证,尤其是第二层那段元数据打印,跑一次只要几秒钟,省下的排查时间是以小时计的。整套的 pom、yml、建表脚本和自检代码我打在一个包里了,照着自己的路径改改就能复现。希望帮到你。
本文还有配套的精品资源,点击获取