
1. 项目背景与选型思路1.1 为什么是 SpringBoot3 和 Mybatis PlusSpring Boot 3.0 在 2022 年 11 月正式发布这一版本最大的变化就是基于 Jakarta EE 9 规范把javax.*包名迁移到了jakarta.*同时强制要求 Java 17 作为最低版本。这意味着如果你还在用 JDK 8那基本就和 Spring Boot 3.x 无缘了。很多老项目升级到 Spring Boot 3 之后最先遇到的就是各种第三方库的不兼容问题——MyBatis 官方版本的适配当时也经历了一段时间的调整。而 Mybatis Plus 作为国内使用率极高的 MyBatis 增强框架它提供的BaseMapper、IService、分页插件、代码生成器这些能力确实让日常 CRUD 的开发效率提升了一大截。说实话很多项目中直接用原生 MyBatis 写业务代码的人应该都能感受到那种每张表都要写一套 XML 映射的重复劳动有多折磨人。Mybatis Plus 把单表操作从写 SQL变成了调方法这一层抽象对中小型项目的收益是非常直观的。在 Spring Boot 3.0 刚推出的时候Mybatis Plus 的适配进度有点滞后需要依赖mybatis-plus-spring-boot3-starter这个专门的 starter 而不是之前的mybatis-plus-boot-starter。很多人在集成的时候直接用旧坐标引入结果启动直接报错这个坑我后面会详细说。1.2 这个集成项目适合谁如果你属于下面这几类人这篇内容应该对你比较有参考价值手上维护着老项目准备从 Spring Boot 2.x 升级到 3.x但数据访问层用的还是 Mybatis Plus需要搞清楚兼容方案新项目直接基于 Spring Boot 3 起步需要快速搭建一套包含 mybatis、分页、代码生成的数据访问层已经启动了 Spring Boot 3 项目但踩了各种版本冲突、Bean 注入失败、分页失效的坑想找一份完整的排查思路。我自己的建议是如果你只是做一个简单 Demo 或者短期的内部工具用不用 Mybatis Plus 其实无所谓但凡是正经业务项目尤其表和接口数量一多Mybatis Plus 带来的收益在维护阶段会越来越明显。2. 环境准备与依赖引入2.1 版本选型的核心原则先说结论和 Spring Boot 3.0 搭配的 Mybatis Plus 版本我最开始用的是3.5.3.1后来稳定版推进到3.5.4之后整个兼容性才算真正完善。这里我给出一份基于实际验证过的版本组合组件推荐版本说明JDK17Spring Boot 3 强制要求Spring Boot3.0.x / 3.1.x / 3.2.x3.0.x 是初代3.1.x 更稳定Mybatis Plus3.5.4必须使用 spring-boot3 专用 starterMySQL 驱动com.mysql:mysql-connector-j注意旧版mysql-connector-java的坐标问题HikariCP随 Spring Boot 自带默认连接池不用额外引入Maven3.6Java 17 编译需要较新的 Maven 支持我在初始搭建时踩过最难受的一个坑是Maven 项目里 JDK 版本写的是 17但编译插件用的还是老版本maven-compiler-plugin导致编译时报错invalid target release: 17。这个问题看起来和 Mybatis Plus 无关但它会卡住整个项目启动流程所以建议一开始就把编译插件升级到 3.10.1 以上并且把maven.compiler.source/target显式配置好。2.2 依赖坐标的变化一定要看仔细旧版项目中引入 Mybatis Plus 一般是这样写的dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.x/version /dependency但这个坐标在 Spring Boot 3 下是不能直接用的。因为 Spring Boot 3 的自动配置机制迁移到了jakarta命名空间且spring.factories机制被AutoConfiguration.imports文件取代。Mybatis Plus 官方为此单独出了一套面向 Spring Boot 3 的 starterdependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version3.5.4/version /dependency注意看 artifactId 里面多了个spring-boot3这个细节直接决定项目能不能跑起来。如果你在 Spring Boot 3 里写的是mybatis-plus-boot-starter启动时会看到类似ClassNotFoundException: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration或者 MapperScan 不生效等诡异现象本质都是 starter 与 Boot 3 的自动装配机制不匹配导致的。还有一个经常被忽略的依赖是 JDBC 驱动。MySQL 8 之后官方驱动的 Maven 坐标发生了调整dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency如果使用的是mysql:mysql-connector-java,新驱动包名的前提是版本号要对应上如8.0.33以上。这里我不建议写死版本号直接用 Spring Boot 3.0.x 中自带的版本管理即可。2.3 Maven 工程的基础目录结构假设你从 Spring Initializr 生成了一个标准的 Spring Boot 3 Maven 工程核心目录大概是这样的src/main/java/com/example/demo/ ├── DemoApplication.java ├── controller/ ├── service/ ├── mapper/ ├── entity/ └── config/DemoApplication.java中如果不想在每个 Mapper 接口上写Mapper注解就需要在启动类或配置类上加上MapperScan(com.example.demo.mapper)注解。这一点在 Spring Boot 3 中依然有效。不过有个小细节说一下MapperScan指向的包路径如果写错了启动时不会立刻报错而是一旦你放入容器调用对应 Mapper 方法时才会抛出Invalid bound statement (not found)之类的错误。这种错误很隐蔽排查起来比较费时。所以包路径一定要写准确最好找到 Mapper 接口所在的包路径之后复制粘贴。3. 核心配置与基础代码编写3.1 YAML 配置文件中的关键选项一个能正常跑的数据访问配置最基本的 YAML 片段是这样的spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/demo_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: your_password hikari: minimum-idle: 5 maximum-pool-size: 20 idle-timeout: 30000 connection-timeout: 30000 mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml type-aliases-package: com.example.demo.entity configuration: map-underscore-to-camel-case: true global-config: db-config: id-type: assign_id logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0逐项解释几个容易出问题的点serverTimezoneAsia/Shanghai如果不配时区MySQL 8 驱动会报The server time zone value is unrecognized错误。useSSLfalse本地开发建议关闭 SSL 校验省去一堆证书警告。allowPublicKeyRetrievaltrueMySQL 8 使用 sha256_password 或 caching_sha2_password 插件时在非 SSL 连接下第一次握手需要此参数否则报Public Key Retrieval is not allowed。id-type: assign_idMybatis Plus 默认主键策略是ASSIGN_ID基于雪花算法生成 19 位 Long 型 ID。如果你的表主键是数据库自增应该改成auto或者id-type: auto。这里牵扯到数据库 DDL 的定义稍后演示建表脚本时会再强调。map-underscore-to-camel-case是非常关键的一个开关。比如数据库列名user_name对应 Java 属性userName开启后自动映射不用每次手动写resultMap。不要小看这个配置实际项目里大量 XML 之所以省了很多代码靠的就是它。如果你在 XML 里不写 resultMap并且把这个配置关了查出来的对象里所有下划线字段全是 null这类问题我已经帮人排查过多次。3.2 实体类定义与主键策略的取舍假设我们建一张用户表CREATE TABLE sys_user ( id bigint(20) NOT NULL, username varchar(50) NOT NULL, password varchar(100) NOT NULL, nickname varchar(50) DEFAULT NULL, email varchar(100) DEFAULT NULL, deleted tinyint(1) NOT NULL DEFAULT 0, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;为了配合 Mybatis Plus 的实体映射Java 里对应这样写Data TableName(sys_user) public class SysUser { TableId(type IdType.ASSIGN_ID) private Long id; private String username; private String password; private String nickname; private String email; TableLogic private Integer deleted; TableField(fill FieldFill.INSERT) private LocalDateTime createTime; TableField(fill FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; }这里我推荐一个非常省事的做法把公共字段提取到父类或使用 BaseDO 基类。因为几乎每张业务表都有create_time、update_time、deleted这几个字段如果每张表的实体都写一遍这些字段代码会显得很冗余。用一个基类继承会干净很多。主键策略这块我建议和数据库的表设计对齐如果表主键是bigint且不依赖数据库自增那用ASSIGN_ID没问题如果是int/auto_increment就设置TableId(type IdType.AUTO)。两种策略都能正常工作但混用就容易出问题尤其是分布式场景下用自增 ID 很容易撞号。3.3 Mapper 接口和 Service 基础实现一个最基础的 Mapper 接口其实就是继承BaseMapperMapper public interface SysUserMapper extends BaseMapperSysUser { }如果你想在 Service 层省更多事可以继续继承ServiceImplpublic interface SysUserService extends IServiceSysUser { } Service public class SysUserServiceImpl extends ServiceImplSysUserMapper, SysUser implements SysUserService { }这两层继承在 Mybatis Plus 里提供了非常多的开箱即用方法getById、saveOrUpdate、removeById、lambdaQuery、lambdaUpdate等。实话说,如果只是简单 CRUD这比在 XML 里手写 SQL 快太多了。Controller 里可以这样快速写出一个接口RestController RequestMapping(/user) public class SysUserController { private final SysUserService sysUserService; public SysUserController(SysUserService sysUserService) { this.sysUserService sysUserService; } GetMapping(/{id}) public ResultSysUser getUser(PathVariable Long id) { return Result.ok(sysUserService.getById(id)); } PostMapping public ResultBoolean saveUser(RequestBody SysUser user) { return Result.ok(sysUserService.save(user)); } }注意我用的构造器注入方式。Spring Boot 3 和 Spring Framework 6 都推荐构造器注入Autowired字段注入虽然能跑但在单元测试和不可变设计上不如构造器友好。3.4 分页插件的正确打开方式Mybatis Plus 3.5.x 系列中分页插件已经分成了单独的模块mybatis-plus-jsqlparser。特别是从3.5.9版本之后官方把 SQL 解析依赖进行了调整这些细节你不留意的话分页插件怎么配置都不会生效的。基础版本的用法是在配置类中注册一个MybatisPlusInterceptorBeanConfiguration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); PaginationInnerInterceptor paginationInterceptor new PaginationInnerInterceptor(DbType.MYSQL); paginationInterceptor.setMaxLimit(500L); paginationInterceptor.setOverflow(true); interceptor.addInnerInterceptor(paginationInterceptor); return interceptor; } }几个参数具体含义DbType.MYSQL告诉插件当前数据库方言是 MySQL这样在生成LIMIT ?语句时才是正确格式。如果配错成 OracleSQL 就会生成成用ROWNUM的形式执行必然报错。Overflow当请求页码超过总页数时如果设置为true会回查最后一页而不是直接返回空列表。这个看业务需求来定可以根据用户习惯选择是否开启。MaxLimit单页最大查询数量对接口稳定性是一种很实用的保护。有些接口前端不小心把size传成了几万如果没有限制一条分页 SQL 就可能把数据库拖垮。配套的分页查询这样写public PageSysUser pageUsers(int pageNum, int pageSize) { PageSysUser page new Page(pageNum, pageSize); LambdaQueryWrapperSysUser wrapper new LambdaQueryWrapper(); wrapper.orderByDesc(SysUser::getCreateTime); return sysUserService.page(page, wrapper); }Page对象被直接作为第一个参数传入后Mybatis Plus 会自动把 SQL 改写成带LIMIT的版本并把 total 等元数据回填到同一个 Page 对象上。你不用手动去 count插件会自动做。4. 实操过程中的关键细节与问题排查4.1 启动时常见的 Bean 创建失败我见过很多人集成完成后一启动就在控制台看到类似这一段信息Description: Field sysUserMapper in com.example.service.impl.SysUserServiceImpl required a bean of type com.example.mapper.SysUserMapper that could not be found. Action: Consider defining a bean of type com.example.mapper.SysUserMapper in your configuration.这个报错的大概原因就是 Mapper 接口没有交给 Spring 管理。解决方式有两个在启动类加MapperScan(com.example.mapper)在每一个 Mapper 接口上直接加Mapper注解。我个人更推荐第一种直接在启动类上标注包扫描路径一次搞定所有 Mapper避免漏注。尤其 Mapper 接口一多逐个加注解既费时又容易跳。还有种特殊情况虽然加了MapperScan但 Mapper 接口所在的包不在 Spring Boot 应用主类的子包下这种扫描也可能会失效。因为MapperScan指定的路径是和 Spring 组件扫描路径独立解析的如果项目有多个源码根目录就要确保 MapperScan 路径确实存在。4.2Invalid bound statement (not found)是什么回事这个报错第一次遇到时很容易蒙感觉 Mapper 方法都写得没错、接口也好好的为什么运行时找不到 SQL其实这个错误通常意味着通过 Mapper 接口方法名找不到对应的 SQL 语句。如果你用的是纯BaseMapper提供的方法一般不会出现但当你写自定义方法时就要检查是不是 XML 没有正确加载。排查思路我整理成一个流程确认mybatis-plus.mapper-locations配置的路径是否覆盖到了 XML 所在目录。比如 XML 放在src/main/resources/mapper/但是配置写成了classpath*:mybatis/**/*.xml必然找不到检查 XML 文件里的namespace是否完全等于 Mapper 接口的全限定名看看 Mapper 接口方法在 XML 中的id与方法名是否一致编译后有没有把 XML 复制到target/classes下。如果resources配置把src/main/java下的 XML 排除了也可能出现这种问题。这里我还建议一个自带定位方式项目启动时把 Mybatis Plus 的日志级别打开找到Creating class file mapper或者Loaded plugin相关日志如果看到Class not found: ...mapper.SysUserMapper之类的字样基本就是扫描路径出问题了。logging: level: com.example.demo.mapper: debug com.baomidou.mybatisplus: debug配好后运行时可以在控制台看到真正的 SQL 语句和查询参数很多诡异问题都能从 SQL 的拼写里直接发现。4.3 逻辑删除字段不生效的隐藏原因TableLogic注解可以让 Mybatis Plus 在做 delete 操作时自动改成UPDATE ... SET deleted 1也会在 select 时自动加上deleted 0条件。但很多人配置完发现逻辑删除不生效常见原因如下字段名不是deleted而全局配置里logic-delete-field: deleted与实体字段没有对上实体字段类型是Boolean数据库列是tinyint(1)。这个映射虽然一般能正常工作但在某些驱动版本下会转换异常稳妥起见用Integer自定义 XML 里手写 SQL 时Mybatis Plus 的自动逻辑删除是不作用于自定义 SQL 的。也就是说TableLogic主要通过内置 CRUD 方法才生效如果你写了delete iddeleteByIdDELETE FROM sys_user WHERE id #{id}/delete即使实体类标注了逻辑删除,也会真的物理删除这个要小心。这里我给一个明确的结论如果大量使用自定义 SQL 或 XML 复杂查询逻辑删除的便利性会被削弱因此建表时删除字段要提前设计好不能指望 Mybatis Plus 在所有 SQL 场景里自动兜底。4.4 代码生成器的坑与推荐Mybatis Plus 官方提供代码生成器Mybatis-Plus Generator但围绕它的版本调整非常多。3.5.3 之后生成器变成了独立模块需要单独引入。使用生成器能极大提升建实体/Mapper/Service 的效率但我不建议直接一把梭生成所有代码。原因是生成的代码可能包含大量你不需要的模板方法后续还要手动删反而增加工作量。我更建议只生成entity和mapperService 层根据业务判断再手动编写。这样数据库字段改动迁移到代码时至少实体和 Mapper 层能保持一致。代码生成器示例片段:FastAutoGenerator.create(jdbc:mysql://localhost:3306/demo_db?serverTimezoneAsia/Shanghai, root, password) .globalConfig(builder - builder.author(yourName) .outputDir(D:/generator/src/main/java)) .packageConfig(builder - builder.parent(com.example.demo) .entity(entity) .mapper(mapper)) .strategyConfig(builder - builder.addInclude(sys_user) .entityBuilder() .enableLombok() .enableTableFieldAnnotation()) .templateConfig(builder - builder.disable(TemplateType.SERVICE) .disable(TemplateType.SERVICE_IMPL) .disable(TemplateType.CONTROLLER) .disable(TemplateType.XML)) .execute();生成后注意检查TableName和字段注解是否正确特别是数据库表名有前缀或带下划线的情况。enableTableFieldAnnotation()会让所有字段都生成TableField注解偶尔会把主键也打上TableField注解如果主键没有TableId后面一切基于 ID 的内置方法都会出错。所以生成完文档后自己一定要去把TableId检查一遍。5. 进阶场景与性能优化建议5.1 Lambda 条件构造器的使用姿势Mybatis Plus 的一大优势就是LambdaQueryWrapper可以避免在代码里写一堆魔法字符串编译期就能发现字段名拼写错误。我平时写查询接口时基本上都是这样组织条件public ListSysUser queryUsers(String keyword, Integer status, LocalDateTime startTime, LocalDateTime endTime) { LambdaQueryWrapperSysUser wrapper Wrappers.lambdaQuery(); wrapper.like(StringUtils.hasText(keyword), SysUser::getUsername, keyword) .eq(status ! null, SysUser::getStatus, status) .ge(startTime ! null, SysUser::getCreateTime, startTime) .le(endTime ! null, SysUser::getUpdateTime, endTime) .orderByDesc(SysUser::getCreateTime); return sysUserService.list(wrapper); }注意.like(condition, column, value)这种写法中第一个参数是布尔值只有条件成立时才拼接 SQL。我最早写代码时没掌握这个技巧用一堆if判断后调wrapper代码看起来很啰嗦。用这种方式之后代码可读性好了不少也不容易漏拼条件。但这并不代表条件构造器在所有场景下都适用。当需要多表 join 或者复杂子查询时代码写起来反而更绕不如直接用 XML 写 SQL。所以我的原则是单表操作、简单条件用 LambdaWrapper多表关联、复杂聚合直接走 XML 自定义 SQL。5.2 分页查询中的 count 优化分页插件自动生成了 count 语句之后在大数据量场景下可能会很慢。因为默认生成的 count 一般长这样SELECT COUNT(*) FROM sys_user WHERE ...假如查询条件里关联了多张表虽然是单表查询包装了子查询或者 WHERE 条件里有很重的函数计算那 count 往往比真正取数还慢。Mybatis Plus 在PaginationInnerInterceptor中提供了一些优化入口但不一定所有场景都能识别。我实操中最有效的方案是如果不需要精确总数直接设置SearchCount(false)比如管理后台的日志列表或者无限滚动的记录列表。这时候分页查询里不要再 total 即可返回的 Page 里 total 会是默认值虽然无法展示总页数但响应速度可以提升不少。PageSysUser page new Page(pageNum, pageSize, false);第三个参数为false表示不查 countMybatis Plus 在解析时也会省略掉 count 语句的生成。如果业务上确实需要总数那就为超大数据表单独设计统计 SQL 或使用 ES 做汇总而不是把所有压力都放在这一个 count 上。5.3 多数据源与动态数据源Spring Boot 3 项目如果涉及多个数据库Mybatis Plus 官方提供了dynamic-datasource-spring-boot3-starter。它的核心思路是在运行时通过DS注解切换数据源非常适合读写分离或分库分表场景。dependency groupIdcom.baomidou/groupId artifactIddynamic-datasource-spring-boot3-starter/artifactId version4.x.x/version /dependency配置如下spring: datasource: dynamic: primary: master strict: false datasource: master: url: jdbc:mysql://localhost:3306/demo_master username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver slave: url: jdbc:mysql://localhost:3306/demo_slave username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver然后在 Service 层打上注解DS(slave) public ListSysUser getReadOnlyList() { return sysUserMapper.selectList(null); }DS可以作用在 Service 方法上也可以作用在实现类上。注意事务和动态数据源的搭配如果在同一个事务里切换到另一个数据源因为事务边界通常在一个连接上动态数据源切换可能失效或走错连接所以跨库事务尽量得用分布式事务方案不能只依赖DS和Transactional。5.4 字段自动填充与乐观锁除了逻辑删除Mybatis Plus 还支持自动填充功能常见场景是创建时间和更新时间。实现方式是自定义一个MetaObjectHandlerComponent public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createTime, LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } }这里面有个细节需要注意strictInsertFill只有在实体类对应字段上配置了TableField(fill FieldFill.INSERT)时才会生效。如果实体字段没有这个注解虽然 MetaObjectHandler 里写得再完整也不会填充进去。乐观锁插件是常见的并发控制方式Mybatis Plus 提供的OptimisticLockerInnerInterceptor可以在 update 时把版本号作为一个条件带上Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); PaginationInnerInterceptor paginationInnerInterceptor new PaginationInnerInterceptor(DbType.MYSQL); interceptor.addInnerInterceptor(paginationInnerInterceptor); return interceptor; }实体字段加一个VersionVersion private Integer version;执行 update 时Mybatis Plus 会自动拼接WHERE version 旧值更新成功后再把版本号1。如果并发下别人先更新了你的 update 影响行数为 0业务层拿到影响行数后就可以做重试或提示。这个机制简单可靠但注意乐观锁只对内置的 updateById/update 生效走自定义 XML 的 update 语句依然需要自己手动加上版本条件。6. 常见问题速查与避坑心得6.1 配置与启动阶段速查表问题现象可能原因解决方案ClassNotFoundException: jakarta.servlet.*依赖仍是旧版 javax 体系排查所有第三方 spring boot starter 的版本换用支持 jakarta 的新版本Invalid bound statement (not found)XML 加载路径不对或者 namespace 错误检查mapper-locations与 XML 的 namespaceMapper Bean 找不到缺少MapperScan或Mapper启动类加MapperScan指向真实 Mapper 包路径Public Key Retrieval is not allowedMySQL 8 在新连接下需要公钥获取JDBC URL 加allowPublicKeyRetrievaltrue分页不生效返回全量数据没有注册MybatisPlusInterceptor配置类中注册并添加PaginationInnerInterceptor时间字段为 null实体没有配置自动填充或者数据库默认值存在但 Java 对象未同步加入 MetaObjectHandler 并配置TableField(fill...)6.2 实践中总结的三条经验第一升级大版本时不要盲目追新。Spring Boot 3.0 刚发布那阵子各种第三方依赖根本跟不上网上很多技术文章抄来抄去自己不动手验证就是纸上谈兵。我现在倾向稳定的做法是先在一个独立的 feature 分支升级只改依赖和必要代码跑通核心接口再去处理边角逻辑。第二配置文件和注解要能对应上。Mybatis Plus 很多功能是“注解 全局配置 拦截器”三件套一起工作缺一个就不生效。比如逻辑删除全局配置里声明了logic-delete-field实体类却没有TableLogic注解实际上并不会生效。所以遇到功能不生效先按这几个维度逐项排查。第三日志是排查数据访问层问题最直接的手段。调整一下logging.level就能看到 Mybatis Plus 生成的所有 SQL 和参数。当你发现 SQL 跟自己想的不一样比如多了ORDER BY或者少了某个条件很快就能锁定问题是代码拼接错误还是插件干扰。6.3 后续还可以继续扩展的方向这个基础集成做完后如果你有兴趣继续深挖我建议按这几条线走引入mybatis-plus-extension中的BlockAttackInnerInterceptor拦截全表 update/delete防止误操作把整张表清掉结合mybatis-plus-jsqlparser做 SQL 解析层面的自定义拦截器比如多租户数据权限自动拼接 tenant 条件数据量上来后把分页查询迁移到 ES 或 ClickHouse 中MySQL 只负责事务型核心数据的写入。这些方向都是基于同一个 Spring Boot 3 Mybatis Plus 底座去延伸的先把基础集成做扎实后面扩展就顺理成章。最后分享一个我自己编程时候的小习惯每次新建 Spring Boot 3 Mybatis Plus 项目我都会先做一次最小闭环验证——建一张表、写一个实体、跑通增删改查和分页再继续接业务代码。这个闭环验证一遍大概只需要十几分钟但能提前把数据源配置、Mapper 扫描、插件启用这些容易出错的底层问题全部暴露出来后面写业务时顺畅很多。如果你也正在做集成别急着复制一堆代码先搭好地基再盖楼这个思路在哪一层都通用。