☰
Spring Boot 本地连接 MySQL 实战:从配置到 CRUD 全流程避坑指南
2026/10/16 10:04:15 网站建设 项目流程

很多人在本地写 Spring Boot 项目时,第一步不是写业务代码,而是卡在“怎么让项目连上 MySQL”。明明照着官方文档配了 spring.datasource.url,启动却报一堆红色异常,或者是 Connection refused,或者是 Access denied,再或者是一连串看不懂的时区警告。这篇文章就是围绕“Spring Boot 项目本地连接并操作 MySQL 数据库”这件事展开的,把我实际折腾过的坑、配置、代码和排查思路都整理出来,给那些刚开始接触 Spring Boot + MySQL、想在本地快速跑通一条数据链路的朋友做参考。

我不打算只贴一段能跑的配置就完事,而是会带你理清为什么这么配、底层发生了什么、遇到问题该怎么定位。整个过程以 Spring Boot 2.7 版本为主,配合 MySQL 8.0,讲解 JPA 和 MyBatis 两种操作数据库的方式。无论你是刚学后端、准备做毕业设计,还是工作中需要搭一个本地开发环境,这篇文章都能帮你少走弯路。

1. 项目背景与核心思路拆解

1.1 这个项目到底在解决什么问题

Spring Boot 项目本地连接 MySQL,听起来像是一个“填空题”,实际做起来却牵扯到驱动加载、连接参数、账号权限、连接池、ORM 映射等多个环节。标题里“本地连接并操作”这六个字,核心需求其实包含三层:

第一层是“连得上”。项目启动时能够成功从应用服务器通过 JDBC 协议连到本地 MySQL 实例,建立物理连接,并且能通过这条连接执行 SQL 语句。

第二层是“操作得了”。不只是执行 SELECT,还包括增、删、改,以及事务处理。Spring Boot 项目里通常不会直接写 JDBC 原生代码,而是通过 Spring Data JPA、MyBatis 这类持久层框架帮我们完成对象和数据库表之间的转换。

第三层是“开发体验好”。本地开发反复重启项目是家常便饭,如果连接池、配置、数据库初始化这些环节不顺畅,每次启动都要等半天,遇到连接超时还得改配置重启,整个人的状态都会受影响。

所以这个项目解决的,实际是一条“从配置文件到数据库表,再从数据库表到接口返回”的完整链路打通问题。打通之后,你才有精力去关注业务逻辑、缓存、异步任务这些更高层次的东西。

1.2 为什么选 Spring Boot + MySQL 这个组合

技术选型上,Spring Boot 和 MySQL 的组合几乎已经成了 Java 后端项目的默认配置,理由也很直接。

Spring Boot 提供了自动配置机制。只要在 classpath 里加入 spring-boot-starter-data-jpa 或 mybatis-spring-boot-starter,再把数据源参数写到 application.yml 中,容器启动时会自动创建 DataSource、SqlSessionFactory 或 EntityManagerFactory 这些核心对象。对本地开发来说,你不需要像以前写 SSM 框架时那样手动配置一大堆 XML,一个 starter 依赖加几行配置就能跑起来。

MySQL 本身是开源、免费、部署简单的数据库,本地装一个服务端就能用,语法也非常通用。Java 生态中的 MySQL Connector/J 驱动由官方维护,兼容性有保障。再加上 MySQL 8.0 以后默认使用 caching_sha2_password 认证插件,驱动版本也要跟上,否则会碰到“Public Key Retrieval is not allowed”这种奇怪的报错,这块后面细讲。

更进一步说,Spring Boot 还帮我们收集了大量最佳实践。比如连接池默认使用 HikariCP,它是目前性能最好的连接池之一,零配置也能正常工作。你只需要关心业务,而不是关心怎么管理数据库连接。这种“默认值合理、可配置项丰富”的特质,非常适合本地开发,也适合项目慢慢长大之后做生产化改造。

1.3 操作数据库的三种方式怎么选

Spring Boot 操作 MySQL 的主流方式有三种,很多新手在这里纠结,我直接说结论和建议。

第一,直接用 JdbcTemplate。它是 Spring JDBC 模块提供的轻量封装,SQL 由你手写,参数用占位符绑定,代码量不算多,适合写固定 SQL、不想引入复杂映射框架的小项目。用它可以清楚看到 JDBC、Connection、PreparedStatement、ResultSet 这些底层概念是怎么被一步步封装的。

第二,用 Spring Data JPA。这是基于 Hibernate 的 ORM 框架,核心是“实体类映射表结构”,你定义一个实体类,然后写一个继承 JpaRepository 的接口,常用的 CRUD 方法框架自动实现,不用写 SQL。适合实体关系复杂、业务以增删改查为主的场景。缺点是遇到复杂查询时,要么写 JPQL,要么走原生 SQL,学习成本并不低。

第三,用 MyBatis。SQL 由开发者自己掌控,通过注解或 XML 描述参数和结果集的映射关系,适合复杂报表查询、动态 SQL 较多的项目。国内团队用得很多,因为它对 SQL 优化比较友好,DBA 审查时直接看 XML 就行,而不用去猜框架生成的 SQL。

我个人的建议是:如果只是为了这个标题下的练习,想快速看到效果,选 JPA 最简单。如果你希望提前熟悉国内企业主流技术栈,那可以把 MyBatis 一起跑一遍。这篇文章后面两条路线都会给出可执行的代码示例,方便你对照学习。

无论选哪种,底层都离不开 DataSource 和事务管理器。Spring Boot 自动配置会根据类路径判断你的选择,你只需要保证依赖不冲突,并且数据源配置正确即可。

2. 本地环境准备与依赖引入

2.1 本地环境我需要装点什么

先把环境清单列出来,一版比较稳妥的组合是:

  • JDK 1.8 或 JDK 11,Spring Boot 2.7 两个版本都能正常跑,推荐 1.8 保证兼容性。
  • Maven 3.6 以上,IDE 里自带也行。
  • MySQL 8.0 社区版,本地安装服务端并启动。
  • 一个开发工具,IntelliJ IDEA 或 Eclipse 都可以。
  • 一个数据库客户端,我用的是命令行加 Navicat 风格的工具,其实用 IDE 自带的数据库面板也够用。

mysql 命令行工具是关键,因为排查问题时常要用它直接执行 SQL。比如说你在 Spring Boot 里连接失败,首先要确认是不是 MySQL 服务本身没启动、账号密码是不是正确,这些都能通过命令行快速验证。

安装好后,在终端里执行mysql -u root -p,如果提示输入密码后能进入 mysql> 提示符,说明服务端没问题。这一步很多人会跳过,结果程序连不上时,先在 Spring 配置里反复改,其实根因就是 MySQL 服务没有启动,或者在 windows 服务里没有设为自动启动。

2.2 Maven 依赖这样加最省心

用 IDEA 新建一个 Spring Boot 项目时,可以在 Spring Initializr 里勾选 Web、MySQL Driver、Spring Data JPA、Lombok(可选)。如果你是手工建 Maven 项目,直接在 pom.xml 里加依赖就行。

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!-- 如果想用 MyBatis,则加上这个依赖,和>server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/test_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver hikari: minimum-idle: 2 maximum-pool-size: 10 connection-timeout: 30000 jpa: hibernate: ddl-auto: update show-sql: true properties: hibernate: format_sql: true

配置里有几个地方必须解释清楚,否则你照抄了也不知道为什么。

serverTimezone=Asia/Shanghai。MySQL 8 之前的版本连接时如果没有显式指定时区,驱动会读取 JVM 默认时区,容易出现时区不一致的问题;MySQL 8 更严格,可能直接报错The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized。老老实实加上这一项。

useSSL=false。本地开发一般不需要 SSL 加密,MySQL 8 默认虽然没有强制开启 SSL,但驱动会尝试进行 SSL 握手,有时候会因为本地证书问题抛异常。设为 false 明确关闭,减少干扰。

allowPublicKeyRetrieval=true。这是 MySQL 8 的 caching_sha2_password 认证插件带来的问题。如果你用 root 用户连接,服务器要求客户端获取公钥来加密密码,而默认情况下驱动禁止自动获取公钥,就会报Public Key Retrieval is not allowed。本地开发直接把这个设置为 true 是最快的解法。

ddl-auto这个属性能帮你自动建表。第一次练习时,把它设为update,Hibernate 会根据实体类自动创建或更新表。注意这只适合本地测试和生产初始化场景,正式环境不推荐用 Hibernate 管理表结构。

show-sql: true可以把 Hibernate 生成的 SQL 打印到控制台。这个属性本地开发建议开启,你能直观看到 JPA 到底执行了什么语句,对理解映射关系非常有帮助。但生产环境记得关掉,避免日志刷屏和敏感信息泄露。

3. 从零跑通一条数据链路

3.1 先准备一张数据库表和几条数据

环境配好之后,不要急着写 Java 代码,先把数据库和表建好。我习惯在命令行工具里执行,这样如果后面程序报“表不存在”,你能快速确认是不是数据库本身的问题。

登录 MySQL 后执行:

CREATE DATABASE IF NOT EXISTS test_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE test_db; CREATE TABLE IF NOT EXISTS `user` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `name` VARCHAR(50) NOT NULL, `email` VARCHAR(100) DEFAULT NULL, `age` INT DEFAULT NULL, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; INSERT INTO `user` (`name`, `email`, `age`) VALUES ('张三', 'zhangsan@example.com', 25), ('李四', 'lisi@example.com', 30);

用 utf8mb4 而不是 utf8,是因为 MySQL 的 utf8 实际只能存 3 字节字符,遇到 Emoji 或生僻字会存不进去。现在项目默认都用 utf8mb4,我们在 URL 里配置 characterEncoding=utf8,这里的 utf8 是 Java 侧的编码名,Java 会把它映射到数据库的 utf8mb4,不会冲突。

表名我故意用了user,这个在 MySQL 里并不是保留字,但是在某些 SQL 方言里可能比较敏感,实际项目里建议用sys_user、t_user这类前缀,避免后续扩展或者切换数据库时踩坑。

3.2 使用 Spring Data JPA 完成基础 CRUD

既然加了spring-boot-starter-data-jpa,就先走 JPA 路线。首先是实体类,注意命名要与表结构对应,但不用完全一致,通过注解映射即可。

package com.example.demo.entity; import javax.persistence.*; import java.time.LocalDateTime; @Entity @Table(name = "user") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "name", nullable = false, length = 50) private String name; private String email; private Integer age; @Column(name = "create_time", insertable = false, updatable = false) private LocalDateTime createTime; // 省略 getter/setter,用 Lombok 的话直接加 @Data 即可 }

关键点:主键@GeneratedValue(strategy = GenerationType.IDENTITY)对应 MySQL 的自增主键,这样插入时数据库会自动生成 id。create_time列我设置了insertable = false, updatable = false,意思是 JDBC 操作时不把该字段作为 SQL 参数,让数据库自己去用CURRENT_TIMESTAMP填充,避免 Java 端插入 null。

接着定义 Repository 接口:

package com.example.demo.repository; import com.example.demo.entity.User; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; public interface UserRepository extends JpaRepository<User, Long> { List<User> findByNameContaining(String keyword); List<User> findByAgeGreaterThan(Integer age); }

JpaRepository<T, ID>会默认提供save、findById、findAll、deleteById等方法,你不需要写任何实现。新的查询方法只要按照 Spring Data 的方法命名规范(比如 findByNameContaining)声明,框架会自动生成查询逻辑。

现在写一个 Controller 来暴露接口:

package com.example.demo.controller; import com.example.demo.entity.User; import com.example.demo.repository.UserRepository; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/users") public class UserController { private final UserRepository userRepository; public UserController(UserRepository userRepository) { this.userRepository = userRepository; } @GetMapping public List<User> list() { return userRepository.findAll(); } @GetMapping("/{id}") public User detail(@PathVariable Long id) { return userRepository.findById(id).orElse(null); } @PostMapping public User create(@RequestBody User user) { return userRepository.save(user); } @PutMapping("/{id}") public User update(@PathVariable Long id, @RequestBody User user) { user.setId(id); return userRepository.save(user); } @DeleteMapping("/{id}") public void delete(@PathVariable Long id) { userRepository.deleteById(id); } }

启动项目后,直接在浏览器访问http://localhost:8080/users,就能看到查询出来的 JSON 数组。如果你配置了show-sql: true,控制台会打印类似下面这句:

select u1_0.id,u1_0.age,u1_0.create_time,u1_0.email,u1_0.name from user u1_0

这说明 JPA 已经把 Java 方法调用转换成了 SQL,并成功和 MySQL 完成了数据交互。

3.3 用 MyBatis 结合原生 SQL 再跑一遍

如果你更想使用 MyBatis,我提供一个最小可用版本。假设实体类还是上面的 User 类,但不需要加@Entity注解,改成普通 POJO 即可。引入 MyBatis 依赖后,在启动类上加@MapperScan或者直接在 Mapper 接口上打@Mapper注解。

package com.example.demo.mapper; import com.example.demo.entity.User; import org.apache.ibatis.annotations.*; import java.util.List; @Mapper public interface UserMapper { @Select("SELECT id, name, email, age, create_time AS createTime FROM user") List<User> findAll(); @Select("SELECT id, name, email, age, create_time AS createTime FROM user WHERE id = #{id}") User findById(@Param("id") Long id); @Insert("INSERT INTO user(name, email, age) VALUES (#{name}, #{email}, #{age})") @Options(useGeneratedKeys = true, keyProperty = "id") int insert(User user); @Update("UPDATE user SET name = #{name}, email = #{email}, age = #{age} WHERE id = #{id}") int update(User user); @Delete("DELETE FROM user WHERE id = #{id}") int deleteById(@Param("id") Long id); }

注意三点:

一是实体属性名和表字段名的映射问题。我在 Java 里用驼峰createTime,数据库表里是下划线create_time,MyBatis 默认不会把两者自动对应。最简单的办法是在 application.yml 里开启驼峰映射:

mybatis: configuration: map-underscore-to-camel-case: true

开启之后,create_time会自动映射到createTime,不用手写 resultMap。

二是@Insert里我写的是INSERT INTO user(name, email, age),明确指定列名,而不是INSERT INTO user VALUES(...)。这样和数据库表结构的耦合度更低,以后新增字段不容易出错。

三是useGeneratedKeys = true的作用。加了这一项,数据库生成的自增 id 会回填到传入的 User 对象的 id 属性,方便插入后立刻拿到主键。很多新手忘了加,插入完永远不知道新数据 id 是多少,只能再查一次。

为了验证效果,可以写个简单的单元测试:

@SpringBootTest class UserMapperTest { @Autowired private UserMapper userMapper; @Test void testInsert() { User user = new User(); user.setName("王五"); user.setEmail("wangwu@example.com"); user.setAge(28); int rows = userMapper.insert(user); System.out.println("影响行数:" + rows + ",自增 id:" + user.getId()); } }

如果你看到测试输出里出现自增 id:xx,说明 MyBatis 这条链路也通了。

3.4 用 REST 接口验证本地连接是否正常

无论你选 JPA 还是 MyBatis,最终都需要一个外部入口来验证“本地连接并操作 MySQL”是否真正完成。最简单的测试方式是使用 Postman、curl,或者在浏览器中输入 GET 请求。

下面是我常用的几条 curl 命令:

curl http://localhost:8080/users curl -X POST http://localhost:8080/users \ -H "Content-Type: application/json" \ -d '{"name":"测试用户","email":"test@example.com","age":20}' curl -X PUT http://localhost:8080/users/1 \ -H "Content-Type: application/json" \ -d '{"name":"改过的名字","email":"new@example.com","age":25}' curl -X DELETE http://localhost:8080/users/1

如果你用的是 MyBatis 路线,记得把 Controller 里的 UserRepository 换成 UserMapper,包装一层 Service 会更规范,但本地快速验证时直接在 Controller 注入 Mapper 也没问题。

到这里,“连接并操作”已经完成了 80%。“连接”体现在 Spring Boot 启动后,连接池建立到 MySQL 的连接;“操作”体现在通过接口读写数据库记录。剩下 20% 是各种加持项,比如参数调优、事务控制、日志链路和异常统一处理。下面继续展开。

4. 连接池参数与配置细节

4.1 HikariCP 连接池参数到底怎么理解

Spring Boot 2.x 默认的数据库连接池就是 HikariCP。它的设计目标可以用“极快”一个词概括,但再怎么快,参数配置不对还是会出现连接超时、获取连接等待时间过长、连接池被占满等问题。

先理一下连接池的核心工作方式。连接池里会维护若干个数据库连接,项目启动时按配置初始化几条连接。每次从 Service 层执行 SQL 时,从连接池里“借”一条连接,执行完后“还”回去。如果项目并发超过连接数上限,新的请求就只能排队等待,等待时间超过 connectionTimeout 就报异常。

HikariCP 最重要的几个参数是:

  • maximumPoolSize:连接池允许的最大连接数,包含空闲连接和正在使用的连接。默认值是 10,本地开发建议设置在 10 左右,不需要调得太大。
  • minimumIdle:连接池希望保持的最少空闲连接数。默认和 maximumPoolSize 一样大,Hikari 官方建议直接把 minimumIdle 和 maximumPoolSize 设为相同值,避免连接数量频繁波动。
  • connectionTimeout:客户端从连接池获取连接的最长等待时间,单位毫秒,默认 30000。如果 30 秒内拿不到连接,会抛异常。
  • idleTimeout:空闲连接存活时间,默认 10 分钟。只有当 minimumIdle 小于 maximumPoolSize 时这个参数才会实际生效。
  • maxLifetime:连接的最大存活时间,默认 30 分钟。设置这个值是为了应对数据库、网络设备对空闲连接的限制,建议比数据库 wait_timeout 短一些。

本地开发我习惯这样配:

spring: datasource: hikari: minimum-idle: 3 maximum-pool-size: 10 connection-timeout: 10000 idle-timeout: 600000 max-lifetime: 1800000

connection-timeout从默认 30 秒改成 10 秒,是因为本地如果配置错了,不用傻等 30 秒才看到报错,能更快暴露问题,排除问题后也节省重启时间。

4.2 本地开发和线上配置有哪些差异

本地连接数据库,很多时候我们图省事直接把生产环境的连接池配置贴过来,这是没必要的。本地开发的特点是:并发量小、数据库服务就在本机、网络延迟几乎为零、经常重启应用。所以不需要很大的连接池,也不需要特别复杂的调优,关键是“快速失败”和“稳定”。

线上环境则相反,建议设置更严格的验证机制、合理的连接数上限、加上连接回收策略。比如线上 MySQL 默认的wait_timeout是 8 小时,如果某个连接空闲超过 8 小时,MySQL 服务端会断开它,而客户端连接池还“以为”这条连接是好的,第一次使用就会报Communications link failure。HikariCP 的 maxLifetime 默认 30 分钟,是低于 8 小时的,所以理论上能自动淘汰坏连接。但如果连接确实被服务端断开,你还是可能遇到偶尔一次查询报错的情况。

解决办法也不是没有,在配置里加上一条连接测试:

spring: datasource: hikari: connection-test-query: SELECT 1

HikariCP 默认的验证方式其实已经很智能,不一定需要写死 connection-test-query,但显式写出来有一个好处:能让你确信连接在复用之前被校验过。不过要注意,HikariCP 官方不建议显式配置这个方法,除非默认机制无法正常工作。这里我只是作为“万一遇到”的补充。

4.3 字符集、时区、SSL 这些容易忽略的配置项

字符集问题在本地连接中文数据时特别常见。如果你插入中文后,数据库里看到的是问号或乱码,首先考虑三处是否一致:

  • 表结构 charset 必须是 utf8mb4。
  • JDBC URL 中带不带useUnicode=true&characterEncoding=utf8。
  • MySQL 服务端默认字符集。

查询当前数据库编码:

SHOW VARIABLES LIKE 'character_set%';

如果某些变量还是 latin1,建议修改 MySQL 配置文件,在[mysqld]下设置character-set-server=utf8mb4,然后重启服务。本地开发再新建库时,编码就会正确。

时区这一块更隐蔽。你往create_time字段插入2024-06-01 12:00:00,Java 端读出来却差了几个小时,或者反过来,多半是时区设置不一致导致的。MySQL 连接里的serverTimezone=Asia/Shanghai要和你本地系统时区保持一致,同时 JVM 时区也已经通过spring.jackson.time-zone或者操作系统时区设置达成一致。最简单的验证方式是在 MySQL 命令行执行SELECT NOW(),然后在 Java 端打印LocalDateTime.now(),两次结果相差小时数,就说明有时区偏差。

SSL 这个配置,我直接说结论:本地开发关闭,用useSSL=false。如果因为某些原因必须开启 SSL,你需要生成自签名证书,并在 JDBC URL 里通过sslMode=VERIFY_CA指定信任库。这个复杂度对本地开发毫无必要。

5. 常见问题与排查技巧实录

5.1 一启动就报 Connection refused 或 Communications link failure

这类报错可以排到新手问题第一位。看到完整异常里有Exception: Communications link failure或嵌套的Caused by: java.net.ConnectException: Connection refused,排查顺序按下面来。

第一步,确认 MySQL 服务已经启动。Windows 下可以打开“服务”,查看 MySQL 服务状态;Mac 下用brew services list(如果通过 brew 安装);Linux 用systemctl status mysql。这一步通常能解决 40% 的问题。

第二步,确认连接地址和端口。本地默认端口 3306,如果 MySQL 启动时改了端口,比如 3307,那 JDBC URL 里的localhost:3306自然连不上。用命令行mysql -h 127.0.0.1 -P 3306 -u root -p测试一下,如果命令行能连上而 Spring Boot 连不上,问题大概率在 URL 后面的参数。

第三步,查看 MySQL 端口监听情况。Windows 执行netstat -ano | findstr 3306,Linux 执行ss -lntp | grep 3306,确认是 0.0.0.0 还是 127.0.0.1。有些 MySQL 配置只绑定了 127.0.0.1,如果你用另一个 IP 连接,也会失败。

还有一种隐蔽情况:本地装的是 MariaDB 而不是 MySQL。MariaDB 10.x 很多语法和 MySQL 兼容,但驱动和版本策略不完全相同,连接报错信息也很容易误导人。排查时确认一下SELECT VERSION()结果。

5.2 Access denied for user 'root'@'localhost'

提示Access denied,基本就是用户名、密码或者主机白名单问题。先检查 application.yml 里的 username 和 password 是否正确,用命令行登录验证。命令行能登录但 Spring Boot 报错,可能是密码里有特殊字符,比如@、#,在 yml 文件里没有加引号导致解析错误。解决方法很简单:给密码加上单引号,或者用环境变量注入:

spring: datasource: password: ${DB_PASSWORD:123456}

另一个原因是 MySQL 8 的认证插件和驱动版本不匹配。如果你用的驱动版本太老,而 MySQL 8 用户配置的是caching_sha2_password,可能报Unable to load authentication plugin 'caching_sha2_password'。要么升级驱动到 mysql-connector-j 8.x,要么把用户改成mysql_native_password认证方式:

ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '123456'; FLUSH PRIVILEGES;

我不建议为了兼容去改 root 的认证方式,本地临时排查时可以,但最好养成使用独立账号的习惯,给项目单独建一个低权限账号,避免把密码泄露在代码里造成安全问题。

5.3 Public Key Retrieval is not allowed

这个报错单独拿出来讲,是因为它几乎只出现在 MySQL 8.0 的本地连接场景。原因我在配置里提到过:caching_sha2_password 在首次连接时会要求客户端获取服务端的 RSA 公钥,而驱动为了安全默认禁止自动获取公钥。

解决办法有两个方向。

最快的是在 JDBC URL 上加上allowPublicKeyRetrieval=true,本地开发直接加这个参数,亲测有效。注意这个参数在公网环境会有中间人攻击风险,线上不建议开启。

另一种更安全的做法是在 MySQL 客户端里使用 SSL 连接,这样不需要下载公钥也能认证,但要给 URL 配置 SSL 参数,复杂度上来了。本地调试阶段,直接加参数最省心。

5.4 中文乱码问题

中文乱码有两个表现,输入乱码和输出乱码,排查思路完全不同。

如果插入数据库后看到的是???,问题出在数据库或表字符集。执行查询:

SHOW CREATE TABLE user;

看DEFAULT CHARSET是不是 utf8mb4,如果不是,就需要改表:

ALTER TABLE user CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

还要检查 JDBC URL 里的characterEncoding。这串参数看起来不起眼,但少了它,驱动可能使用服务端默认编码解析你传入的 SQL 和参数。

如果数据库里存的是正常中文,但接口返回给前端是乱码或问号,问题多半在 HTTP 层。Spring Boot 默认的server.servlet.encoding已经配置了 UTF-8,一般不会出问题。你可以检查 Response 的 Content-Type 是否为application/json;charset=UTF-8。如果自行设置了 StringHttpMessageConverter,注意它的默认编码并不一定是 UTF-8,需要手动指定。

另外,传参阶段也可能出问题。比如用 GET 请求带中文参数,Tomcat 默认对 URL 的编码解析也可能产生乱码,需要在配置里加上server.tomcat.uri-encoding=UTF-8,老版本 Spring Boot 要注意这个。

5.5 表不存在或者 JPA 自动建表失败

加了ddl-auto: update后,Hibernate 应该会帮我们自动建表,但如果实体类和数据库对不上,启动日志里可能会有 WARN,或者首次查询时报Table 'test_db.user' doesn't exist。

出现这种情况,先确认你连接的是不是test_db这个库,URL 中test_db后面是不是漏了?导致参数被拼进数据库名。其次确认实体类是否扫描到了。我的经验是实体类包和启动类不在同一个包下,启动时注解扫描不到,@Entity没有生效。

如果不想用 JPA 自动管理表,干脆手动建表,再把ddl-auto设为none,这样结构完全由 SQL 脚本控制,不会出现 Hibernate 擅自修改表结构的情况。本地做练习时,我比较推荐先手动建表,再用 JPA 的 validate 模式来校验实体映射:

spring: jpa: hibernate: ddl-auto: validate

validate不会改表,如果字段对不上启动会报错,更适合当作一种“校验器”。

5.6 连接池获取连接超时

如果日志提示:

HikariPool-1 - Connection is not available, request timed out after 30000ms

可能的场景很多。最典型的是最大连接数太小,而某个慢 SQL 长时间占着连接不放。本地开发遇到这个问题的原因往往是代码里忘记释放连接,或者出现了死循环,把连接池打满了。

排查思路是:先看数据库里当前连接数:

SHOW PROCESSLIST;

可以看到正在执行的 SQL 和 Sleep 连接数。如果某条 SQL 一直处于Sending data或Waiting for table metadata lock,说明它把连接占死了。kill 掉这个进程后再看应用是否恢复。长期解决办法是给慢 SQL 加索引、加超时时间、在代码里控制事务粒度。

本地开发如果只是想快速恢复,可以在配置里临时把 maximum-pool-size 调大,但这只是缓解,不是根治。

5.7 事务不生效的常见原因

Spring Boot 项目里操作 MySQL 时,事务也是“操作”的一部分。@Transactional不生效的情况在面试里经常被问,实际开发里也很常见。

常见的坑有三个。

第一,方法没有被 Spring 容器管理。如果@Transactional加在 private 方法上,或者同类内部调用时,事务切面不知道你调了它,事务自然不生效。正确做法是让事务注解加在 public 方法上,并且通过 Controller 调 Service 的代理方法。

第二,MySQL 表使用了 MyISAM 引擎。MyISAM 不支持事务,即使代码里加了@Transactional,执行到一半出错也不会回滚。本地建表时默认 InnoDB,但如果你复制过别人的建表语句被指定成了 MyISAM,就会踩这个坑。用SHOW TABLE STATUS LIKE 'user';可以看到引擎类型。

第三,事务传播行为设置成了REQUIRES_NEW或NEVER,字面意思和实际效果容易搞混。默认的REQUIRED已经能覆盖绝大多数业务场景,本地练习先不要动它。

6. 写在最后的实操建议

我个人在实际操作中的体会是,Spring Boot 连接 MySQL 最大的门槛不是代码,而是对“连接链路”的理解。你写的每一行配置,最终都会变成 Java 代码和 MySQL 服务端之间一次真实的握手。所以当你遇到看不懂的异常时,不要急着在框架配置里兜圈子,先用命令行把 MySQL 侧连接一遍,再逐层排除。

这里还有几个小技巧分享给你。

第一,把数据源配置里的密码用环境变量代替,避免密码写死在代码仓库里。本地开发可以设置默认值,线上一定要通过外部配置传入。

第二,养成看启动日志的习惯。Spring Boot 启动时,会自动打印“Tomcat started on port 8080”和 HikariCP 的初始化信息,比如:

HikariPool-1 - Starting... HikariPool-1 - Start completed.

看到 Start completed 一般说明连接池已经和 MySQL 建好了连接,后面如果查询还报错,问题大概率在 SQL 或 ORM 映射层面。

第三,给项目加上一个简单的健康检查接口。你可以引入spring-boot-starter-actuator,访问/actuator/health能看到数据库连接是否正常。这个依赖在本地调试阶段非常有用,不用每次通过业务接口验证。

后续你也可以在这个练习基础上继续扩展。比如把数据库操作封装成 Service 层,加入参数校验和全局异常处理;或者引入 Flyway、Liquibase 做数据库迁移脚本;再或者把连接池参数抽到配置中心,方便多环境切换。本地连接 MySQL 只是起点,但也是所有高级特性的地基。地基打牢了,后面写缓存、读写分离、分库分表时才不会心虚。

我最后再分享一个排查工具箱:MySQL 命令行、SHOW PROCESSLIST、HikariCP 日志、Spring Boot 的启动日志、加上一个简单的定时任务统计连接池状态。这五个工具足够处理本地开发中 95% 的数据库连接问题。剩下的 5%,大概率是驱动 bug 或者版本不兼容,升级/降级版本就能解决。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询