Spring Boot 项目里连 MySQL,这事看起来简单,但我在群里看到太多人卡在起跑线上:依赖加了、配置写了,一启动就报错,或者好不容易起来了,查询接口又给你来一个 404,再不就是分页查不出来。究其原因,大多是三件套之间的版本匹配和配置细节没对上。
所以我就想用一篇文章,把"Spring Boot + MyBatis-Plus 连接 MySQL"这条链路从头到尾捋一遍。不绕弯子,直接给你能跑通的方案,顺带把为什么这么配、背后是什么原理讲清楚。无论你是刚入门想搭第一个项目,还是工作中需要快速初始化一个带数据库的服务,这篇文章都能让你少踩几个坑。
1. 整体方案与选型思路
1.1 为什么是这三件套
先说结论:这个组合几乎就是当前 Java 后端业务系统开发最主流的起步配置,没有之一。
Spring Boot 负责把整个应用的骨架搭起来,它最大的价值是自动装配。你只需要在 pom 里声明依赖,框架会自动帮你把数据源、事务管理器、Web 容器这些基础设施初始化好,省掉了大量 XML 配置。Spring 官方也一直在往"约定优于配置"的方向走,所以用它做底座,团队协作时心智负担最小。
MyBatis-Plus 是在 MyBatis 之上做增强的持久层框架。它保留了 MyBatis 的灵活 SQL 能力,同时内置了通用 Mapper、通用 Service、分页插件这些高频能力。实际开发中你不需要手写 INSERT、UPDATE、SELECT BY ID 这类基础 SQL,框架会根据实体类自动生成。这一点对快速开发来说太关键了——你想想,一个模块几十张表,每张表都手写一遍增删改查,那是在浪费生命。
MySQL 这个就不用多说了,开源免费、生态成熟、资料多,中小规模项目的首选关系型数据库。虽然现在各种分布式数据库层出不穷,但 MySQL 作为业务主库的地位依然稳固,几乎所有人学习后端的第一站都是它。
1.2 技术选型背后的考量
如果只追求"能跑",方案不止这一个,Spring Data JPA 和原生 MyBatis 也都是选项。我这里强调一下为什么 MyBatis-Plus 在实战中更讨喜。
Spring Data JPA 的抽象层级高,简单 CRUD 确实香,但一旦业务查询复杂起来,你要不就得写 @Query 注解里那串 JPQL,要不就得忍受嵌套接口继承带来的调试痛苦。尤其国内很多团队的 SQL 功底比 Java 功底扎实,让他们用 Hibernate 那种自动生成 SQL 的思路工作,反而觉得不顺手。
而原生 MyBatis 又太"裸"了——每张表都要手写 Mapper 接口、Mapper XML、实体类,一个简单的列表接口零件一堆。MyBatis-Plus 刚好卡在中间:它把你的常规操作接管了,99% 的基础接口一行代码都不用写;那 1% 的复杂查询,你依然可以用 @Select 注解或 XML 手写 SQL。这是非常务实的设计哲学。
而且 MyBatis-Plus 的代码生成器也很成熟,可以从数据库表结构直接反推出实体类、Mapper、Service、Controller 全套代码。这意味着你在新项目里定好表结构之后,代码骨架几分钟就能拉出来。
1.3 这套方案适用的场景
选型还要看场景,不是所有项目都适合。
如果你的团队熟悉 SQL,追求快速交付业务功能,尤其是传统的信息管理系统、后台管理系统、中小型互联网应用,这个组合是最稳的。MyBatis-Plus 提供的分页、条件构造器、逻辑删除、乐观锁插件,几乎覆盖了 CRUD 系统的所有常规需求。
反过来,如果你的项目是纯 RESTful API 且查询逻辑极其复杂(比如报表聚合、多维度分析),或者需要大量走 Hibernate 的一级二级缓存特性,那可能要重新评估。另外,如果团队明确需要数据库无关性(比如同套代码支持多个数据库切换),基于 JPA 的方言抽象会更省心。不过绝大多数项目部署时数据库就定死了一个,所以这个问题不常见。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与版本匹配
2.1 JDK、Maven、MySQL 的安装检查
连接数据库之前,先确认本机环境是齐的。这三个工具缺一个,后面跑源码都是白搭。
JDK 建议直接用 8 或 11。Spring Boot 2.x 系列在 JDK 8 上最稳,Spring Boot 3.x 强制要求 JDK 17。如果你刚起步,我建议用 Spring Boot 2.7.x + JDK 8 的组合,网上资料最多,踩坑时一搜一大把。安装 JDK 后记得在命令行敲 java -version 验证一下,Path 环境变量没配好的话,IDE 里能跑但命令行跑不了,后面部署时会很痛苦。
Maven 的安装相对简单,下载二进制压缩包解压后配置 MAVEN_HOME 和 PATH 就行。需要注意的是,Maven 默认中央仓库在国外,首次下载依赖会很慢。建议在 settings.xml 里配置阿里云镜像,这个不是可选项,是省时间利器。配置方式很简单,在 <mirrors> 标签里加一段:
xml复制<mirror>
<id>aliyunmaven</id>
<mirrorOf>central</mirrorOf>
<name>阿里云公共仓库</name>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>
MySQL 的安装分两种情况。一种是本地直接装,Windows 用户去官网下载 MySQL Installer,选择 Server 组件安装即可,安装过程中会让你设置 root 密码,自己记好。另一种是直接用 Docker 拉镜像,我喜欢这种方式,因为干净、可重复、删了重来不心疼。命令行跑一下:
bash复制docker run -d \
--name mysql8 \
-p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=root123 \
-e MYSQL_DATABASE=testdb \
mysql:8.0
这样 MySQL 8.0 就在本机 3306 端口起来了,还顺手建了一个 testdb 数据库。注意 -p 3306:3306 是端口映射,容器内 MySQL 默认 3306,映射到宿主机 3306,这样 Spring Boot 在宿主机上连接时就用 localhost:3306。
2.2 版本对应关系与兼容性
这里一定要强调,Spring Boot 和 MySQL 驱动、MyBatis-Plus 的版本是有联动关系的。我整理了一张常用版本匹配表,照着选基本不会出问题:
| Spring Boot | JDK 版本 | MyBatis-Plus | MySQL 驱动 | 说明 |
|---|---|---|---|---|
| 2.3.x | 8 | 3.4.x | mysql-connector-java 8.0.x | 老项目常见 |
| 2.5.x | 8/11 | 3.5.x | 8.0.x | 稳定组合,推荐新手 |
| 2.7.x | 8/11 | 3.5.3+ | 8.0.33 | 组合最稳,资料最全 |
| 3.0.x | 17+ | 3.5.4+ | com.mysql:mysql-connector-j 8.0.33 | 注意 javax 改 jakarta |
| 3.1.x | 17+ | 3.5.5+ | com.mysql:mysql-connector-j | 最新生态 |
一个常见的坑是:MySQL 8.x 版本下,驱动类名要用 com.mysql.cj.jdbc.Driver,而 MySQL 5.x 用的是 com.mysql.jdbc.Driver。如果你复制了老项目的配置,怎么启动都报 ClassNotFoundException,大概率就是驱动类名不对。另一个坑是 Spring Boot 3.x 之后,驱动坐标从 mysql:mysql-connector-java 改成了 com.mysql:mysql-connector-j,依赖名称也变了。
2.3 准备数据库和用户
MySQL 装好之后,不要直接用 root 账号去连业务数据库,这是我从生产环境学到的教训。开发环境虽然随意,但统一规范能避免不少权限问题。我们先建一个专用用户,并赋予它某个库的所有权限:
sql复制CREATE DATABASE IF NOT EXISTS `demo` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
CREATE USER 'demo_user'@'%' IDENTIFIED BY 'demo_pass';
GRANT ALL PRIVILEGES ON `demo`.* TO 'demo_user'@'%';
FLUSH PRIVILEGES;
字符集用 utf8mb4 而不是 utf8,否则存不了 emoji 和部分生僻字。这是 MySQL 8.0 的默认推荐,但手工建库时很多人不写,老默认为 latin1,到时候插入中文直接变问号,查一天都查不出原因。顺手也确认一下时区,MySQL 8.0 默认情况下 serverTimezone 不一定匹配你的本机时区,后面 JDBC 连接时大概率会报时区错。最简单的办法是建库后执行 SET GLOBAL time_zone = '+08:00';,或者在 JDBC URL 里指定。
3. 快速初始化 Spring Boot 项目
3.1 通过 Spring Initializr 创建骨架
创建 Spring Boot 项目,我几乎不用 IDE 内置向导,都是直接访问 start.spring.io,因为网页上能直观看到依赖清单,生成完下载 zip 导入 IDE 即可,清爽又不会带入多余配置。
如果你更习惯命令行,也可以用 Spring Initializr 的 HTTP API,一条命令拉一个工程:
bash复制curl https://start.spring.io/starter.zip \
-d type=maven-project \
-d language=java \
-d bootVersion=2.7.18 \
-d groupId=com.example \
-d artifactId=demo \
-d name=demo \
-d packageName=com.example.demo \
-d javaVersion=8 \
-d dependencies=web,mysql,mybatis-plus \
-o demo.zip
解压后,你会得到一个标准的 Maven 工程,有 pom.xml、启动类、application.properties(或 yml)、测试类。这里注意,start.spring.io 并不直接支持 mybatis-plus 这种第三方依赖,上面的写法只是为了示意,实际 MyBatis-Plus 的依赖要自己加进 pom。所以简化起见,一般项目就选 Web 和 MySQL 两个官方依赖,MyBatis-Plus 手动加。
3.2 pom.xml 依赖详解
打开 pom.xml,核心依赖大概是这样的:
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>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.5</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
逐个说下作用。spring-boot-starter-web 提供了内嵌 Tomcat 和 Spring MVC,没有它,你的 Controller 是跑不起来的。mybatis-plus-boot-starter 是 MyBatis-Plus 的启动器,它会自动配置 SqlSessionFactory、Mapper 扫描等,比单独引 mybatis 再配 spring-mybatis 省事太多。mysql-connector-j 是 MySQL 驱动,没有它 Java 程序根本无法和 MySQL 通信。lombok 用来简化实体类的 getter/setter 和日志声明,属于开发期工具,所以 optional 标为 true。
如果你是 Spring Boot 3.x,把父版本换成 3.2.x,JDK 用 17,同时把 javax 命名空间的相关依赖换成 jakarta,这里不展开,但要知道有这回事。
3.3 配置启动类与 Mapper 扫描
启动类非常重要,我一并说了。默认生成的启动类长这样:
java复制@SpringBootApplication
@MapperScan("com.example.demo.mapper")
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
关键就是 @MapperScan。MyBatis-Plus 的 Mapper 接口需要被 Spring 容器扫描到才能注入使用,有两种方式:一种是在每个 Mapper 接口上标注 @Mapper 注解,另一种是在启动类上统一加 @MapperScan 指定包路径。我推荐后者,因为接口一多,每个都加注解太啰嗦,而且漏一个就启动失败。
扫描路径一定要写对,只扫描到 mapper 包这一层,别扫到 controller、service 包,否则 MyBatis 会把所有接口都当成 Mapper 去解析,报 Invalid bound statement 之类的错。
4. 配置文件编写:application.yml 的每一项都有讲究
4.1 数据源配置项逐行拆解
Spring Boot 的配置默认写在 src/main/resources/application.yml 里。连接 MySQL 和数据源相关的内容,最简版是这样:
yaml复制server:
port: 8080
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
username: demo_user
password: demo_pass
mybatis-plus:
mapper-locations: classpath:/mapper/**/*.xml
type-aliases-package: com.example.demo.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: assign_id
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
逐行说。
driver-class-name 是驱动类全路径,MySQL 8 用 com.mysql.cj.jdbc.Driver,这个前面说过。
URL 里的参数一个都不能乱删。useUnicode=true&characterEncoding=utf8 保证中文不乱码。serverTimezone=Asia/Shanghai 解决时区报错——你如果不加,有时会看到 The server time zone value '�й���ʱ��' is unrecognized,这就是因为 MySQL 服务端时区无法被 JDBC 识别。useSSL=false 是因为本地开发通常没有配置 SSL 证书,且 MySQL 8 默认不开 SSL 也没事,加上 false 避免握手警告。allowPublicKeyRetrieval=true 这个很隐蔽,MySQL 8 的 caching_sha2_password 认证插件下,如果连接时没有提前拿到服务器公钥,会报错 Public Key Retrieval is not allowed,加上这一项可以省掉很多麻烦。
mapper-locations 指向 XML 文件所在的目录,如果你的 SQL 全部用注解写在接口里,这项可以不写;但一旦涉及复杂查询要 XML,就必须配置,否则 MyBatis 找不到语句。
type-aliases-package 是实体类的包路径,配置后在使用 resultType 时可以直接写类名,不用全限定名。
map-underscore-to-camel-case 这个强烈建议打开。MySQL 的字段命名习惯是下划线风格(如 user_name),Java 属性是驼峰风格(如 userName),打开这个配置后,MyBatis-Plus 和 MyBatis 会自动做映射,不用每个字段都写 @TableField("user_name")。
log-impl 设成 StdOutImpl,会在控制台打印 SQL 语句。开发阶段一定要开着,你能直观看到 MyBatis-Plus 替你执行了什么语句,排查问题效率翻倍。生产环境记得关掉。
4.2 MyBatis-Plus 全局策略配置说明
global-config.db-config 是 MyBatis-Plus 的全局配置。id-type: assign_id 表示实体主键默认使用雪花算法生成 19 位 Long 型 ID,这是分布式系统下推荐的主键策略,避免了数据库自增 ID 在分库分表场景下的冲突。如果你就是单库单表,也可以用 auto 让数据库自增。
logic-delete-field 这块是逻辑删除配置。业务系统里物理删除要慎用,很多时候数据只是标记删除而已。MyBatis-Plus 会在全局层面自动把 delete 操作转成 update 语句,把 deleted 字段置为 1。查询时自动追加 deleted = 0 条件。这是默认行为,不需要你手写任何 SQL,非常省心,但前提是实体类里得有一个 deleted 属性。
配置这些项的时候注意一个原则:能全局配置就不在实体上重复标注。比如逻辑删除字段名,全局配一次,所有实体都生效;你要是每个实体类单独 @TableLogic,漏一个就是一条生产事故。
4.3 连接池参数调优
Spring Boot 2.x 默认使用 HikariCP 作为连接池,这是目前 Java 生态性能最好的连接池,不需要换。配置如下:
yaml复制spring:
datasource:
hikari:
minimum-idle: 5
maximum-pool-size: 20
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
这几个参数怎么定?maximum-pool-size 不是越大越好。连接池大小和数据库最大连接数、并发量、单个查询耗时都有关系,一般经验值是 CPU 核心数 × 2 + 磁盘 IO 等待数。单机开发环境 10 到 20 足够,一个查询几十毫秒的话,20 个连接每秒能支撑数百请求。max-lifetime 要小于 MySQL 的 wait_timeout,MySQL 默认为 8 小时,如果你的连接被 MySQL 服务端主动断开,而连接池不知道,就回收到一个坏连接,下次请求就会报各种诡异的通信异常。HikariCP 默认单测下会自动检测并丢弃失效连接,但这个时长仍需配好。
5. 核心代码实践:从实体类到 Controller
5.1 实体类标注与注意事项
我拿一张最典型的用户表举例。数据库表结构简化版:
sql复制CREATE TABLE `user` (
`id` bigint NOT NULL,
`name` varchar(50) DEFAULT NULL,
`age` int DEFAULT NULL,
`email` varchar(100) DEFAULT NULL,
`deleted` tinyint NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
对应实体类:
java复制@Data
@TableName("user")
public class User {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String name;
private Integer age;
private String email;
@TableLogic
private Integer deleted;
}
@TableName("user") 当表名和实体类名不一致时必须写,否则 MyBatis-Plus 会默认把 User 映射到 user,这里碰巧一致,但建议都写上,清晰。@TableId(type = IdType.ASSIGN_ID) 表示主键策略是雪花 ID,和全局配置保持一致,如果你全局已经配了 assign_id,这里也可以不写,但写上更明确。
有个细节:属性 deleted 加上 @TableLogic 后,MyBatis-Plus 会自动处理,但全局配置里也配了 logic-delete-field: deleted,两者只需其一,重复写也不冲突。实际项目里我喜欢全局配置 + 实体里不写,保持实体干净。
5.2 Mapper 接口和 Service 层
实体类建好后,Mapper 接口极其简单:
java复制public interface UserMapper extends BaseMapper<User> {
}
没看错,就这一行。BaseMapper<T> 提供了 selectById、selectList、insert、updateById、deleteById、selectPage 等十几个现成的 CRUD 方法。你不需要定义任何方法,直接用注入的 UserMapper 调用就行。
但业务代码里我习惯再套一层 Service,而不是在 Controller 里直接用 Mapper。一是事务边界可以统一管理,二是未来要加业务逻辑不用改 Controller。MyBatis-Plus 也为 Service 层提供了一套通用模板:
java复制public interface UserService extends IService<User> {
}
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}
继承 IService 和 ServiceImpl 后,userService.save(user)、userService.list()、userService.getById(id) 这些方法就全有了,而且自带批量操作能力,比直接调 Mapper 方便很多。
5.3 Controller 里写一个完整的 CRUD
Controller 层我写一个标准的 REST 风格接口,演示怎么把 Service 层的能力用起来:
java复制@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping
public List<User> list() {
return userService.list();
}
@GetMapping("/{id}")
public User detail(@PathVariable Long id) {
return userService.getById(id);
}
@PostMapping
public User create(@RequestBody User user) {
userService.save(user);
return user;
}
@PutMapping("/{id}")
public User update(@PathVariable Long id, @RequestBody User user) {
user.setId(id);
userService.updateById(user);
return user;
}
@DeleteMapping("/{id}")
public void delete(@PathVariable Long id) {
userService.removeById(id);
}
}
接口测试不用多说,启动服务后访问 http://localhost:8080/users 就能看到 JSON 数组。这里有个习惯我强调一下:create 接口返回时,user 对象里的 id 如果用的雪花 ID,保存后 MyBatis-Plus 会自动回填到实体对象里,所以直接返回实体就能看到生成的主键。如果是数据库自增策略,同样会自动回填,原理是 MyBatis 的 useGeneratedKeys 机制。
5.4 条件构造器:复杂查询不用写 SQL
MyBatis-Plus 最大的亮点之一是 QueryWrapper 和 LambdaQueryWrapper。比如我要查年龄在 18 到 30 之间、名字包含"张"的用户,用传统 SQL 肯定要拼接条件,用 QueryWrapper 一行搞定:
java复制@GetMapping("/search")
public List<User> search(@RequestParam(required = false) String name,
@RequestParam(required = false) Integer minAge,
@RequestParam(required = false) Integer maxAge) {
LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery();
wrapper.like(StringUtils.hasText(name), User::getName, name)
.ge(minAge != null, User::getAge, minAge)
.le(maxAge != null, User::getAge, maxAge);
return userService.list(wrapper);
}
这里有两个细节值得说。第一,我优先用 LambdaQueryWrapper 而不是 QueryWrapper,因为前者用方法引用 User::getName 代替字符串 "name",编译期就能检查属性名正确性,重构时不至于炸。第二,like(boolean condition, column, value) 这种重载,条件为 false 时自动忽略该条件。实现动态查询时特别有用,不需要 if 判断层层嵌套,代码整洁多了。
如果你之后遇到 or、in、group by、甚至子查询,Wrapper 都支持,语法不复杂,多用几次就熟了。它生成的 SQL 会在控制台打出来,配合前面配置的 SQL 日志,调试起来非常直观。
6. 分页插件配置:新版 MyBatis-Plus 必须手动注册
6.1 为什么 MyBatis-Plus 3.5.x 需要手动注入插件
很多老教程里,MyBatis-Plus 的分页是自动生效的,因为老版本内置了分页插件。但从 3.4.0 开始,官方把分页插件抽离成了 MybatisPlusInterceptor + PaginationInnerInterceptor 的组合,并且不再默认加载,必须你手动在配置类里注册 Bean。
如果你只依赖了 mybatis-plus-boot-starter,没有注册分页插件,就调用 selectPage 方法,你会发现返回的 total 是 0,records 也有问题,因为 MyBatis-Plus 实际的物理分页并不会生效。这个坑几乎每个人都踩过,所以单独拿出来说。
6.2 注册分页拦截器的标准写法
用一种配置类来注册:
java复制@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
PaginationInnerInterceptor pagination = new PaginationInnerInterceptor(DbType.MYSQL);
pagination.setMaxLimit(500L);
interceptor.addInnerInterceptor(pagination);
return interceptor;
}
}
PaginationInnerInterceptor(DbType.MYSQL) 需要指定数据库类型,因为不同数据库的分页 SQL 语法不同。MySQL 是 LIMIT 语法,PostgreSQL 是 OFFSET 语法,SQL Server 是 TOP 语法,指定错了生成的 SQL 就是错的。setMaxLimit(500L) 设置最大每页条数,防止有人调接口时传个 size=1000000 把数据库打崩,这是一个安全兜底,生产环境很有必要。
注册完成后,Service 里要分页,直接写:
java复制@GetMapping("/page")
public IPage<User> page(@RequestParam(defaultValue = "1") long current,
@RequestParam(defaultValue = "10") long size) {
Page<User> page = new Page<>(current, size);
return userService.page(page);
}
返回值 IPage 里包含 records 列表、total、current、size 等分页元数据,前端拿这个直接渲染分页组件就行。
6.3 分页原理简析
分页插件本质上是一个 MyBatis 的拦截器,它在执行 SQL 之前,拦截到你的查询语句,做两件事:第一,把原 SQL 包装成一条 count 语句,用于计算总记录数;第二,根据你传入的 current 和 size,在 SQL 尾部拼接 LIMIT。这就是为什么你写的 SQL 里不需要手写 LIMIT,插件会自动处理。看控制台日志,你会发现 MyBatis-Plus 执行了 SELECT COUNT(*) FROM user 和 SELECT id,name,age,... FROM user LIMIT ?,? 两条语句,这就是分了两次查询。如果数据量特别大,count 语句也会有性能开销,针对这个问题 MyBatis-Plus 还提供了 optimizeCountSql 配置,会尝试优化 count 语句,比如把 ORDER BY 去掉,这个在拦截器构造时默认开启的。
7. 常见问题与排查技巧实录
7.1 启动报错时的排查思路
现象 1:Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured
这个不用看第二眼,就是没找到数据源配置。检查 application.yml 里 spring.datasource.url 是不是写对了,或者配置文件有没有被 Maven 打进 classpath。如果你的项目是从别处拷来的,先看看 src/main/resources/application.yml 存不存在,文件后缀是不是 .yml 而非 .yaml(两者都支持,但别一会儿用这个一会儿用那个)。
现象 2:java.sql.SQLException: The server time zone value '...' is unrecognized
明显的时区问题,最直接的解法是在 JDBC URL 末尾加上 serverTimezone=Asia/Shanghai,如果你放在容器里,也可以 -e TZ=Asia/Shanghai 指定容器时区。
现象 3:Public Key Retrieval is not allowed
MySQL 8 的 caching_sha2_password 认证插件导致。有两种解法,一是在 URL 加 allowPublicKeyRetrieval=true,二是在 MySQL 里把用户认证插件改成 mysql_native_password。我建议先改 URL,因为简单、不动数据库。实际线上如果安全要求高,可以两者结合着看。
现象 4:Invalid bound statement (not found)
这个十有八九是 Mapper XML 没找到或没映射上。检查 mapper-locations 配置,确保路径正确,XML 文件的 namespace 要对应 Mapper 接口的全限定名,方法 id 要和接口方法名一致。还有一种可能是用了注解 SQL 但依赖里没加对应的注解处理器类,一般不会出现,先查 XML。
7.2 运行期间连接断开的经典问题
现象:服务跑了一段时间后,第一个查询特别慢,或者直接报 Communications link failure,The last packet successfully received from the server was X milliseconds ago
这是 MySQL 的 wait_timeout 值导致的。MySQL 默认 8 小时没有请求就关闭连接,而应用侧的连接池可能还持有这些已被关闭的连接。解决方案是调整连接池参数,让 max-lifetime 小于 MySQL 的 wait_timeout。比如 MySQL 默认 28800 秒,你要把 HikariCP 的 max-lifetime 设为 1800000 毫秒(即 30 分钟),这样连接在使用前会定期重建,就不会用到被服务端踢掉的连接。
现象:偶尔报 SQLException: Connection is not available, request timed out after 30000ms
这个意思是连接池里没连接可用了,请求排队超时。排查方向是:数据库的 max_connections 是否满了?连接池的 maximum-pool-size 是不是设得太大?是否有慢查询把连接长期占住?我见过一次事故,就是某个接口里忘了释放数据库资源,同时连接池只配了 5,并发一高瞬间打满,表现为偶发超时。解决办法是先打开慢查询日志定位 SQL,再考虑调参。
7.3 常见问题速查表
| 现象 | 根本原因 | 推荐解法 |
|---|---|---|
| 时区报错 | 服务器时区配置不生效 | URL 加 serverTimezone=Asia/Shanghai |
| 中文乱码 | 数据库字符集不是 utf8mb4 | 建库时指定 utf8mb4,URL 加 characterEncoding=utf8 |
| 查询结果字段为 null | 下划线转驼峰没开 | 开启 map-underscore-to-camel-case |
| 主键不自动生成 | 主键策略配错 | 配置 assign_id 或 auto |
| 分页 total=0 | 分页插件未注册 | 手动注入 MybatisPlusInterceptor |
| 启动找不到驱动类 | 驱动坐标或类名不对 | 用 com.mysql.cj.jdbc.Driver |
| 逻辑删除没生效 | 实体字段/全局配置未设 | 加 @TableLogic 或配 logic-delete-field |
7.4 几个容易被忽略的细节
说说我自己的经验吧。
第一,mysql-connector-java 的版本不要手动指定,让 spring-boot-starter-parent 管理版本。Spring Boot 2.7.x 默认管理的 MySQL 驱动是 8.0.33,足够用了。手动指定低版本可能导致 MySQL 8 的认证插件不兼容。
第二,@MapperScan 应该只扫描 mapper 包,不要扫描到 controller 或者 service 包。因为 MyBatis 会把扫描到的接口都解析成 Mapper,如果你把 service 接口也扫进去,启动时就会报 Failed to parse mapping resource,非常莫名其妙。
第三,表名 user 在 MySQL 8 里是可以用的,但有些工具或框架会把它当保留字处理。实际建表我建议加反引号,或者干脆用 sys_user、t_user 这种带前缀的表名,能省不少麻烦。MyBatis-Plus 里如果表名恰好是保留字,要记得在 @TableName 里用反引号包裹。
第四,配置 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl 后,控制台确实会出现所有 SQL 日志,但这种方式性能略差,如果项目 QPS 高,日志会刷屏。生产环境建议改用 org.apache.ibatis.logging.slf4j.Slf4jImpl,配合 logback 的日志级别控制。
8. 扩展:直接能在真实项目中用的技巧
8.1 代码生成器:从建表到 CRUD 一键生成
如果你已经确认了数据库表结构,可以顺手集成 MyBatis-Plus 的代码生成器(旧版叫 AutoGenerator,新版是 3.5.x 的完整模块)。配置一个模板代码,指向你的数据库连接,它能自动生成实体类、Mapper 接口、Mapper XML、Service、ServiceImpl、Controller。
这个工具生成的工程质量相当高,命名规范、注解齐全,直接拿过来改业务逻辑就行。我一般在每个新模块开发前,先建好表,然后跑一次生成器,把整个模块的骨架拉出来,比手写快了几倍。生成器配置略复杂,但一次性配置好,以后都是复用,性价比极高。
8.2 多数据源与读写分离思路
项目变大后,可能会遇到多数据源需求,比如主库和从库分离。MyBatis-Plus 提供了 dynamic-datasource 这个扩展包,可以配置多套数据源,并支持 @DS 注解在方法或类级别指定走哪个库。读写分离的大致做法是:主库配置写库连接,从库配置读库连接,通过 AOP 切面在事务开始时动态选择数据源。这套方案在中小项目里足够用了,不需要上 ShardingSphere 那么重的框架。
但我的建议是:如果你的项目还在起步阶段,单体数据源先用好,等确实出现读压力了再考虑读写分离。过早引入多数据源,开发调试复杂度会明显上升。
8.3 实际部署时的一点提醒
开发环境跑通之后,部署到服务器上要注意:MySQL 的端口 3306 一定要在云服务安全组里放通,否则远程连接照样失败。数据库密码不要硬编码在 application.yml 里,利用环境变量注入,比如 ${DB_PASSWORD},这个在 Docker Compose 和 K8s 场景下很常见。
Spring Boot 项目的打包产出是一个可执行 jar,用 mvn package 生成,然后 java -jar demo.jar 启动。如果服务器内存紧张,可以加 JVM 参数限制堆内存,比如 -Xms256m -Xmx512m。部署 MySQL 时记得设置 lower_case_table_names=1(Linux 下),避免表名大小写不一致导致的问题——Windows 下开发时表名不区分大小写,Linux 下区分,这个差异我见人踩过,上线才报错,很难查。
在真实开发中,我建议你养成一个习惯:每次执行完一段 SQL,就在日志里确认 MyBatis-Plus 实际执行的语句跟你预期是否一致。它虽然帮你写了 SQL,但真正跑的是什么,你心里要有数。这个习惯能让你在遇到各种"玄学"问题时快速定位到根因,而不是盲目瞎猜。
