先说结论Spring Boot 3.3.X 整合 MyBatis-Plus 并没有那么想象中轻松。很多刚升级 Boot 3 的兄弟第一件事就是照搬旧项目依赖结果启动直接报ClassNotFoundException或者Invalid bound statement然后陷入“到底是依赖问题还是配置问题”的无限排查里。这篇文章我按自己实际项目从 2.7 升级到 3.3.4 的过程来写把版本选择、starter 坑位、XML 映射路径、分页插件、LambdaQueryWrapper 实操、以及很多人关心的“XML 和 Mapper 接口放同一个文件夹怎么配置”一次讲清楚最后再加一个和 Spring Data JPA 的实战对比方便你在技术选型时心里有底。整套方案最终我是在 Spring Boot 3.3.4 MyBatis-Plus 3.5.7 的组合上跑通的用了小半个月单表 CRUD、复杂多表查询、分页、逻辑删除、自动填充都验过。如果你也是 Spring Boot 3.3.X 项目要接 MyBatis-Plus或者正在纠结 XML 路径总是扫不到的问题这篇可以直接照着操作。1. 整体设计与版本选型为什么是 Spring Boot 3.3.X MyBatis-Plus1.1 版本适配关系重点中的重点Spring Boot 3.3.X 和 MyBatis-Plus 之间是有一个隐藏适配门槛的。不是说我随便引一个mybatis-plus-boot-starter就能用而是必须用官方针对 Spring Boot 3 单独发布的mybatis-plus-spring-boot3-starter两者区别非常大。Spring Boot 3 底层的核心变化是这两点基础框架从 Spring Framework 5.x 升级到 6.x整个包结构从javax.*迁移到jakarta.*命名空间。自动配置机制从spring.factories换成了AutoConfiguration.imports文件加载方式。这就导致 MyBatis-Plus 老版本里那套基于spring.factories的自动装配在 Boot 3 下完全失效。MyBatis-Plus 官方从 3.5.5 版本开始才提供了专门的 Boot 3 starter所以在引入依赖时不要再用老写法mybatis-plus-boot-starter而是下面这种dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version3.5.7/version /dependency如果你在 Spring Boot 3.3.X 项目里用了mybatis-plus-boot-starter大概率启动时会遇到类似Failed to instantiate [org.apache.ibatis.session.SqlSessionFactory]的报错。这个不是代码问题纯粹是 starter 用错了。另外一个坑是版本最低要求。Boot 3.3.X 对应 Spring Framework 6.1MyBatis-Plus 低于 3.5.5 即使能启动分页插件和条件构造器也容易出现兼容性异常。所以我直接建议Boot 3.3 的版本用 MyBatis-Plus 3.5.5 以上我自己用的 3.5.7 是目前比较稳的版本。此外 MyBatis 底层依赖也会被这个 starter 带进来你不需要另外引mybatis-spring-boot-starter以免出现两个SqlSessionFactory冲突。对了JDK 版本也必须注意。Spring Boot 3 最低要求 JDK 17官方推荐 17 或 21。很多同学本机还是 JDK 8启动直接报UnsupportedClassVersionError。这个不属于整合问题但也经常被误认为 MyBatis-Plus 的锅。1.2 这套组合能解决什么问题MyBatis-Plus 的定位是“MyBatis 的增强工具只做增强不做改变”。在我的实际项目里它解决的核心问题主要有三个。第一单表 CRUD 零 SQL。日常开发里最耗时的是什么不是复杂的报表 SQL而是对每一张业务表都要写 insert、update、deleteById、selectById 这一套重复代码。MyBatis-Plus 提供了BaseMapperT接口继承之后单表操作基本不用写 XML 映射文件。这不光省了代码量还减少了因为手写 SQL 导致的字段漏写问题。第二单表条件查询不需要手工拼 SQL。传统 MyBatis 里一个“按用户名查用户并且按创建时间排序”的查询至少要在 XML 里写一段where动态判断用 MyBatis-Plus 的LambdaQueryWrapper之后三行 Java 代码搞定而且能保证条件为空时不会生成错误的 SQL 片段。第三团队协作时对 SQL 的可控性更强。MyBatis-Plus 并不限制你写 XML遇到复杂多表关联、子查询、动态排序时依然可以把 SQL 写到 XML 里和接口方法一一对应。这样既不牺牲 MyBatis 原有的粒度控制又能在简单场景下开箱即用。不过要注意MyBatis-Plus 不是 JPA 那种全自动 ORM它不会帮你自动建表、不会帮你维护关系映射。它更像是一个“偷懒工具”在 MyBatis 的基础上把简单事自动化把复杂事保留给 SQL。理解这一点你在项目里就不会对它有超出职责的期望。2. 基础整合从空项目到第一个 Mapper 跑通2.1 依赖引入starter 和版本号怎么选我不是直接新建空项目带你敲命令而是把依赖部分讲透因为这是我踩坑最多的地方。假设你已经是 Spring Boot 3.3.X 项目pom.xml里除了基本的 web 依赖之外至少要有下面几个依赖dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version3.5.7/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependencyMySQL 驱动为什么不用写版本号因为 Spring Boot 3.3.X 的spring-boot-dependenciesBOM 已经帮你管理了com.mysql:mysql-connector-j的版本所以可以省略 version。MyBatis-Plus 的 starter 不在 Spring 官方 BOM 里必须显式指定版本。如果你用的是 PostgreSQL数据源驱动改成org.postgresql:postgresql即可MyBatis-Plus 对数据库方言的支持是通用的。不过我这里后续所有 SQL 示例以 MySQL 为主。这里有个细节你可能没注意很多老教程会让你同时引入mybatis-plus-boot-starter和mybatis-plus-generator但 Boot 3 项目引mybatis-plus-boot-starter会连带引入一个旧版mybatis-spring导致兼容性问题。正确的做法是使用mybatis-plus-spring-boot3-starter如果要做代码生成器再去单独引生成器模块后面我会简单提一下。2.2 连接池与数据源配置Spring Boot 3.3.X 默认的数据库连接池是 HikariCP不需要额外引入 C3P0 或者 Druid。除非你有监控需求否则我建议直接用 HikariCP性能和稳定性都足够好。在application.yml里的最简配置如下spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/demo?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 123456 hikari: minimum-idle: 5 maximum-pool-size: 15 connection-timeout: 30000 mybatis-plus: mapper-locations: classpath*:com/example/demo/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: banner: false db-config: id-type: assign_id logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0先解释几个关键配置后面用到才不会懵mapper-locations指定 Mapper XML 文件路径。classpath*表示从所有依赖的 classpath 根目录搜索适合多模块项目。配置成com/example/demo/mapper/**/*.xml意思是扫描这个包路径下的所有 XML。type-aliases-package实体类所在的包路径这样在 XML 里写resultTypeUser时不用写全限定名。map-underscore-to-camel-case把数据库create_time自动映射到 Java 属性createTime。必须打开否则你查出来的实体类创建时间字段永远是 null。log-impl打印 MyBatis 执行的 SQL 到控制台。这个在排查问题时非常重要强烈建议开发环境开启生产环境改成不打印或使用 logback 的级别控制。id-type: assign_id默认主键策略。如果不配置MyBatis-Plus 生成的新主键是雪花 ID字符串形式对数据库是 bigint 比较友好。如果你的表是数据库自增主键这里需要改成auto并在实体类主键上加TableId(type IdType.AUTO)两者要对应上。2.3 第一次 CRUD 跑起来按下面的步骤走一遍基本就能确定整合没问题。第一步创建实体类Data TableName(sys_user) public class User { TableId(type IdType.ASSIGN_ID) private Long id; private String username; private String password; private String email; TableField(fill FieldFill.INSERT) private LocalDateTime createTime; TableField(fill FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; TableLogic private Integer deleted; }TableName指定数据库表名。如果你的表名和类名符合驼峰转下划线规则不写也可以但明确了更保险。TableLogic表示逻辑删除字段后面的 SQL 会自动带上deleted 0条件。第二步创建 Mapper 接口Mapper public interface UserMapper extends BaseMapperUser { }有两条路让 Spring 扫描到这个 Mapper一是在接口上加Mapper注解二是在启动类上加MapperScan(com.example.demo.mapper)。我自己更推荐启动类MapperScan这样不用每个 Mapper 都写注解。第三步写一个 Controller 或者直接用单元测试验证SpringBootTest class UserMapperTest { Autowired private UserMapper userMapper; Test void testSelectList() { ListUser users userMapper.selectList(null); users.forEach(System.out::println); } }selectList(null)表示无条件查询全表。如果控制台打印出了 SQL并且能查出数据说明整合成功。之后你可以直接体验insert、updateById、deleteById等方法这也是 MyBatis-Plus 最吸引人的地方单表 CRUD 再也不用手写 SQL。3. XML 与 Mapper 同目录的配置痛点与解法3.1 为什么 XML 会“找不到”我猜你搜到这篇文章很可能就是因为Invalid bound statement (not found)这个报错。这个报错的意思其实很简单MyBatis 根据 Mapper 接口的方法名找不到对应的 SQL 语句。原因通常有三个XML 文件没有被复制到 classpath 路径下。mapper-locations配置的路径没有覆盖到 XML 文件所在位置。XML 里的namespace写错了导致 XML 和 Mapper 接口对不上。对于“XML 与 Mapper 接口放在同一个文件夹下”这个需求很多人直接在src/main/java里建了一个和 Mapper 接口同名的UserMapper.xml然后在application.yml里配置classpath*:com/example/demo/mapper/**/*.xml但启动后依然报错。原因在于 Maven 构建时src/main/java目录下的.java文件会被编译到 target/classes但.xml文件默认不会被拷贝进去。所以你配置的路径在 classpath 下根本不存在。3.2 同目录摆放的三种配置方式先说结论我不建议为了追求“接口和 XML 放同一个目录”去改 Maven 配置因为这不是标准做法。但如果你项目组就是有人坚持要这么干有两条路可以走。方案一修改pom.xml把源码目录下的 XML 也打进去。build resources resource directorysrc/main/java/directory includes include**/*.xml/include /includes /resource resource directorysrc/main/resources/directory /resource /resources /build这样src/main/java/com/example/demo/mapper/UserMapper.xml就会被打包进target/classes。此时mapper-locations配置保持classpath*:com/example/demo/mapper/**/*.xml就能扫描到。方案二推荐做法让 XML 放到src/main/resources下但目录结构和 Mapper 接口保持一致。比如 Mapper 接口是com.example.demo.mapper.UserMapper那么 XML 就放到src/main/resources/com/example/demo/mapper/UserMapper.xml这样不需要改任何 Maven 配置只要配置好 mapper-locations 就行mybatis-plus: mapper-locations: classpath*:com/example/demo/mapper/**/*.xml方案三把 XML 直接放到src/main/resources/mapper/目录下不做任何包结构对应统一管理。mybatis-plus: mapper-locations: classpath*:mapper/*.xml这种方式对项目小、Mapper 数量少的情况完全够用。但项目大了之后几十个 XML 堆在一个目录里有碍管理。在这三种方案里我最推荐方案二。原因很简单它既满足了“XML 和 Mapper 包结构一致”的直觉需求又不用额外改 Maven 配置打包后路径清晰。3.3 打包阶段的两个坑如果你用了方案一还有一个很隐蔽的坑IDE 编译时没问题但mvn clean package打出来的 jar 包可能仍然找不到 XML。原因就是你只配置了src/main/java资源目录却没有保留src/main/resources作为资源目录。上面给的配置里我两个resource都写了不要只写第一个。第二个坑是 XML 的namespace绑定错误。无论你用哪种目录方案UserMapper.xml里的 namespace 必须是 Mapper 接口的全限定名?xml version1.0 encodingUTF-8? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecom.example.demo.mapper.UserMapper select idselectUserDetailById resultTypecom.example.demo.entity.User SELECT * FROM sys_user WHERE id #{id} /select /mapper内联的select标签id要和 Mapper 接口方法名完全一致。这个方法名错了也会报Invalid bound statement。另外提醒一句当你同时使用了BaseMapper的内置方法又定义了同名的自定义方法时XML 里必须有对应的 SQL否则调用同名方法会报找不到语句。这种情况在做代码演进时很容易出现我建议自定义方法的命名尽量不要和内置方法重名比如加个前缀selectCustom、queryPageList。4. 核心功能实操分页、条件构造器与自动填充4.1 分页插件配置底层原理简说分页是 MyBatis-Plus 使用频率最高的功能之一。但它不是默认开启的需要手动注册一个MybatisPlusInterceptorBean。Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); PaginationInnerInterceptor paginationInterceptor new PaginationInnerInterceptor(DbType.MYSQL); paginationInterceptor.setMaxLimit(500L); paginationInterceptor.setOverflow(false); interceptor.addInnerInterceptor(paginationInterceptor); return interceptor; } }DbType.MYSQL指定数据库方言这样分页时才能生成正确的LIMIT语句。如果你用的是 PostgreSQL就改成DbType.POSTGRE_SQL。overflow参数控制当页码超出最大值时是否回退到第一页默认为 false也就是继续查空数据。分页插件底层做的事情是把Page参数解析成物理分页。比如你调用selectPage(new Page(1, 10), wrapper)插件会先在执行前改写 SQL生成一条COUNT(*)查询统计总数再把原来的 SQL 拼接上LIMIT 0, 10。两个动作是同步完成的这就是为什么你不需要自己写 count 语句。一个比较典型的翻车场景是分页插件对该分页的 SQL 没生效查询出来的 List 是全表数据。排查思路首先确认是否有且只有一个MybatisPlusInterceptorBean。如果你项目里同时存在多个拦截器或者误注册了两次分页就会失效。分页使用示例PageUser page new Page(1, 10); LambdaQueryWrapperUser wrapper Wrappers.lambdaQuery(); wrapper.like(StringUtils.hasText(keyword), User::getName, keyword) .orderByDesc(User::getCreateTime); PageUser result userMapper.selectPage(page, wrapper); long total result.getTotal(); ListUser records result.getRecords();这里有个值得注意的细节wrapper.like(condition, column, value)的第一个参数是 boolean当 keyword 为空时这个条件不会拼接到 SQL 里。这是 MyBatis-Plus 条件构造器最常用的防空查询技巧。4.2 条件构造器 Wrapper 的使用套路Wrapper 是 MyBatis-Plus 的灵魂。新手最容易犯的错误是直接 new QueryWrapper 然后用字符串写列名比如wrapper.eq(user_name, 张三)。这样做的问题很明显数据库字段改名后Java 代码里的字符串不会跟着变编译期不报错运行期才炸。更推荐的做法是LambdaQueryWrapper用方法引用代替字符串列名LambdaQueryWrapperUser wrapper Wrappers.lambdaQuery(); wrapper.eq(User::getUsername, admin) .like(StringUtils.hasText(keyword), User::getEmail, keyword) .between(startTime ! null endTime ! null, User::getCreateTime, startTime, endTime) .orderByDesc(User::getCreateTime) .last(LIMIT 100);最后那个.last(LIMIT 100)是个高危操作因为它会把字符串原封不动拼到 SQL 末尾。如果内容是用户输入且没有经过校验会造成 SQL 注入风险。能用query.limit或者分页解决的就不要用.last拼用户数据。还有一个场景大家经常会搞混就是eq和in的区别。eq是等值查询in是范围查询。很多人在需要查“ID 为 1、2、3 的集合”时用了.eq(User::getId, ids)结果 SQL 变成id [1, 2, 3]数据库直接报错。正确写法是ListLong ids Arrays.asList(1L, 2L, 3L); LambdaQueryWrapperUser wrapper Wrappers.lambdaQuery(); wrapper.in(User::getId, ids);如果你做后端接口经常需要把前端的keyword参数同时匹配用户名和邮箱可以用嵌套andwrapper.and(w - w.like(User::getUsername, keyword) .or() .like(User::getEmail, keyword));不加这种方式的话直接用.like(...).or().like(...)会导致整个 SQL 查询条件变成无条件查询一部分加上前面其他的eq条件后优先级就不对了。这个是被很多人忽略的小坑。4.3 自动填充与逻辑删除的配置create_time、update_time这种字段每次 insert 和 update 都手动 set 是很蠢的做法。MyBatis-Plus 提供了自动填充功能。实体类字段上要指定填充策略TableField(fill FieldFill.INSERT) private LocalDateTime createTime; TableField(fill FieldFill.INSERT_UPDATE) private LocalDateTime updateTime;然后实现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只会在字段值为空的时候填充。如果你在代码里手动给createTime赋值了它不会覆盖。这个行为对绝大多数场景是合理的但如果你确实需要强制更新就要在填充前先setFieldValByName。我自己做审计类操作时曾因为没弄懂这个规则导致手动传入的时间被丢掉了排查了半天才意识到填充器“不覆盖”的规定。逻辑删除也是做互转系统时必配的功能。前面实体类中已经加了TableLogic private Integer deleted;再加上全局配置里的mybatis-plus: global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0以后执行deleteByIdMyBatis-Plus 底层会把它转成UPDATE sys_user SET deleted 1 WHERE id ? AND deleted 0而所有查询会自动拼接AND deleted 0。如果这个逻辑没生效先确认实体类的字段名和logic-delete-field是否一致。其次如果你把logic-delete-field配置在 yml 里同时又在实体类字段上加TableLogic两者不会冲突但配置优先级容易让人迷惑。我建议统一用TableLogic注解这样代码里一眼就能看到逻辑删除字段比放在 yml 里更直观。自动填充和逻辑删除属于那种“配置一次受益终身”的功能建议新项目集成时第一时间就配上。这比后期在几百个接口里手动补充deleted 0条件要省太多事。5. MyBatis-Plus 与 Spring Data JPA 怎么选一个实战对比5.1 同一条查询两种写法既然搜到了 MyBatis-Plus 和 Spring Data JPA 的区别这个对比还是值得拿出来聊聊。我在中间件技术选型时经常看到团队在这两个框架之间反复横跳。其实关键不是谁好谁坏而是同一需求下两种框架的表达成本不一样。假设我们要实现一个接口根据用户名模糊查询用户列表按创建时间倒序并做分页。MySQL 表结构是sys_user(id, username, email, create_time, deleted)用户名不能为空。先用 MyBatis-Plus 写PageUser page new Page(pageNum, pageSize); LambdaQueryWrapperUser wrapper Wrappers.lambdaQuery(); wrapper.like(User::getUsername, username) .orderByDesc(User::getCreateTime); PageUser result userMapper.selectPage(page, wrapper);用 Spring Data JPA 写Pageable pageable PageRequest.of(pageNum - 1, pageSize, Sort.by(Sort.Direction.DESC, createTime)); SpecificationUser spec (root, query, cb) - { ListPredicate predicates new ArrayList(); predicates.add(cb.like(root.get(username), % username %)); return cb.and(predicates.toArray(new Predicate[0])); }; PageUser result userRepository.findAll(spec, pageable);两组代码一对比就能感受出来MyBatis-Plus 的 README 风格是“撸起袖子写条件”JPA 的 Specification 风格是“引入一堆类型安全查询 API”。代码量上差距不算大但可读性上 MyBatis-Plus 更接近人类思维。JPA 的Specification和Predicate概念也不是不好只是团队成员如果没有精通 JPA 核心概念的人维护起来确实费劲。如果遇到更复杂的动态条件比如多表 join、group by、having、临时变量MyBatis-Plus 可以无压力地在 XML 里写原生 SQL 控制一切JPA 虽然也能用Query(nativeQuery true)但复杂 SQL 写进去之后复杂的拼接和动态条件处理依然不如 MyBatis 的where/if顺手。5.2 什么时候适合用 MyBatis-Plus从我个人的实操感受来看如果项目具备下面任一特征MyBatis-Plus 通常是更稳的选择频繁需要多表关联查询而且 SQL 里充满动态条件。团队对 SQL 有很强掌控欲要求每一句 SQL 都在代码审计范围内。项目是从老系统迁移过来的历史遗留 SQL 逻辑复杂。需要兼容多种数据库比如 MySQL 测试、PostgreSQL 上生产的场景。JPA 的优势则集中在纯单表、模型驱动、领域事件这类场景里尤其是 DDD 风格项目实体生命周期管理比 MyBatis-Plus 这种半自动框架更有存在感。但 JPA 想把复杂查询做好需要团队成员有比较扎实的 JPA 基础否则就是满屏的Query(nativeQuery true)最后还是回到了 SQL 老路。所以不存在哪个框架碾压哪个只存在你的团队和业务更适合哪个。如果只是做后台管理系统CRUD 占大头复杂报表占小头MyBatis-Plus 的综合成本更低。6. 常见问题与排查技巧实录6.1 常见报错速查表下面这几类问题都是我在实际项目中真实碰到过的按出现频率从高到低整理如下。报错现象根本原因解决方案Invalid bound statement (not found)XML 没有被扫描到或 namespace/方法名不匹配检查 mapper-locations 路径、检查 Maven 打包是否包含 XML、检查 namespace 是否为接口全限定名Failed to configure a DataSource数据源依赖或配置缺失检查 spring.datasource.url 是否配置驱动依赖是否引入Property sqlSessionFactory or sqlSessionTemplate are required依赖冲突或 starter 版本错误确认使用了 mybatis-plus-spring-boot3-starter 且版本≥3.5.5排除多余 mybatis-spring-boot-starterClassNotFoundException: jakarta.servlet.*项目里混用了 javax 和 jakarta 包检查是否有旧版 servlet-api 依赖统一为 jakarta分页失效查到全表MybatisPlusInterceptor 未注册或重复注册检查配置类是否生效Bean 是否被扫描逻辑删除不生效查询带出已删除数据注解或全局配置没对上实体类字段加 TableLogic或 yml 配置 logic-delete-field 并保持名字一致时间字段为 null未开启驼峰映射配置 map-underscore-to-camel-case: true或 TableField 显式指定字段名其中Invalid bound statement是最容易误判的。有一次我排查了很久最后发现是.jar包里的 XML 不存在。因为本地 IDE 运行时会自动把 resources 目录加入 classpath但用mvn package打出来部署到测试环境后XML 路径错误才暴露。所以集成阶段就应该提前看构建产物里的目录结构别等部署了才开始怀疑人生。判断 XML 是否被打进 jar 里的方法很简单jar tf target/xxx.jar | grep \.xml如果输出为空说明 XML 没被打进包优先查 Maven resources 配置而不是代码逻辑。6.2 我建议一上来就设置好的全局配置最后分享一套可以“无脑拷贝”到新项目的 MyBatis-Plus 初始化配置模板。这些配置不是功能依赖但能帮你省掉后续非常多的手工操作。mybatis-plus: mapper-locations: classpath*:com/example/**/mapper/**/*.xml type-aliases-package: com.example.project.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: banner: false db-config: id-type: assign_id logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0banner: false可以关掉启动时的 MyBatis-Plus 大图标不影响功能但在日志系统里能减少无用输出。id-type: assign_id适合所有主键是雪花 ID 的表。如果你的表用了自增主键需要改成auto。这里有一点比较隐蔽如果一个项目里既有雪花 ID 表又有自增表不要在全局配置写死而是在每个实体类的TableId上单独指定IdType全局配置留给大多数场景。日志打印我建议开发环境一定开启因为 MyBatis-Plus 生成的 SQL 不是那么直观比如 lambda 表达式最终拼出来的 SQL 长什么样你不看日志根本不知道。生产环境可以用org.apache.ibatis.logging.slf4j.Slf4jImpl并用 logback 级别控制。自动填充、逻辑删除的配置我在前面章节讲过这里不再重复。需要提醒的是这两个功能并不是所有项目都需要如果你的表设计中根本没有deleted字段或create_time公共字段就不要强行加这些配置。一次把全局配置配得太花哨新人接手时反而很难理解为什么每个查询都多了一个deleted 0。6.3 两个容易被忽略的“冷门”坑根据我多次从零搭建项目的经验最后再补充两个容易忽略、但遇到就头疼的问题。第一个是LocalDateTime和数据库字段类型的映射。MySQL 5.6 之前的版本对datetime类型的精度支持有限如果你的create_time字段是timestamp并且实体类用的是LocalDateTime在序列化时可能出现Invalid value for getTimestamp这类异常。解决方式是把数据库字段改成datetime并在 JDBC URL 后增加serverTimezoneAsia/Shanghai。这个配置我在前面的 yml 已经写了别删。第二个坑是Jackson对Long类型主键的精度丢失。雪花 ID 是 19 位数字超过 JavaScriptNumber.MAX_SAFE_INTEGER前端接收后精度会丢。要解决很简单在application.yml里加配置jackson: generator: write-numbers-as-strings: true或者给主键字段加注解JsonSerialize(using ToStringSerializer.class)。这个问题不属于 MyBatis-Plus但每次项目一接上就遇到谁用谁知道。另外我个人的习惯是启动后在测试类里写一个contextLoads测试并在里面注入SqlSessionFactory打印sqlSessionFactory.getConfiguration().getMappedStatementNames()这样能第一时间看到 XML 是否成功注册。如果这里没有你的 mapper 方法那后面所有基于该 mapper 的查询都会报Invalid bound statement。这个排查思路比一个个查配置高效得多。最后再分享一个小技巧如果你的 XML 文件比较多而且确实想和 Mapper 接口放在同一个源码目录下与其改 Maven 配置不如直接用 IDEA 的File Watcher插件来做文件同步开发时自动把 XML 复制到 target/classes 对应目录。但团队成员不一定都装了 IDEA 插件考虑到协作稳定我还是推荐把 XML 放在src/main/resources下并保持和接口相同的包路径这是兼容性最好的方式。
