ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

SpringBoot 连接 MySQL 从配置到实战:JDBC、连接池与常见坑全解析

SpringBoot 连接 MySQL 从配置到实战:JDBC、连接池与常见坑全解析 1. 动手之前SpringBoot连MySQL到底在连什么很多刚接触SpringBoot的朋友第一件事就是把数据库连起来但大多数人卡住的地方根本不是SpringBoot本身而是搞不清楚一套连接到底拆成了哪几层。我在本地折腾过不少次SpringBoot项目对接MySQL数据库的坑先说一个最关键的认知SpringBoot自己不会连数据库它只是把底下的JDBC驱动、连接池、ORM框架和MySQL服务端串起来的一个调度中心。一次完整的本地连接包含四层协作MySQL服务端真正存数据的地方默认监听在localhost:3306你要保证它已经启动、账号密码正确、库和表都存在。JDBC驱动Java和MySQL之间的翻译官。SpringBoot 2.x时代常用mysql-connector-java3.x版本改名为mysql-connector-j名字和坐标都变了很多人升级之后栽在这里。连接池SpringBoot默认集成HikariCP。它负责维护一批到MySQL的长连接你写的每个SQL请求都从池里借连接用完归还不用频繁建连。ORM框架这一层可以选Spring Data JPA、MyBatis或JdbcTemplate。ORM帮你把Java对象映射成表记录也帮你把SQL结果翻译回Java对象。本文用Spring Data JPA演示因为它在本地开发场景下最省事实体类写好后基本不用手写SQL。所以当你点下启动按钮之后看到一堆报错别急着认为是SpringBoot坏了你得按上面这条链路一层一层排查先确认MySQL可不可连再看驱动有没有被正确加载接着看连接参数是否合理最后才轮到ORM配置。本文面向的是本地开发场景适合正在学SpringBoot的初学者、准备自己搭个Demo项目的学生还有那些公司项目跑在远程、想先在本地还原一套环境的开发同学。读完之后你能独立完成从MySQL安装建库、SpringBoot工程初始化、数据源配置到增删改查的全流程并且知道每个配置项到底是干嘛的而不是照抄网上的配置。2. 环境准备从MySQL安装到建库建表2.1 装一个能用本机连接的MySQL本地开发用的MySQL安装方式取决于你的操作系统。Windows下最简单的是去官网下载MySQL Installer装完选Developer Default一路Next中间会让你设root密码我建议本地开发就设成一个简单密码比如root或123456别把生产环境的密码策略搬过来否则你写配置文件时老是被特殊字符和转义折腾。macOS上推荐用Homebrewbrew install mysql brew services start mysql mysql_secure_installationLinuxUbuntu/Debian系sudo apt update sudo apt install mysql-server sudo systemctl start mysql sudo systemctl enable mysql装完之后先别急着碰SpringBoot先用命令行验证MySQL本身是好的mysql -u root -p输入密码后能出现mysql提示符说明服务端没问题。这里多说一句最近很多人下载的是MySQL 8.0.x甚至8.4MySQL 8默认认证插件是caching_sha2_password而SpringBoot这边如果驱动版本太老会报Public Key Retrieval is not allowed这个坑我放到后面专门讲现在你只需要知道装8.0也完全没问题关键是你的连接驱动要跟上。2.2 建库建表字符集和排序规则别偷懒进入mysql命令行之后执行以下SQL创建一个专门的开发库CREATE DATABASE IF NOT EXISTS springboot_demo DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE springboot_demo; CREATE TABLE user ( id BIGINT NOT NULL AUTO_INCREMENT, name VARCHAR(64) NOT NULL COMMENT 用户名, email VARCHAR(128) DEFAULT NULL COMMENT 邮箱, age INT DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;字符集选utf8mb4而不是utf8是个很实际的教训。utf8在MySQL里最多存3个字节遇到emoji或者生僻字直接报错或在库里变成乱码utf8mb4才是完整的Unicode实现。排序规则utf8mb4_general_ci对本地开发完全够用不需要纠结和unicode_ci的性能差异。表设计的几个常识也顺手说一下主键用BIGINT AUTO_INCREMENT业务表基本都这个套路create_time用DEFAULT CURRENT_TIMESTAMP省得每次插入都要手填所有字段能加NOT NULL就加Null值在统计和索引上都会埋坑。建完表后可以用SHOW CREATE TABLE user\G确认表结构也可以INSERT一条测试数据INSERT INTO user (name, email, age) VALUES (张三, zhangsanexample.com, 28);这里有个小建议千万别在生产或共享环境用root连但本地开发用root能省掉不少权限报错。如果你坚持建专用账号参考下面这条CREATE USER devlocalhost IDENTIFIED BY dev123456; GRANT ALL PRIVILEGES ON springboot_demo.* TO devlocalhost; FLUSH PRIVILEGES;2.3 初始化SpringBoot工程创建工程的姿势就两种网页版 Spring Initializr 或者IDEA自带的Spring Initializr。需要勾选的依赖Spring Web提供Controller能力等下交接口就靠它Spring Data JPAORM层MySQL DriverJDBC驱动如果你是Spring Boot 3.x配置文件里驱动坐标长这样dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependencySpring Boot 2.x则是dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency很多人在用IDEA新建项目时发现选依赖列表里写的是com.mysql:mysql-connector-j而网上的老教程写的是mysql:mysql-connector-java这俩其实是同一个东西换了个坐标Spring Boot的依赖管理会自动匹配合适版本不需要你手动写version。工程建好之后默认目录结构里有两个关键入口主类带SpringBootApplication的类和src/main/resources/application.properties。我习惯把配置文件改成application.yml可读性好很多后面所有数据库配置都写在这个文件里。3. 数据源配置里每一行到底管什么3.1 一份能跑通的application.yml先把完整的配置贴出来再逐行拆解spring: datasource: url: jdbc:mysql://localhost:3306/springboot_demo?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8allowPublicKeyRetrievaltrue username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: update show-sql: true properties: hibernate: format_sql: true这段配置是本地开发最稳的一套但你要明白每个参数为什么这么写以后才能根据报错灵活调整。3.2 URL里的每个参数都是一个坑jdbc:mysql://localhost:3306/springboot_demo是基础结构协议是jdbc:mysql地址是localhost:3306数据库名是springboot_demo。后面跟的问号参数依次解释useSSLfalseMySQL 8.0和Connector/J 8.x组合下驱动默认会尝试建立SSL连接而本地MySQL默认没配置SSL证书于是会出现一串以SSL connection error开头的报错。本地开发直接关掉SSL最省心生产环境再开启SSL加密。serverTimezoneAsia/Shanghai不指定时区的话连MySQL 8时大概率报The server time zone value Öйú±ê׼ʱ¼ä is unrecognized这串乱码是中国标准时间用错误编码显示的结果。写死Asia/Shanghai能彻底解决比写CTT这种缩写清晰。characterEncodingutf8强制客户端和服务器用UTF-8传输配合数据库的utf8mb4中文和特殊字符都不会乱码。allowPublicKeyRetrievaltrueMySQL 8的caching_sha2_password插件在非SSL连接下第一次认证需要拿服务器的公钥驱动默认不允许自动获取于是报Public Key Retrieval is not allowed。本地开发加这个参数就能绕过去。driver-class-name在Spring Boot 2.0以上必须是com.mysql.cj.jdbc.Driver老的com.mysql.jdbc.Driver虽然还兼容但已被标记为过时。如果你用的是MySQL 5.7.xURL参数基本一样但serverTimezone和useSSL的报错没8.0那么敏感。3.3 JPA配置里的ddl-auto到底选哪个spring.jpa.hibernate.ddl-auto有四个可选值很多人不查就照抄我明确告诉你none什么都不做表必须自己建validate启动时校验实体类和表结构是否匹配不匹配就报错update启动时自动建表表结构有变化时自动加字段create每次启动先删表再建表数据直接清空create-drop启动建表关闭后删表适合测试本地开发用update最爽实体类改一个字段重启后表就同步了。但你心里要有数update只负责增加不负责删除和改字段类型生产环境千万别用否则表结构会不可控。show-sql: true会在控制台打印每条SQL开发期建议开方便你确认Hibernate生成的SQL是否符合预期。format_sql: true把SQL格式化得好看一点不嫌日志长就开着。3.4 连接池参数要不要配SpringBoot默认的HikariCP非常好用本地开发几乎不用调。但有两个参数值得知道spring: datasource: hikari: maximum-pool-size: 10 connection-timeout: 30000maximum-pool-size是连接池里最多同时有多少个连接本地开发10个绰绰有余connection-timeout是拿连接的超时时间默认30秒其实已经够长如果你想快速暴露连接问题可以改成5秒。我见过有人把maximum-pool-size配成500结果MySQL端max_connections默认151直接把数据库挤爆这种问题排查起来相当隐蔽。配置写完后直接启动SpringBoot应用看到控制台输出HikariPool-1 - Start completed并且没有Connection refused或Access denied之类的异常说明数据源已经打通。如果此时报错别慌先看你自己能不能用mysql -u root -p连上能连上就说明问题出在配置文件不能连上就要回去查MySQL服务。4. 打通增删改查实体类、Repository、Service与Controller4.1 写实体类让Java对象和表字段对上在项目中新建com.example.demo.entity包创建User实体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 64) private String name; Column(name email, length 128) private String email; Column(name age) private Integer age; Column(name create_time, updatable false) private LocalDateTime createTime; // 无参构造、getter/setter建议用Lombok的Data简化 }几个点说明一下Entity告诉Hibernate这个类对应一张表Table(name user)指定表名。user在MySQL里不算保留字但为了安全还是建议加反引号或干脆把表命名为t_user我当时用user没出问题但有些版本的Hibernate会生成带反引号的SQL反而更稳。GeneratedValue(strategy GenerationType.IDENTITY)对应表的自增主键如果你的主键是别的策略这里要相应改成SEQUENCE或AUTO。updatable false确保create_time只在插入时自动填更新时不会动它。4.2 Repository接口Spring Data JPA的秘密武器新建com.example.demo.repository包创建接口package com.example.demo.repository; import com.example.demo.entity.User; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import java.util.List; public interface UserRepository extends JpaRepositoryUser, Long { ListUser findByName(String name); ListUser findByAgeGreaterThan(Integer age); Query(select u from User u where u.email like %:keyword%) ListUser searchByEmail(Param(keyword) String keyword); }JpaRepositoryUser, Long自带了一整套方法findAll()、findById()、save()、deleteById()等等零实现就能跑。findByName这种命名方法也不需要SQLSpring Data JPA会根据方法名自动解析成WHERE name ?。这背后的魔法叫方法名派生查询规则很直观比如findByAgeGreaterThan就是WHERE age ?你要记住的常用组合无非是And、Or、Between、LessThan、GreaterThan、Like、OrderBy。如果命名方法表达不了的复杂查询用Query写JPQL。我这里写的like %:keyword%就是按邮箱模糊搜索注意JPQL操作的是实体类属性名而不是数据库字段名。4.3 Service层事务边界放在这里package com.example.demo.service; import com.example.demo.entity.User; import com.example.demo.repository.UserRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; Service public class UserService { private final UserRepository userRepository; public UserService(UserRepository userRepository) { this.userRepository userRepository; } Transactional public User createUser(User user) { return userRepository.save(user); } Transactional public User updateUser(Long id, User newData) { User user userRepository.findById(id) .orElseThrow(() - new RuntimeException(用户不存在)); user.setName(newData.getName()); user.setEmail(newData.getEmail()); user.setAge(newData.getAge()); return userRepository.save(user); } Transactional public void deleteUser(Long id) { userRepository.deleteById(id); } public ListUser listUsers() { return userRepository.findAll(); } }Transactional必须加在Service层而不是Repository层。原因很实际一次业务操作往往涉及多个SQL比如转账是扣款入账两条更新如果中间一条失败了事务保证两条一起回滚不至于出现钱扣了但没入账的情况。Spring默认只有运行时异常RuntimeException及其子类触发回滚受检异常不会这个细节在写业务代码时要注意。构造器注入是Spring官方推荐的方式private final配合构造器比Autowired字段注入更安全不容易出现循环依赖和空指针。本地Demo规模不必过度设计但这种习惯从第一天养成比较好。4.4 Controller层把能力暴露成接口package com.example.demo.controller; import com.example.demo.entity.User; import com.example.demo.service.UserService; import org.springframework.web.bind.annotation.*; import java.util.List; RestController RequestMapping(/api/users) public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService userService; } PostMapping public User create(RequestBody User user) { return userService.createUser(user); } PutMapping(/{id}) public User update(PathVariable Long id, RequestBody User user) { return userService.updateUser(id, user); } DeleteMapping(/{id}) public void delete(PathVariable Long id) { userService.deleteUser(id); } GetMapping public ListUser list() { return userService.listUsers(); } }写完之后启动应用分别验证# 新增 curl -X POST http://localhost:8080/api/users \ -H Content-Type: application/json \ -d {name:李四,email:lisiexample.com,age:30} # 查询 curl http://localhost:8080/api/users # 修改 curl -X PUT http://localhost:8080/api/users/1 \ -H Content-Type: application/json \ -d {name:张三丰,email:zhangsanexample.com,age:28} # 删除 curl -X DELETE http://localhost:8080/api/users/1你也可以用Postman、Apifox这类工具。重点不是工具的选型而是你要明白从HTTP请求到数据库的完整流转请求打到ControllerController调ServiceService调RepositoryRepository通过JPA自动生成的SQL访问MySQL。任何一环断了都能从前到后顺着日志定位。4.5 分页查询本地简单但值得一会的操作数据多起来后你迟早需要分页。Spring Data JPA分页几乎是白送的import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; public PageUser listByPage(int page, int size) { return userRepository.findAll( PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, createTime)) ); }PageRequest.of(page, size)从第0页开始编号Sort.by(...)按创建时间倒序。这个写法在本地看不出威力但等你接手真实项目分页是最常见的数据库操作之一先记下这套结构没有坏处。5. 本地连库最容易翻车的几个坑5.1 SSL连接错误八成是useSSL没配报错长这样Communications link failure The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server. java.sql.SQLException: SSL connection error: protocol error这是MySQL 8.0和Connector/J 8.x组合下最常见的本地报错。MySQL服务端默认开了SSL支持但本地装的往往没有有效证书驱动尝试SSL握手时失败了。解决办法就是URL里加useSSLfalse。如果你坚持要加密连接那得去给MySQL配证书本地开发完全没必要。5.2 时区乱码serverTimezone一招解决报错The server time zone value Öйú±ê׼ʱ¼ä is unrecognized or represents more than one time zone.这段乱码的根源是MySQL返回的语言环境时区名称和Java侧编码不一致。改成URL里加serverTimezoneAsia/Shanghai问题立即消失。有些老教程会教你改MySQL全局时区变量SET GLOBAL time_zone 8:00;但这不是治本因为你换了开发环境又得改一遍写死在JDBC URL里最直观。5.3 Public Key Retrieval is not allowed报错文本很直白Caused by: java.sql.SQLException: Public Key Retrieval is not allowed触发条件MySQL 8的默认认证插件是caching_sha2_password第一次连接时需要从服务端获取RSA公钥来加密密码而驱动默认不允许从服务端自动获取公钥。URL加上allowPublicKeyRetrievaltrue就解决了。有安全洁癖的人可能觉得这样有隐患但本地开发环境你直接关SSL、允许拿公钥实际风险可以忽略。5.4 Access denied账号密码之外还有一个坑最常见的报错Access denied for user rootlocalhost (using password: YES)先检查密码是不是真错了。如果密码是对的但用的是老客户端或者老驱动连接MySQL 8就可能因为caching_sha2_password插件导致老驱动不认解决办法是把这个用户的认证插件改回mysql_native_passwordALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码; FLUSH PRIVILEGES;不过我的建议是优先升级驱动版本而不是降级认证插件。因为你迟早要上生产生产环境如果是新装的MySQL 8它默认就是caching_sha2_password你不可能为了本地方便改生产的认证。5.5 端口被占第二份MySQL或旧进程在捣乱如果你本地之前装过MySQL 5.7后来又装了MySQL 8.0或者用Docker跑了一个MySQL容器那么3306端口很容易被其中一份占用。此时SpringBoot这边报的通常是Connection refused但你自己用命令行却连得上因为命令行连的是另一份实例。排查方法# Windows netstat -ano | findstr :3306 # macOS / Linux lsof -i :3306如果发现3306被占用要么停掉旧进程要么在SpringBoot配置里把端口改成新实例实际监听的端口。定位这类问题关键是记住一个原则先在MySQL侧复现再到SpringBoot侧排查。5.6 驱动和Spring Boot版本不匹配Spring Boot 3.x把javax.persistence改成了jakarta.persistence如果你从网上抄的2.X教程把javax.persistence.*的import直接粘到3.x项目里编译肯定挂。实体类的包里要写jakarta.persistence.Entity、jakarta.persistence.Id。驱动那边从mysql-connector-java改成mysql-connector-j两个坐标我都写在前面了。快速判断自己项目是哪一代Spring Boot看pom.xml里parent标签的版本号是2.x.x还是3.x.x然后对应使用正确的import坐标。5.7 本地连库踩坑速查表报错关键字常见原因快速解决SSL connection error驱动默认尝试SSL本地无证书URL加useSSLfalsetime zone value ... unrecognized时区未指定URL加serverTimezoneAsia/ShanghaiPublic Key Retrieval is not allowedMySQL 8认证插件需要公钥URL加allowPublicKeyRetrievaltrueAccess denied for user密码错或认证插件不兼容检查密码必要时改认证插件Connection refusedMySQL没启动或端口不对检查服务状态和netstat/lsofTable xxx doesnt exist表没建或数据库名配错核对URL里的库名和SHOW TABLES结果Cannot load driver class驱动坐标没引入按Spring Boot版本补依赖5.8 一个更隐蔽的坑HikariCP连接池初始化超时如果你把connection-timeout调得很短MySQL又刚好慢启动时会看到HikariPool-1 - Exception during pool initialization这其实是连接池拿不到连接导致的。处理思路不是调大超时而是先搞明白MySQL为什么慢是不是skip-name-resolve没开导致每连接都做DNS反解是不是max_connections太小本地开发如果MySQL和SpringBoot都跑在同一台机器直接连localhost而不要用127.0.0.1的差异虽然不大但某些系统上localhost会走IPv6反而引发连接慢的问题实测下来用127.0.0.1更稳。我在自己项目里就遇到过这个换成127.0.0.1之后连接耗时降了一个数量级。6. 本地开发的数据管理习惯与调试心得6.1 表结构变更别只靠ddl-autoupdate前边说update开发期好用但它的副作用是表结构和实体类的一致性没有版本控制。今天我加一个字段明天你把字段改了表里可能残留着老字段时间一长库和代码就对不上了。我现在的习惯是本地开发可以开着update但每改一次结构就把变更SQL单独存一份在项目的src/main/resources/db目录下。将来上生产或者同步给同事时用这些SQL文件手动执行比你用ORM自动改可靠得多。6.2 看SQL日志调试数据库操作的捷径配置里开了show-sql: true之后每次查询都会打印Hibernate生成的SQL。我排查问题第一步永远是看这个SQL是否符合预期如果是update操作但没where条件数据可能被全表更新如果是关联查询但join条件不对结果集肯定有问题。本地开发就这一招能让你少走很多弯路。另外Hibernate的SQL日志默认参数是?占位符不能直接拿来执行。想看真实参数值在日志配置里加上logging.level.org.hibernate.type.descriptor.sql.BasicBinderTRACE这条配置会把绑定参数的值打出来调试时非常好用排查为什么查不到数据或者为什么删错了行时直接看参数就明白。6.3 用可视化工具把数据看明白命令行操作MySQL固然可靠但本地开发我建议配一个可视化客户端DBeaver、Navicat、DataGrip都行。它们能做的不仅是看数据还包括直接执行SQL验证SpringBoot写的SQL是否正确看表结构变化确认ddl-autoupdate到底动了什么对比多个数据库实例排查端口和实例混淆问题有的人嫌装客户端麻烦用IDEA自带的Database面板也完全可以反正目的只有一个把数据库里的真实状态看清楚。我在本地排查接口返回空列表时经常是先开客户端看一眼表里到底有没有数据有数据再去查代码没数据就先去造数据这一个习惯能省掉大量瞎猜的时间。6.4 从本地连库走向实操的几点体会我在实际使用中最受益的一条调试原则是把问题边界划清楚永远先确认MySQL本身能连。不管是Access denied、Connection refused还是SSL报错先打开命令行连一次排除服务端问题再回头看SpringBoot的配置和代码。这条原则听着简单但我见过太多同事在SpringBoot里反复改驱动版本、加依赖最后发现是MySQL服务压根没启动。另外一个小技巧如果启动时遇到莫名其妙的连接问题可以在application.yml临时把日志级别调成DEBUGlogging: level: com.zaxxer.hikari: DEBUGHikariCP的DEBUG日志会把每次获取连接、归还连接和失败原因都打出来比看异常栈直观得多。排查完记得调回INFO否则日志量太爆炸。我还会定期用SHOW PROCESSLIST;看一眼当前有哪些连接连到了MySQL如果发现大量Sleep状态的连接堆着说明连接池的maximum-pool-size可能配太大了或者代码里有连接泄漏。本地开发规模小正常也就几个连接一眼就能看出异常。最后把自己踩过的坑总结成一句口诀先MySQL、后驱动、再URL、最后看SQL日志。按这个顺序排查本地连接问题九成能在一分钟内定位。剩下的就是耐心读报错的第一行别只看异常栈的末尾Caused by才算真正的原因。
返回列表