简介:面向Spring Boot与MyBatis-Plus整合开发的Java后端工程师,这份资料包围绕企业级应用常见场景,展示从自动配置、起步依赖,到增强CRUD、分页、乐观锁、逻辑删除等能力的落地方式,适合希望快速搭建数据访问层,或参考微服务拆分实践的开发者。压缩包共2000个文件、约60.87MB;其中Java源文件最多,共1689个,覆盖实体、服务、控制器及Mapper接口,146个XML文件多用于MyBatis映射与SQL定义,99个Shell脚本可辅助启动、打包或环境初始化,另有YAML配置、factories配置和少量Markdown文档,目录中还可看到公共配置与开发环境等模块化划分。目前已有434人学习,通过整套工程结构和典型配置,读者可以获得可直接参考的Spring Boot+MyBatis-Plus基础工程模板,理解多环境配置与数据访问层组织的常见手法。同时,脚本与文档也能帮助加快本地环境准备和二次开发,尤其适合初学者对照学习,或团队将其作为内部项目脚手架。 写Spring Boot后端的人,十有八九最后都会在数据访问层跟MyBatis-Plus打上照面。这个组合火到什么程度?热搜词里从"springboot版本太高"到"事务失效场景",从"自动装配原理"到"循环依赖",每一个都是实际项目里大家真金白银踩出来的问题。我自己的经验是,只要项目里用了Spring Boot + MyBatis-Plus,前面几个接口写得确实爽,但一旦业务复杂起来,条件构造器滥用、插件配置没生效、事务莫名回滚,各种问题就全来了。这篇东西我不打算写成一个面面俱到的官方文档,而是想从选型逻辑、环境版本、核心用法、插件机制、复杂SQL处理,再到事务和性能这些真实场景,把最容易被卡住的地方全部过一遍。
1. 为什么Spring Boot项目里的数据层,我首选MyBatis-Plus
先说个背景。Spring Boot本身解决的是框架集成和自动配置的问题,它帮你把Tomcat、Jackson、数据源这些都收拾利索了,但数据访问层到底用哪套方案,它不管。于是每个团队都得自己选:JPA、原生MyBatis、MyBatis-Plus、jOOQ,甚至直接JdbcTemplate。我见过不少团队在这个选项上反复横跳,最后又跳回MyBatis-Plus,原因其实很朴素——它把单表CRUD里那部分毫无技术含量的重复劳动直接消灭了。
如果你写过一个基于原生MyBatis的项目,你一定记得那种感觉:一张表对应一个实体类、一个Mapper接口、一个XML文件,然后里面全是insert、update、deleteById、selectById这种近乎一样的SQL。数据表一多,写这些东西纯粹是体力活,而且很容易出低级错误,比如字段名写错、参数没对应上。MyBatis-Plus做的事情,就是把这些通用操作全部内置到BaseMapper里。你的Mapper接口只要继承它,单表的增删改查、批量插入、条件查询全都齐了。
很多人以为MyBatis-Plus是MyBatis的替代品,其实它只是MyBatis的增强插件。它没有改掉MyBatis的任何底层行为,SQL会话、参数映射、结果集映射这些依然走的是MyBatis那套机制。所以你在原MyBatis里积累的XML写法、动态SQL经验,在这里依然全部有效。这也是它比JPA更容易被Java后端团队接受的深层原因——学习曲线几乎是平的,老代码不用推翻重来。
还有个我不太想承认但确实是现实的因素:招聘市场上,会MyBatis-Plus的候选人明显比熟悉JPA的人多。一个新项目交到团队手里,用MyBatis-Plus意味着所有人都能快速上手,不至于出现"这个配置只有某某人看得懂"的局面。从项目长期维护的角度看,这个选择比技术上的微末优劣更重要。
2. 版本与依赖:启动前最容易被卡住的一环
标题里的热搜词第一个就是"springboot版本太高",这真不是段子。Spring Boot 3.0发布之后,很多新项目直接就是3.x起步,然后发现MyBatis-Plus启动报错、Mapper扫描不到,折腾半天才意识到是版本不匹配。
先说结论:如果你用的是Spring Boot 3.x,那么JDK必须17以上,MyBatis-Plus这边不能用传统那个mybatis-plus-boot-starter,而是要引入mybatis-plus-spring-boot3-starter,版本号建议在3.5.5以上。如果是Spring Boot 2.7.x这种还在用JDK 8的项目,那用mybatis-plus-boot-starter的3.5.x版本就行。这个分水岭非常关键,因为两个starter的包名、自动配置类路径都不一样,网上那些老教程很多都是针对Spring Boot 2写的,直接抄过来在3.x上是跑不起来的。
还有一个容易忽略的点,就是版本冲突。MyBatis-Plus自带一个mybatis-spring版本,它和Spring Boot 3.2以后内置的mybatis-spring版本可能会打架。我遇到过的情况是启动时提示Error creating bean with name 'sqlSessionFactory',排查到最后发现是mybatis-spring的版本被Maven的依赖调解机制给覆盖了。解决办法很直接,在pom里显式声明一个跟当前Spring Boot版本匹配的mybatis-spring,或者干脆用MyBatis-Plus官方提供的BOM来统一版本。
依赖这块我贴一个当前比较稳妥的配置供参考,基于Spring Boot 2.7.18加MyBatis-Plus 3.5.7的组合,我实测在多个生产项目里都跑得比较稳:
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.7</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency>如果你打算上Spring Boot 3.2以上的版本,就把第一行的artifactId换成mybatis-plus-spring-boot3-starter。光看这个名字就知道,官方也有点被版本分裂搞烦了,索性直接拆成两个starter来维护。所以你的第一件事不是写代码,而是确认好你手上的Spring Boot版本和JDK版本,再决定用哪个依赖。这一步错了,后面全是白忙。
3. 从配置到CRUD:让第一个接口跑起来
版本理清之后,实际开发中的第一步就是配置。MyBatis-Plus的配置项不算多,但有几个别疏忽。最基础的是数据源配置,这没什么好说的,就是Spring Boot标准的DataSource配置。然后要留心的是Mapper扫描。Spring Boot主类上要加@MapperScan注解,否则你的Mapper接口不会被注册成Bean。很多人第一次跑项目报Invalid bound statement (not found),八成就是这里漏了。
再说实体类。MyBatis-Plus约定表名和字段名默认采用驼峰转下划线映射,比如实体类里的createTime会自动映射到数据库字段create_time,这个行为与MyBatis原生配置里的map-underscore-to-camel-case是一致的,默认开启的。但主键那条不能偷懒,建议显式写上@TableId(type = IdType.ASSIGN_ID)。我为什么强调这个?因为默认的ASSIGN_ID会生成一套雪花ID,对于大部分分布式场景是好事,但如果你的项目主键是数据库自增的,不改成AUTO的话,插入之后主键不会回填,后面要用到主键做关联操作时就会拿到一个空值或者一个奇怪的字符串。这个坑真的很隐蔽,我在代码评审里见过好几次。
基础CRUD的写法我就不逐条演示了,核心就是你的Mapper接口继承BaseMapper<T>,你的Service接口继承IService<T>,实现类继承ServiceImpl<M, T>。这套组合下来,单表的基础操作不需要写一行SQL。但我想特别提一下ServiceImpl里的saveOrUpdate方法,它的逻辑是先判断主键是否存在,存在就更新、不存在就插入。听起来很省事,但如果你没做好并发控制,在高并发下可能出现重复插入或者覆盖更新的问题。所以这个方法适合用在后台管理这种低并发场景,核心交易链路里还是老老实实先查询再决定。
还有一件容易被忽略的事就是分页插件。MyBatis-Plus的分页不是开箱即用的,你必须手动注册一个PaginationInnerInterceptor。我在项目里见过有人写好了分页代码但怎么都不生效,查了半天发现是用户返回了全部数据——因为拦截器没注册。注册方式一般是搞一个MybatisPlusInterceptor的Bean,再把分页拦截器加进去:
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }DbType根据你的数据库类型来,MySQL和PostgreSQL的方言不一样,填错了分页SQL生成也不对。这个配置属于"一次配置,全局受益"的类型,早配早安心。
4. 条件构造器与代码生成:单表开发的效率密码
CRUD基础能力有了之后,真正让开发效率质变的是条件构造器。QueryWrapper和LambdaQueryWrapper几乎是每个项目里出现频率最高的类。它们的核心价值是让你用面向对象的方式拼查询条件,不用手写字符串SQL。
我自己的习惯是能用Lambda就尽量不用字符串版QueryWrapper。原因很简单——Lambda版在编译期就能检查字段名是否正确,比如lambdaQuery().eq(User::getStatus, 1),如果你把getStatus写错了,IDE直接给你标红。而字符串版queryWrapper.eq("status", 1)写错了只能等运行时报错。团队协作时,这种细微差异能省掉不少沟通成本。
条件构造器里我重点说两个容易用错的地方。第一个是and和or的嵌套。很多人写多条件查询时直接eq(...).or().eq(...),然后发现SQL逻辑完全不对。原因在于链式调用天然是一个"从左到右"的平铺结构,没有括号分组概念。如果你要表达的其实是a AND (b OR c),在MyBatis-Plus里不能用简单的连链,而是要嵌套:
LambdaQueryWrapper<Order> wrapper = Wrappers.lambdaQuery(); wrapper.eq(Order::getUserId, 1001) .and(w -> w.eq(Order::getStatus, 1) .or() .eq(Order::getStatus, 2));第二个是条件判断的写法。我见过大量代码里这么写:
if (StringUtils.isNotBlank(name)) { wrapper.like(User::getName, name); }这种写法没错,但代码不简洁。更推荐的做法是使用like方法提供的一个重载:
wrapper.like(StringUtils.isNotBlank(name), User::getName, name);第一个参数是boolean condition,为false时这个条件就不拼接。这样能把if判断全部压缩进链式调用里,代码看起来清爽很多,也减少了漏写if导致空条件拼接的问题。
说完条件构造器,就不得不提代码生成器。MyBatis-Plus的AutoGenerator让你连接数据库之后,直接把实体类、Mapper、Service、Controller全部生成出来。这个工具挺香,但我有个劝告:生成完之后,一定要手工过一遍生成的代码,尤其是Controller和Service实现类——因为它是按固定模板生成的,命名规范、业务逻辑跟你的项目实际情况往往有出入。代码生成器适合用来"建骨架",不适合"替代思考"。我习惯用它的方式是把生成的代码直接扔进项目里当初始版本,然后在此基础上改逻辑,而不是指望它一次到位。
5. 插件机制实战:多租户、乐观锁、逻辑删除
MyBatis-Plus能在企业项目里站稳脚跟,很大程度靠的是它几个常用插件。我按真实项目里的使用频率排个序:逻辑删除、乐观锁、字段自动填充、多租户隔离。这几个插件如果配置得当,能省掉大量手写SQL和重复逻辑。
逻辑删除是最常被团队启用的插件。只要在实体类的删除标记字段上标@TableLogic,MyBatis-Plus就会自动把所有delete操作变成update语句,把deleted字段置为1。这个机制在数据审计和误删恢复场景下非常好用,但要注意两个问题。第一个是全局唯一索引的坑:比如user表里有个phone字段设了唯一索引,用户删除后再注册一个相同手机号的用户,因为旧记录只是打标没真删,唯一索引就冲突了。第二个是查询条件:所有Mapper的查询都会自动带上deleted=0,这是好事,但如果你写自定义SQL时没用MyBatis-Plus的API,而是直接在XML里写了select * from user,那这个自动过滤是不会生效的,会查出已删除的数据。
乐观锁插件也是企业项目里的常客。它通过版本号机制解决并发更新覆盖问题,思路很简单:更新前检查version是否和当前读取到的一致,一致才更新并version+1,不一致就更新失败。MyBatis-Plus里的做法是给实体类加一个@Version字段,然后注册OptimisticLockerInnerInterceptor。这里有个我测试过的细节:并不是所有update方法都能触发乐观锁。updateById可以,但如果你用update(entity, wrapper)这种构造,需要确保wrapper里没有把version字段当作更新条件,否则可能更新不到。还有,update语句里的set部分必须带上version字段才能生效,MyBatis-Plus的拦截器实际是在set后面追加了version=version+1,并在where里带上旧版本号。
字段自动填充是另一个比较实用的插件。创建时间、更新时间这类字段,很多表都会有。标准做法是继承MetaObjectHandler,实现insertFill和updateFill两个方法,在实体类的对应字段上标@TableField(fill = FieldFill.INSERT)或INSERT_UPDATE。这个功能做好之后,你再也不用在每次插入和更新时手动set时间了,而且可以顺便把创建人、更新人这些审计信息一起填上,对权限审计很有帮助。
多租户插件(TenantLineInnerInterceptor)适合SaaS系统。它的原理是在SQL执行前自动帮你在where后面追加tenant_id = 当前租户值。这个插件很强大,但也危险——如果配置了全局生效,那么一些不需要租户隔离的表(比如字典表、配置表)也会被强行加上条件,导致查不到数据。解决方法是配置ignoreTable列表,或者让指定Mapper跳过租户解析。我第一次用这个插件的时候就翻过车,一张系统配置表被加了tenant_id条件,查出来全是空,排查了好久才反应过来。所以启用多租户插件之前,务必把表清单拉出来,逐个确认哪些表需要隔离。
6. 复杂SQL场景:MyBatis-Plus与XML的正确协作
MyBatis-Plus再强,它也解决不了复杂的多表关联查询。我的经验是:单表操作用它,多表操作直接XML。两者完全可以共存于同一个项目,因为我前面提过,MyBatis-Plus底层就是MyBatis,XML mapper机制原样保留。
具体操作起来也很简单。Mapper接口里定义方法,然后在同包或resources里的mapper目录写同名XML,namespace指向Mapper接口全限定名。比如你要做一个订单和用户表的join查询,只要在XML里写custom的select语句,然后Mapper接口加一个方法就行。MyBatis-Plus的BaseMapper提供的那些方法完全不受影响。
但这里有个小问题:自定义SQL里怎么用MyBatis-Plus的分页?很多人在这卡住。其实非常简单,方法签名里直接加一个IPage<T>类型的第一参数,然后在XML里正常写select语句,不用手动写limit。分页拦截器会拦截这个方法,自动把limit和count语句生成出来。示例:
IPage<OrderVO> selectOrderPage(Page<OrderVO> page, @Param("userId") Long userId);<select id="selectOrderPage" resultType="com.example.vo.OrderVO"> SELECT o.*, u.name AS userName FROM t_order o LEFT JOIN t_user u ON o.user_id = u.id WHERE o.user_id = #{userId} </select>还有一个容易踩的细节:自定义SQL如果涉及逻辑删除字段,MyBatis-Plus的@TableLogic自动过滤不会生效。比如你写了一条SELECT * FROM t_order WHERE o.user_id = #{userId},那么已删除的订单会被查出来。这时候你得在XML里手动加o.deleted = 0,或者用MyBatis-Plus提供的@InterceptorIgnore注解忽略某些自动功能。我的习惯是XML里的自定义SQL全部手工处理删除标记,不依赖插件,因为插件生成的条件在复杂查询里常常会跟你自己写的where条件产生语义冲突。
再补充一个我在代码评审中反复强调的点:不要在一个方法里面既用QueryWrapper又用XML去查同一张表。如果有两张同类查询,优先合并成一条XML SQL。因为条件构造器太灵活,容易让查询逻辑散落在Service代码里,一旦SQL需要优化,你得把Java代码翻个底朝天才能拼出完整的SQL。XML虽然看起来笨重,但所有SQL一览无余,DBA做优化时也方便。
7. 事务失效与性能优化:线上踩坑复盘
最后这部分是实打实的经验教训,全都是我在项目里遇到并排查过的。先用热搜词里的"事务失效场景"开头。MyBatis-Plus本身不感知事务,它用的是Spring的@Transactional。事务失效最常见的几种情况我在这个话题上必须提醒:
第一种是自调用失效。同一个类里的一个方法调用了另一个带@Transactional的方法,事务是不生效的。因为Spring的事务是基于AOP动态代理实现的,自调用走的是this调用,绕过代理,事务自然就没挂上。解决办法是把调用拆到另一个Bean里,或者用AopContext.currentProxy()。
第二种是方法非public。Spring默认只对public方法做事务代理,private方法上的@Transactional是静默忽略的。这个行为很多新手不知道,代码写了一堆,事务一个没生效,查起问题来非常迷惑。
第三种是异常被吞了。@Transactional默认只在RuntimeException和Error时回滚,受检异常不会触发回滚。如果你在事务方法里catch了异常,什么都没往外抛,事务会正常提交——"try-catch吞异常导致数据不一致"就是这么来的。正确做法是让方法把异常抛出去,或者指定rollbackFor = Exception.class。
再说性能问题。MyBatis-Plus方便是真的方便,但滥用条件构造器和自动生成的批量方法,容易产生慢SQL。我遇到过一个性能事故:一个定时任务用insertBatch逐批插入几千条数据,每批100条,结果一次跑下来要好几分钟。问题不在于MyBatis-Plus的批量方法本身低效,而是它的批量插入实际是循环执行单条insert,没有走JDBC的rewriteBatchedStatements优化。后来我改成了手写XML的foreach批量插入,耗时直接降了一个数量级。
还有一个很隐蔽的性能问题是分页count。当你的查询条件比较复杂、涉及多表join时,MyBatis-Plus自动帮你生成的count语句可能不是最优的。比如你明明只需要统计主表记录数,它却把两个大表join完再count,这就会很慢。解决办法是用Page的setOptimizeCountSql或直接在XML里定义Count查询,让统计SQL走你指定的路径。另外,PaginationInnerInterceptor内部对count查询会尝试去掉不必要的order by,但not优化所有场景,所以复杂统计还是自己动手写比较靠谱。
逻辑删除和性能也有一层关系。当你用@TableLogic时,所有查询都会带上deleted=0条件,如果表数据量大,这个条件又用不上索引,全表扫描就来了。建议在deleted字段上建索引,尤其是那些会频繁做逻辑删除和查询的表。
最后说一句代码生成器和通用Mapper的"度"。MyBatis-Plus的强大之处在于让简单的事情变简单,但并不意味着所有数据访问都该用它兜底。我的原则是:单表、简单查询、CRUD操作用MyBatis-Plus;多表join、复杂聚合、需要精细控制SQL的业务,一律XML手写。这个边界分清楚之后,项目既高效又可控,不会出现"MyBatis-Plus写的SQL看不懂优化不了"的局面。上面的这些坑,我基本全都踩过一遍,写出来也是希望大家能少走点弯路,真遇到了问题也知道往哪个方向查。
本文还有配套的精品资源,点击获取