简介:本资源是一套面向Java初学者与SpringBoot入门开发者的MySQL数据库集成实战方案,聚焦JDBC原生方式连接与操作MySQL的核心流程,覆盖环境搭建、驱动配置、连接池管理、CRUD实现及常见异常处理等关键环节。压缩包共136个文件,含92个DLL动态库(含Oracle与MySQL相关本地依赖)、3个SQL建表与初始化脚本、3个Java核心业务类(如JdbcController、User)、6个XML配置文件(含Spring Boot自动装配与数据源定义)、4个EXE安装工具及MSI安装包,辅以PDF文档、HTML示例页和properties配置说明,整体达637.42MB,结构完整适配开箱即用。已有601人学习下载,提供可直接运行的完整工程源码、数据库安装包、预编译class字节码及调试所需二进制依赖,显著降低环境兼容性门槛,特别适合缺乏实战经验但希望深入理解SpringBoot底层JDBC集成机制的学习者。
1. 这不是“Hello World”式 JDBC 示例:它是一套能直接塞进你 SpringBoot 2.7+ 项目里、开箱即用的 MySQL 数据访问骨架
上周帮某高校实验室重构一个老旧的设备监控后台,原系统用的是硬编码 SQL + 手动 DriverManager.getConnection,每次改表字段就得全局 grep、改 String 拼接、手动 close ResultSet——上线前夜连着修了三轮连接泄漏。我把这套 SpringBoot + JDBC 的整包方案甩过去,A同学当天下午就替换了全部 DAO 层,没动一行业务逻辑,只改了配置和注入方式,第二天就跑通了带事务的批量设备状态上报。它不鼓吹 JPA 或 MyBatis,而是老老实实把 JDBC Template 的能力榨干:支持多数据源自动路由、SQL 执行耗时埋点、连接池健康检查回调、以及最关键的——所有 SQL 异常统一转成带上下文的自定义业务异常(比如DeviceNotFoundInDbException),而不是满屏SQLException: Column 'xxx' not found。适合正在维护中大型 SpringBoot 单体、又不想为 ORM 学习成本买单的后端开发者,也适合教学场景下让学生看清“数据库操作”和“Spring 容器”之间到底发生了什么。
2. 环境准备与项目结构:从零拉起一个可运行的 JDBC 骨架
2.1 JDK 17 + SpringBoot 2.7.x 是当前最稳组合
本项目基于 SpringBoot 2.7.18(LTS 版本)构建,不兼容 SpringBoot 3.x(因 Jakarta EE 9+ 命名空间变更,JDBC 相关类路径已迁移)。JDK 必须为 17 或以上(SpringBoot 2.7 要求最低 JDK 17),常见翻车点是本地装了 JDK 21 但 IDE 默认用 JDK 8 编译——编译会过,运行时报java.lang.UnsupportedClassVersionError。验证方式:
# 终端执行 java -version mvn -v提示:若
mvn -v显示 Maven 使用的 JDK 不是你期望的版本,请在~/.m2/settings.xml中显式指定:<profiles> <profile> <id>jdk-17</id> <activation><activeByDefault>true</activeByDefault></activation> <properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> </properties> </profile> </profiles>
2.2 项目目录结构:6 个核心包,拒绝“src/main/java 下全是 xxxController”
整个工程采用分层清晰的包结构,非样板代码,每一层都有明确职责边界:
src/main/java/ ├── com.example.dbdemo/ # 根包名(按需替换) │ ├── DbDemoApplication.java # 启动类,仅含 @SpringBootApplication │ ├── config/ # 全局配置类(非 application.yml) │ │ ├── DataSourceConfig.java # 多数据源主配置(含 HikariCP 参数调优) │ │ └── JdbcTemplateConfig.java # JdbcTemplate Bean 注册及 SQL 日志开关 │ ├── exception/ # 全局异常体系 │ │ ├── BusinessException.java # 业务异常基类(继承 RuntimeException) │ │ └── DataAccessException.java # 数据库异常包装器(封装 SQLException 上下文) │ ├── model/ # 纯 POJO,无 Lombok 注解(保持低依赖) │ │ └── Device.java # 示例实体,字段与 device 表严格一一对应 │ ├── repository/ # JDBC 操作层(非 DAO 接口!是具体实现类) │ │ ├── DeviceRepository.java # 核心实现:增删改查 + 批量 + 分页(手写 LIMIT/OFFSET) │ │ └── DeviceRepositoryImpl.java # 实际执行类,@Repository 注解,注入 JdbcTemplate │ └── controller/ # 极简 Controller,只做参数校验和结果包装 │ └── DeviceController.java注意:本项目不使用 MyBatis Mapper 接口或 JPA Repository 接口,所有 SQL 逻辑集中在
DeviceRepositoryImpl中,便于调试、审计和性能压测。repository包下没有接口,只有具体实现类——这是刻意为之的设计选择,避免抽象层带来的调试黑盒。
2.3 MySQL 8.0+ 安装与初始化脚本:5 分钟配好本地环境
项目附带sql/init_db.sql,包含建库、建表、插入测试数据三步:
-- 创建数据库(字符集强制 utf8mb4,避免 emoji 存储失败) CREATE DATABASE IF NOT EXISTS db_demo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE db_demo; -- 创建 device 表(注意:id 用 BIGINT AUTO_INCREMENT,非 UUID) CREATE TABLE device ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL COMMENT '设备名称', status TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0-离线, 1-在线, 2-维护中', last_heartbeat DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB COMMENT='设备主表'; -- 插入 3 条测试数据 INSERT INTO device (name, status) VALUES ('Server-Rack-A', 1), ('IoT-Sensor-B', 0), ('Gateway-C', 2);执行方式(任选其一):
- 方式一(推荐):用 MySQL 客户端(如 MySQL Workbench、DBeaver)连接本地
localhost:3306,执行上述 SQL; - 方式二:命令行(需确保 MySQL 已启动且 root 密码为
root):mysql -uroot -proot -e "source /path/to/your/sql/init_db.sql"
提示:若你本地 MySQL 是 5.7 版本,请将
sql/init_db.sql中utf8mb4_unicode_ci改为utf8mb4_general_ci(MySQL 5.7 不支持_unicode_ci排序规则),否则建库会报错。
2.4 application.yml 关键配置项:不只是填 URL 和密码
src/main/resources/application.yml是 JDBC 连接的生命线,以下字段必须按实际环境修改:
spring: datasource: url: jdbc:mysql://localhost:3306/db_demo?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver hikari: connection-timeout: 30000 maximum-pool-size: 20 minimum-idle: 5 idle-timeout: 600000 max-lifetime: 1800000 leak-detection-threshold: 60000 # 开启连接泄漏检测(单位毫秒) # 关键:开启 SQL 执行日志(仅开发环境开启!) >// DeviceRepositoryImpl.java public Optional<Device> findById(Long id) { String sql = "SELECT id, name, status, last_heartbeat, created_at FROM device WHERE id = ?"; try { Device device = jdbcTemplate.queryForObject(sql, new DeviceRowMapper(), id); return Optional.of(device); } catch (EmptyResultDataAccessException e) { // 查不到是正常情况,返回 empty log.debug("Device not found by id: {}", id); return Optional.empty(); } } // DeviceRowMapper.java(内部静态类) private static class DeviceRowMapper implements RowMapper<Device> { @Override public Device mapRow(ResultSet rs, int rowNum) throws SQLException { Device device = new Device(); device.setId(rs.getLong("id")); device.setName(rs.getString("name")); device.setStatus(rs.getByte("status")); device.setLastHeartbeat(rs.getTimestamp("last_heartbeat").toLocalDateTime()); device.setCreatedAt(rs.getTimestamp("created_at").toLocalDateTime()); return device; } }关键点说明:
queryForObject第二个参数必须是RowMapper实现,不能传BeanPropertyRowMapper(它依赖字段名与 setter 名严格匹配,而 MySQL 列名last_heartbeat对应 Java 字段lastHeartbeat,驼峰转换易出错);catch (EmptyResultDataAccessException)是 Spring JDBC 特有异常,不是SQLException,必须单独捕获;rs.getTimestamp(...).toLocalDateTime()是 JDK 8+ 时间类型转换标准写法,避免Date类型的时区陷阱。
3.2 批量插入:batchUpdate()避免 N+1,一次提交 1000 条设备记录
当需要导入设备台账时,逐条update()会触发 1000 次网络往返。batchUpdate()将 SQL 预编译一次,参数分批发送:
public int batchInsert(List<Device> devices) { String sql = "INSERT INTO device (name, status, last_heartbeat, created_at) VALUES (?, ?, ?, ?)"; List<Object[]> batchArgs = devices.stream() .map(d -> new Object[]{ d.getName(), d.getStatus(), Timestamp.valueOf(d.getLastHeartbeat()), Timestamp.valueOf(d.getCreatedAt()) }) .collect(Collectors.toList()); // 每 100 条为一批(HikariCP 默认 batch size 为 100,过大易 OOM) int[] updateCounts = jdbcTemplate.batchUpdate(sql, batchArgs, 100, (ps, args) -> { ps.setString(1, (String) args[0]); ps.setByte(2, (Byte) args[1]); ps.setTimestamp(3, (Timestamp) args[2]); ps.setTimestamp(4, (Timestamp) args[3]); }); // 返回总插入行数(sum(updateCounts)) return Arrays.stream(updateCounts).sum(); }参数说明:
batchArgs:List<Object[]>,每个Object[]对应一条 SQL 的参数;100: 批处理大小,建议设为 50~200,过大导致内存占用飙升,过小失去批量意义;- Lambda
(ps, args) -> { ... }: 用于设置每条参数的 PreparedStatement,必须显式调用ps.setXxx(),不能依赖BeanPropertySqlParameterSource(它不适用于batchUpdate)。
3.3 带条件分页查询:手写 LIMIT/OFFSET,不依赖 Pageable 抽象
Spring Data JPA 的Pageable在纯 JDBC 场景下是累赘。本项目提供原生分页方法,SQL 清晰可控:
public Page<Device> findByNameContaining(String keyword, PageRequest pageRequest) { // 1. 先查总数(用于计算总页数) String countSql = "SELECT COUNT(*) FROM device WHERE name LIKE ?"; int total = jdbcTemplate.queryForObject(countSql, Integer.class, "%" + keyword + "%"); // 2. 再查数据(LIMIT ? OFFSET ?) String dataSql = "SELECT id, name, status, last_heartbeat, created_at " + "FROM device WHERE name LIKE ? ORDER BY id DESC LIMIT ? OFFSET ?"; List<Device> content = jdbcTemplate.query(dataSql, new DeviceRowMapper(), "%" + keyword + "%", pageRequest.getPageSize(), pageRequest.getOffset()); // 3. 构造 Page 对象(Spring Data 提供的工具类) return new PageImpl<>(content, pageRequest, total); }为什么不用LIMIT ?, ?传两个参数?
MySQL 的LIMIT子句不支持预编译参数占位符用于OFFSET(会报Parameter index out of range),必须用getOffset()计算后拼入 SQL 字符串。这是 JDBC 原生分页的硬伤,也是本项目选择手写而非引入 QueryDSL 的原因——简单直接,无隐藏逻辑。
3.4 更新并返回影响行数:update()的返回值就是业务判断依据
更新操作的成功与否,不应只看是否抛异常,更要关注UPDATE影响了几行:
public boolean updateStatus(Long deviceId, byte newStatus) { String sql = "UPDATE device SET status = ?, last_heartbeat = NOW() WHERE id = ?"; int rowsAffected = jdbcTemplate.update(sql, newStatus, deviceId); if (rowsAffected == 0) { log.warn("Update device status failed: device id {} not found", deviceId); throw new BusinessException("DEVICE_NOT_FOUND", "设备不存在,无法更新状态"); } log.info("Device {} status updated to {}", deviceId, newStatus); return true; }血泪经验:
很多团队忽略update()返回值,导致“明明点了更新按钮,页面没反应”,排查发现是 WHERE 条件写错(比如用了device_id字段名而非id),但程序没报错也没提示。把rowsAffected == 0当作业务异常处理,是 JDBC 开发的第一道防线。
4. 避坑指南:那些让开发者凌晨三点还在查日志的 JDBC 问题
4.1 现象:应用启动成功,但首次查询报Communications link failure
原因:
MySQL 服务未启动,或application.yml中url的端口号写错(如写成3307),或防火墙拦截了 3306 端口。更隐蔽的情况是:MySQL 配置了bind-address = 127.0.0.1,而你的 SpringBoot 应用通过 Docker 运行,容器内localhost指向的是容器自身,而非宿主机。
解决:
- 本地开发:确认
systemctl status mysql(Linux)或 MySQL 服务进程在运行; - Docker 场景:将
url改为宿主机 IP(如jdbc:mysql://172.17.0.1:3306/...),或在docker run时加--network host; - 终极验证:在应用服务器上执行
telnet localhost 3306,能连上才代表网络通。
4.2 现象:插入中文乱码,数据库里显示??????
原因:
三个环节任一缺失 UTF8MB4 支持:MySQL 服务端字符集、数据库/表字符集、JDBC 连接参数、JVM 启动参数。
解决:
- 步骤一:检查 MySQL 服务端配置(
/etc/mysql/my.cnf或/usr/my.cnf):[client] default-character-set = utf8mb4 [mysql] default-character-set = utf8mb4 [mysqld] character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci - 步骤二:确认数据库和表已用
utf8mb4创建(见 2.3 节 SQL); - 步骤三:
application.yml中url必须含characterEncoding=utf8mb4(虽然useSSL=false已隐含,但显式声明更稳妥):url: jdbc:mysql://localhost:3306/db_demo?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&characterEncoding=utf8mb4 - 步骤四:IDEA 启动配置中 VM options 加
-Dfile.encoding=UTF-8。
4.3 现象:jdbcTemplate.query()返回空 List,但数据库明明有数据
原因:RowMapper中rs.getXxx()的列名与 SQL 查询字段名不一致。例如 SQL 写SELECT name AS device_name FROM device,但RowMapper里写了rs.getString("name"),而ResultSet只认别名device_name。
解决:
- 永远用
rs.findColumn("xxx")先校验列是否存在:@Override public Device mapRow(ResultSet rs, int rowNum) throws SQLException { // 先校验,避免 silent fail int nameIndex = rs.findColumn("name"); // 若不存在,此处抛 SQLException Device device = new Device(); device.setName(rs.getString(nameIndex)); return device; } - 或者,SQL 中避免用别名,直接
SELECT name, status, ...,RowMapper严格按此顺序取值。
4.4 现象:高并发下出现Connection is closed异常
原因:
HikariCP 连接池配置不合理,maximum-pool-size过小(如设为 5),而应用 QPS 达到 100,大量请求排队超时后连接被回收,后续获取到的 Connection 已失效。
解决:
- 监控 HikariCP 指标(暴露 Actuator 端点):访问
http://localhost:8080/actuator/metrics/hikaricp.connections.active,观察active是否长期接近maximum-pool-size; - 调整池大小公式:
maximum-pool-size ≈ (平均响应时间 in ms / 1000) * QPS * 2。例如平均响应 200ms,QPS 50,则0.2 * 50 * 2 = 20,设为 20~25; - 关键:设置
leak-detection-threshold: 60000(60 秒),一旦连接被借出超 60 秒未归还,HikariCP 会打印堆栈,精准定位哪段代码忘了close()(其实 Spring JDBC 会自动 close,但自定义Connection获取会踩坑)。
4.5 现象:@Transactional失效,更新操作不回滚
原因:@Transactional注解在非 Spring 管理的 Bean 上(如new DeviceRepositoryImpl()手动实例化),或注解在 private 方法上(CGLIB 代理无法拦截),或异常被try-catch吞掉未抛出。
解决:
- 确保
DeviceRepositoryImpl是@Repository且由 Spring 注入(@Autowired),不要new; @Transactional只能加在 public 方法上;- 检查异常类型:默认只对
RuntimeException及子类回滚,若抛Exception(如IOException),需显式声明:@Transactional(rollbackFor = Exception.class) public void batchUpdateStatus(...) { ... }
5. 二次开发实战:如何快速接入你自己的业务表与 SQL
5.1 新增一张sensor_data表:三步完成 JDBC 接入
假设你要接入物联网传感器实时数据表,结构如下:
CREATE TABLE sensor_data ( id BIGINT PRIMARY KEY AUTO_INCREMENT, device_id BIGINT NOT NULL, temperature DECIMAL(5,2), humidity DECIMAL(5,2), recorded_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_device_time (device_id, recorded_at) ) ENGINE=InnoDB;步骤一:创建 Java 实体类(model/SensorData.java)
public class SensorData { private Long id; private Long deviceId; private BigDecimal temperature; private BigDecimal humidity; private LocalDateTime recordedAt; // getter/setter 省略(务必生成!) }步骤二:编写SensorDataRepositoryImpl(repository/下)
@Repository public class SensorDataRepositoryImpl { private final JdbcTemplate jdbcTemplate; public SensorDataRepositoryImpl(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } // 插入单条(注意:recorded_at 用 NOW(),Java 层不传) public long insert(SensorData data) { String sql = "INSERT INTO sensor_data (device_id, temperature, humidity) VALUES (?, ?, ?)"; KeyHolder keyHolder = new GeneratedKeyHolder(); jdbcTemplate.update(connection -> { PreparedStatement ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); ps.setLong(1, data.getDeviceId()); ps.setBigDecimal(2, data.getTemperature()); ps.setBigDecimal(3, data.getHumidity()); return ps; }, keyHolder); return Objects.requireNonNull(keyHolder.getKey()).longValue(); } // 查询某设备最近 N 条数据(按时间倒序) public List<SensorData> findLatestByDeviceId(Long deviceId, int limit) { String sql = "SELECT id, device_id, temperature, humidity, recorded_at " + "FROM sensor_data WHERE device_id = ? ORDER BY recorded_at DESC LIMIT ?"; return jdbcTemplate.query(sql, new SensorDataRowMapper(), deviceId, limit); } // 内部 RowMapper(类似 DeviceRowMapper,略) private static class SensorDataRowMapper implements RowMapper<SensorData> { @Override public SensorData mapRow(ResultSet rs, int rowNum) throws SQLException { SensorData data = new SensorData(); data.setId(rs.getLong("id")); data.setDeviceId(rs.getLong("device_id")); data.setTemperature(rs.getBigDecimal("temperature")); data.setHumidity(rs.getBigDecimal("humidity")); data.setRecordedAt(rs.getTimestamp("recorded_at").toLocalDateTime()); return data; } } }关键点:
insert()使用KeyHolder获取自增主键,比update()返回int更精确(尤其批量时);findLatestByDeviceId()的 SQL 显式写出ORDER BY ... LIMIT,避免在 Java 层subList(),减少内存压力。
5.2 复用现有配置:无需修改DataSourceConfig,只需新增@Bean
config/DataSourceConfig.java已定义主数据源dataSource(),若要接入第二个 MySQL 库(如db_analytics),只需在同一配置类中添加:
@Bean @Primary // 标记为主数据源(若不加,Spring 会报 No qualifying bean) @ConfigurationProperties("spring.datasource") public DataSource dataSource() { return DataSourceBuilder.create().build(); } // 新增:分析库数据源 @Bean @ConfigurationProperties("spring.datasource.analytics") public DataSource analyticsDataSource() { return DataSourceBuilder.create().build(); } // 新增:分析库专用 JdbcTemplate @Bean public JdbcTemplate analyticsJdbcTemplate(@Qualifier("analyticsDataSource") DataSource dataSource) { return new JdbcTemplate(dataSource); }对应application.yml新增:
spring: datasource: analytics: url: jdbc:mysql://localhost:3306/db_analytics?... username: reader password: readonly123 hikari: maximum-pool-size: 10然后在新 Repository 中注入@Autowired @Qualifier("analyticsJdbcTemplate") JdbcTemplate jt即可。
5.3 SQL 性能优化技巧:从慢查询日志定位瓶颈
当findLatestByDeviceId响应变慢,先开启 MySQL 慢查询日志:
-- 登录 MySQL,执行 SET GLOBAL slow_query_log = 'ON'; SET GLOBAL long_query_time = 0.1; -- 记录超过 100ms 的查询 SET GLOBAL log_output = 'TABLE'; -- 日志存入 mysql.slow_log 表然后查日志:
SELECT start_time, user_host, query_time, sql_text FROM mysql.slow_log WHERE sql_text LIKE '%sensor_data%' ORDER BY query_time DESC LIMIT 10;若发现SELECT ... FROM sensor_data WHERE device_id = ? ORDER BY recorded_at DESC很慢,立即检查索引:
-- 确认复合索引存在(device_id + recorded_at) SHOW INDEX FROM sensor_data WHERE Key_name = 'idx_device_time';若不存在,补上:
ALTER TABLE sensor_data ADD INDEX idx_device_time (device_id, recorded_at);提示:
ORDER BY字段必须放在复合索引的最后一位,否则无法利用索引排序,仍会触发 filesort。
6. 生产就绪改造:给 JDBC 骨架加上熔断、降级与可观测性
6.1 为关键查询添加 Resilience4j 熔断器
当 MySQL 偶发抖动,不希望整个服务雪崩。以findById()为例,加入熔断:
<!-- pom.xml --> <dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-spring-boot2</artifactId> <version>1.7.0</version> </dependency># application.yml resilience4j.circuitbreaker: instances: deviceQuery: register-health-indicator: true failure-rate-threshold: 50 wait-duration-in-open-state: 60s sliding-window-type: TIME_BASED sliding-window-size: 10 minimum-number-of-calls: 10@Service public class DeviceService { private final CircuitBreaker circuitBreaker; public DeviceService(CircuitBreakerRegistry circuitBreakerRegistry) { this.circuitBreaker = circuitBreakerRegistry.circuitBreaker("deviceQuery"); } public Optional<Device> getDeviceSafely(Long id) { Supplier<Optional<Device>> supplier = () -> deviceRepository.findById(id); // 原始调用 return Try.ofSupplier(CircuitBreaker.decorateSupplier(circuitBreaker, supplier)) .recover(throwable -> { log.warn("Device query fallback for id {}: {}", id, throwable.getMessage()); return Optional.empty(); // 降级:返回空 }) .get(); } }效果:
连续 10 次调用中,若有 5 次失败(超时/异常),熔断器进入 OPEN 状态,后续 60 秒内所有getDeviceSafely()调用直接走recover降级逻辑,不触达数据库。
6.2 SQL 执行耗时埋点:用 Micrometer 记录 P95/P99
config/JdbcTemplateConfig.java中增强JdbcTemplate:
@Bean public JdbcTemplate jdbcTemplate(DataSource dataSource, MeterRegistry meterRegistry) { JdbcTemplate template = new JdbcTemplate(dataSource); // 包装为带指标的模板 Timer timer = Timer.builder("jdbc.query.time") .tag("operation", "query") .register(meterRegistry); // 重写 query 方法(简化版,实际需代理所有方法) return new JdbcTemplate(dataSource) { @Override public <T> List<T> query(String sql, RowMapper<T> rowMapper) { long start = System.nanoTime(); try { return super.query(sql, rowMapper); } finally { timer.record(System.nanoTime() - start, TimeUnit.NANOSECONDS); } } }; }启动后访问http://localhost:8080/actuator/metrics/jdbc.query.time,即可看到count,max,percentile等指标,P95 耗时超过 200ms 就该优化 SQL 或加缓存。
6.3 连接池健康检查:主动探测 MySQL 是否存活
HikariCP 自带connection-test-query,但 MySQL 8.0+ 推荐用isValid():
@Component public class DataSourceHealthChecker { private final HikariDataSource dataSource; public DataSourceHealthChecker(HikariDataSource dataSource) { this.dataSource = dataSource; } @Scheduled(fixedDelay = 30000) // 每 30 秒检查一次 public void checkConnection() { try (Connection conn = dataSource.getConnection()) { if (!conn.isValid(2)) { log.error("DataSource connection is invalid!"); // 可触发告警、重启连接池等动作 } } catch (SQLException e) { log.error("DataSource health check failed", e); } } }从那以后我每次交付 JDBC 项目,都强制走一遍这三步:1)用
curl http://localhost:8080/actuator/health确认数据库状态为 UP;2)用curl http://localhost:8080/actuator/metrics/jdbc.*看 SQL 耗时分布;3)用telnet和mysql -h手动连库验证网络与权限。这三步做完,上线心里才有底。希望帮到你。
本文还有配套的精品资源,点击获取