本节目标:把「图书管理服务」的存储从内存
Map换成数据库,讲清spring-boot-starter-data-jpa依赖、spring.datasource.*配置、HikariCP 连接池参数与spring.jpa.*常用项,并给出启动期连不上库的排查路径。
适用版本:Spring Boot 4.1.x(Java 21)
12.1 数据源与连接配置
第 5 章到第 11 章,我们的 BookService 一直把数据放在一个 Map<Long, Book> 里。这在教学阶段没问题,但只要你重启应用,加进去的书就全没了。从本章开始,我们把「图书管理服务」的存储换成真正的数据库。整章三节共用同一个领域模型:
package com.example.bookstore.domain;
public record Book(
Long id,
String title,
String author,
String isbn,
int publishedYear) {
}
12.1 先把数据源接通,12.2 把 Book 变成实体并抽出 BookRepository,12.3 用派生查询把「按作者查、按年份区间查」这些需求写出来。本章示例统一用 H2 内存库,因为它零安装、可重复,跑一遍就有结果;末尾再给一套 MySQL 配置供你换到真实场景。
12.1.1 一次访问要经过多少层
很多人第一次配数据源时,把所有东西都叫「数据库连接」。实际上从你的代码到磁盘上的数据,中间至少隔了四层,任何一层配错都会在启动时炸掉:
| 层 | 职责 | 对应组件 |
|---|---|---|
| JDBC 驱动 | 把 JDBC 调用翻译成数据库的私有协议 | com.mysql:mysql-connector-j、com.h2database:h2 |
| 数据源 / 连接池 | 复用 TCP 连接、限制并发、处理失效重连 | HikariCP |
| ORM | 把 Java 对象映射成表行,生成 SQL | Hibernate 7.2 |
| 仓库抽象 | 提供 save、findById 等方法 | Spring Data JPA 2025.1 |
Spring Boot 的自动配置把后三层都装好了,你只需要提供两样东西:驱动依赖和连接参数。理解这张表,后面的每个配置项你都能对上号。
12.1.2 引入依赖
在 pom.xml 里加一个 starter 和两个驱动(演示用 H2,真实场景用 MySQL):
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- 演示用:H2 内存数据库,随应用启停 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- 真实场景:MySQL 8.x -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
spring-boot-starter-data-jpa 会传递引入 Hibernate 7.2、Spring Data JPA 2025.1 和 HikariCP 7.0——这正是 4.x 的依赖大版本口径。两个驱动都标 runtime,因为编译期我们只依赖 javax.sql.DataSource 这类标准接口,不需要具体的驱动类。
注意这里没有 spring-boot-starter-jdbc。data-jpa 已经包含了它,重复声明只会让依赖树更乱。
12.1.3 spring.datasource.* 配置项全解
最小可运行的 H2 配置只有三行。放进 src/main/resources/application.yml:
spring:
datasource:
url: jdbc:h2:mem:bookstore;DB_CLOSE_DELAY=-1
username: sa
password: ""
driver-class-name: org.h2.Driver
h2:
console:
enabled: true
path: /h2-console
jdbc:h2:mem:bookstore表示一个名为bookstore的内存库,进程内有效。DB_CLOSE_DELAY=-1让最后一个连接关闭后库依然保留,否则连接池一收缩数据就没了,调试时会很困惑。driver-class-name其实可以省略:Spring Boot 会从url的jdbc:前缀推断驱动。显式写上更利于排查。- 打开
h2-console后,访问http://localhost:8080/h2-console就能看到表和数据,填 JDBC URL 时务必和上面完全一致。
常用配置项速查:
| 属性 | 作用 | 备注 |
|---|---|---|
spring.datasource.url | JDBC 连接串 | 必填,决定连哪个库 |
spring.datasource.username | 用户名 | H2 默认 sa |
spring.datasource.password | 密码 | 空字符串要写 "",不能留空 |
spring.datasource.driver-class-name | 驱动类 | 通常可省略,自动推断 |
spring.datasource.type | DataSource 实现类 | 默认 Hikari 数据源,一般不改 |
spring.datasource.name | 连接池名字 | 多数据源时用来区分 |
spring.datasource.hikari.* | 连接池参数 | 见下一节 |
spring.datasource.connection-fetch | 连接获取时机 | 4.1 新增,见 12.1.7 |
换成 MySQL,只需要改 url 和凭据,其他都不动:
spring:
datasource:
url: jdbc:mysql://localhost:3306/bookstore?useSSL=false&serverTimezone=Asia/Shanghai
username: bookstore
password: ${DB_PASSWORD}
driver-class-name: com.mysql.cj.jdbc.Driver
密码写成 ${DB_PASSWORD} 是第 6 章讲过的外部化配置写法,避免把明文提交进仓库。
12.1.4 HikariCP 关键参数与默认值
HikariCP 是 Boot 默认的连接池。它默认值合理,绝大多数项目不需要改;但你必须知道它们,因为线上问题十有八九出在这里。下表是 4.x 下的默认值:
| 参数 | 默认值 | 含义 |
|---|---|---|
maximum-pool-size | 10 | 池中最多同时存在的连接数 |
minimum-idle | 同 maximum-pool-size | 池中保持的空闲连接数下限 |
connection-timeout | 30000 ms | 从池里借不到连接时的等待上限,超时抛异常 |
idle-timeout | 600000 ms(10 分钟) | 空闲连接被回收前的存活时间 |
max-lifetime | 1800000 ms(30 分钟) | 一条物理连接的最长寿命 |
validation-timeout | 5000 ms | 校验连接是否可用的超时 |
pool-name | HikariPool-1 | 池名,日志里会出现 |
配置时写在 spring.datasource.hikari 下:
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 3000
max-lifetime: 1200000
几个容易踩的点:
- 池不是越大越好。连接数是数据库的稀缺资源,20 个应用实例各开 20 条就是 400 条,MySQL 默认
max_connections只有 151。起步 10 条、压测后再调,比盲目调大安全得多。 max-lifetime必须小于数据库侧的连接存活上限。MySQL 默认wait_timeout是 28800 秒(8 小时),远大于 30 分钟,所以默认值是安全的;但若中间有防火墙掐空闲连接,就要把max-lifetime调到防火墙阈值以下,否则会拿到已被切断的连接。connection-timeout不是 SQL 执行超时。它只管「借连接」这一步。SQL 本身跑太久,要另配语句超时。
12.1.5 spring.jpa.* 常用配置
数据源接通后,Hibernate 还需要知道「怎么对待表结构」。常用项如下:
spring:
jpa:
hibernate:
ddl-auto: update
show-sql: true
open-in-view: false
properties:
hibernate:
format_sql: true
show-sql: true把 Hibernate 生成的 SQL 打到控制台,开发期观察行为很方便。properties.hibernate.format_sql: true让这些 SQL 换行缩进,否则是一整行,几乎没法读。open-in-view默认是true,它会把数据库连接一直持有到视图渲染结束,容易掩盖懒加载问题。新项目建议显式关掉(12.2 会看到它对LazyInitializationException的影响)。
ddl-auto 是本节最需要讲透的一个。它有五个取值,其中四个最常用:
| 取值 | 启动行为 | 数据风险 | 适用环境 |
|---|---|---|---|
none | 什么都不做 | 无 | 生产(配合迁移工具) |
validate | 校验表结构与实体是否一致,不一致则启动失败 | 无 | 生产(推荐) |
update | 增量补齐缺失的表/列 | 不删列、语义不可控 | 本地开发 |
create | 每次启动删表重建 | 数据全部丢失 | 集成测试 |
create-drop | 启动建表,关闭时删表 | 数据全部丢失 | 演示、临时验证 |
补充一个容易忽略的默认值规则:当检测到嵌入式数据库(如 H2)时,ddl-auto 默认是 create-drop;连接外部数据库时默认是 none。这就是为什么很多人本地用 H2 不写任何 ddl-auto 也能跑起来——它一直在偷偷删表重建。
12.1.6 为什么 ddl-auto 只适合开发环境
update 看起来最省事,加个字段自动补列,为什么生产不能用?三个理由:
- 它只加不减。删掉一个实体字段,对应的列会留在表里;改字段类型,它通常也不会动。跑久了表结构和你的实体就悄悄对不上了。
- 它不处理数据迁移。给一张已有数据的表加一个非空列,
update会直接失败,或者留下语义不明的默认值。 - 多实例会打架。生产通常多个实例同时启动,多个进程并发执行 DDL,轻则报错,重则锁表。
正确的分工是:开发和演示用 update(或 H2 默认的 create-drop),生产用 validate,真正的表结构变更交给迁移工具。第 15 章的 Flyway 就是干这个的:用版本化的 SQL 脚本管理 schema,ddl-auto 退化成一道「实体和库是否一致」的守门校验。
12.1.7 多数据源的简要说明
有些系统需要同时连两个库(比如主库 + 只读库)。Spring Boot 只自动配置一个 DataSource,第二个必须自己声明:
@Configuration
class SecondDataSourceConfig {
@Bean
@ConfigurationProperties("app.second-datasource")
DataSource secondDataSource() {
return DataSourceBuilder.create().build();
}
}
@ConfigurationProperties 会把 app.second-datasource.* 绑到返回的 DataSource 上。要点是:一个标 @Primary 作为默认,其余的靠 @Qualifier 精确注入;如果两边都要 JPA,还要各自声明 EntityManagerFactory 和 @EnableJpaRepositories(basePackages = ...) 把仓库分到不同包。
多数据源的心智成本很高,能用单库就别上。确实需要读写分离时,优先考虑分库分表中间件或 Spring Data 的多模块能力,而不是手工拼两套 EntityManagerFactory。
12.1.8 4.1 新增:惰性 JDBC 连接获取
4.1 引入了一个新属性 spring.datasource.connection-fetch:
spring:
datasource:
connection-fetch: lazy
默认行为(eager)下,连接池在启动阶段就会尝试建立/校验连接,数据库不可用会直接让应用启动失败。设为 lazy 后,真正的连接获取推迟到第一次执行 SQL 时才发生,于是应用可以先把容器启动起来,等数据库恢复后再服务请求。
它适合两类场景:数据库与应用一起编排启动、数据库启动比应用慢;以及你希望「应用能起来」和「数据库能连通」两件事解耦,由健康检查而不是启动失败来暴露问题。要注意 lazy 不是免死金牌——第一次请求仍会失败,只是失败点从启动期挪到了运行期,必须配合就绪探针一起用。
12.1.9 启动时连接失败:真实报错与排查
配置写错时,最典型的报错长这样:
2026-09-26T10:00:03.118+08:00 ERROR 51234 --- [ main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Exception during pool initialization.
java.sql.SQLException: Connection refused
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:112)
...
Caused by: java.net.ConnectException: Connection refused
at sun.nio.ch.Net.pollConnect(Native Method)
...
看到 Exception during pool initialization,就按下面顺序查:
- 库起来了吗。
Connection refused基本就是端口没人监听,先确认数据库进程和端口。 - URL 对吗。库名拼错、端口写错、时区参数缺失(MySQL 常见)都会在这里暴露。
- 凭据对吗。用户名密码错误通常是
Access denied for user。 - 驱动在不在。忘了加驱动依赖会报
Failed to load driver class。 - 根本没配 URL。如果连数据源都没配,Boot 会报一句很直白的话:
Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.——看到它说明你既没配外部库,classpath 上也没有嵌入式驱动。
排查这类问题时,把 logging.level.com.zaxxer.hikari 调成 DEBUG,能看到池的创建、借出、归还全过程,比猜快得多。
小结
本节完成了从内存到数据库的切换,重点有四条:
- 接数据库只需两样东西:驱动依赖和连接参数;
spring-boot-starter-data-jpa已把 ORM、仓库抽象和连接池带齐。 - HikariCP 参数默认值基本够用,最需要理解的是
maximum-pool-size(别盲目调大)与max-lifetime(要小于数据库侧的空闲上限)。 ddl-auto是开发期工具:update方便但不可控,生产请用validate,真正的结构变更交给第 15 章的 Flyway。- 4.1 的
spring.datasource.connection-fetch=lazy把连接获取推迟到首次使用,用于解耦应用启动与数据库可用性。
数据源通了,但 Book 现在还是一张表和一堆字段,没有任何 Java 侧映射。下一节我们把它变成 @Entity,并抽出 BookRepository。
阅读导航:上一节:11.3 拦截器与过滤器 · 下一节:12.2 实体与 Repository 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。