
简介面向Java开发者的Spring Boot与MyBatis-Plus整合项目资源覆盖企业级Web应用从依赖引入、数据访问层开发到服务拆分的完整实践适合正在学习框架整合或准备搭建实际项目的初中级开发者。压缩包共2000个文件以1689个Java源码为主体附146个XML映射与配置、99个Shell运维脚本、12个YAML环境配置文件以及Spring Boot自动配置相关描述文件整体约60.87MB。目前已有434人学习下载。内容呈现完整工程骨架和模块划分可参考其通用Mapper接口、XML SQL编写、多环境Profile切换、脚本化运维部署等做法同时展示MyBatis-Plus在分页查询、乐观锁、逻辑删除等高频场景下的实现方式。对于需要快速构建数据持久层或向微服务架构演进的团队这份资源提供了清晰可对照的代码组织与配置思路能够减少重复劳动和踩坑时间提升开发效率。1. 从零搭一套能上生产的组合Spring Boot MyBatis-Plus做后端开发的朋友绝大多数都绕不开这套组合。Spring Boot 负责把整个项目的骨架撑起来MyBatis-Plus 负责把数据库操作简化到极致。两个东西单独拎出来都不算难但真正把它们组合好、用到生产环境还不出幺蛾子里面其实藏着不少门道。先说个大多数人的痛点项目是 Spring Boot 2.7.18 MyBatis-Plus 3.5.x跑起来没问题但一遇到 MyBatis-Plus 的 SQL 注入器失效、分页插件不生效、自动填充不触发这类问题排查起来就头大。这篇文章我把我踩过的坑、验证过的配置、以及从开发到部署的全流程都整理出来你可以直接照着抄。需要说明的是这篇文章基于我个人的实际项目经验工具的版本和选择基于常见实践不一定是一成不变的真理但至少能帮你少走很多弯路。2. 搭建与核心配置版本、依赖和 YAML2.1 Spring Boot 版本太高MyBatis-Plus 怎么兼容这是最近被问得最多的问题。很多人一上来就用了 Spring Boot 3.x结果发现 MyBatis-Plus 旧版本压根跑不起来。原因很简单Spring Boot 3.x 基于 Jakarta EE 9包名从javax.*改成了jakarta.*而且要求 JDK 17。如果你是新项目我建议直接上 Spring Boot 3.x MyBatis-Plus 3.5.5 的版本这两个版本是兼容的。用 3.5.5 是因为它对 Spring Boot 3 的支持比较完整分页插件、多租户插件这些都能正常用。如果你手里是老项目Spring Boot 2.x就用 MyBatis-Plus 3.5.3.x 及以下版本。别想着硬升到 3.5.5我实测过 3.5.5 在 Spring Boot 2.7 上虽然能启动但某些注解扫描会出现奇怪的 warning不影响使用但看着心烦。!-- Spring Boot 3.x MyBatis-Plus 3.5.5 -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version3.5.5/version /dependency注意这个 artifact 的差异Spring Boot 3 要用mybatis-plus-spring-boot3-starterSpring Boot 2 用mybatis-plus-boot-starter。这个细节很容易被忽略依赖下载没问题但启动时各种诡异报错就来了。2.2 application.yml 不提示让你的 IDEA 恢复智能很多人新建项目后在 application.yml 里面写spring.datasource.url这类配置时IDEA 完全没有代码提示只能手敲。这个问题说到底不是配置写错了而是 IDEA 没有把 Spring Boot 的配置元数据加载进来。解决办法是给项目加上 spring-boot-configuration-processor 依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-configuration-processor/artifactId optionaltrue/optional /dependency加完之后点一下 Maven 刷新再打开 application.yml你会发现所有配置项都有下拉提示了。这个方法在 IDEA 2022 版本实测有效老版本可能需要重新打开文件才生效。还有一个小坑如果你用了application.yml和application-{profile}.yml多环境配置需要确认 Spring Boot 的 Active Profiles 是否设置正确。IDEA 里直接在运行配置的 Environment variables 里加spring.profiles.activedev比在 yml 里写死要灵活得多。2.3 yml 配置 Map 和 List 的写法MyBatis-Plus 的配置以及自定义配置项经常会用到 Map 类型。比如配置多个数据源的连接参数或者给某个业务模块配置策略参数。# application.yml 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 custom: biz-config: order-status: created: 1 paid: 2 shipped: 3 retry-times: - 1 - 2 - 3对应 Java 配置类ConfigurationProperties(prefix custom.biz-config) Data public class BizConfigProperties { private MapString, Integer orderStatus new HashMap(); private ListInteger retryTimes new ArrayList(); }注意order-status这种带横线的 keyJava 字段名用orderStatus即可Spring Boot 会自动做 relaxed binding。这个机制我一开始不知道当时写 Map 类型的配置key 死活映射不上后来才发现是横线和驼峰的问题。3. 核心实操从 Mapper 到 Service 的全链路3.1 新建过滤器的正确姿势项目里常有鉴权、日志、响应头处理的场景需要自定义过滤器。很多人直接写一个类实现javax.servlet.FilterSpring Boot 3 是jakarta.servlet.Filter然后加Component注解以为就完事了。实际上这样过滤器会被 Spring Boot 自动注册但执行顺序和匹配路径都不好控制有时候还会被 Spring Security 或其他框架的过滤器链干扰。我推荐用FilterRegistrationBean的方式手动注册Configuration public class FilterConfig { Bean public FilterRegistrationBeanCustomAuthFilter authFilterRegistration(CustomAuthFilter filter) { FilterRegistrationBeanCustomAuthFilter registration new FilterRegistrationBean(); registration.setFilter(filter); registration.addUrlPatterns(/api/*); registration.setOrder(1); return registration; } }这样注册的好处是可以通过setOrder精确控制过滤器的执行顺序数字越小越先执行。addUrlPatterns可以只让过滤器作用于特定路径而不是全部请求都过一遍。我用这种方式处理过全链路追踪 ID 的注入效果很稳定。3.2 MyBatis-Plus 分页插件和多租户插件分页是 MyBatis-Plus 使用频率最高的功能之一但分页插件不生效是新手最容易踩的坑。分页插件不生效的典型特征是Page对象作为参数传进去返回的记录不是按分页来的而是全量查询。原因是分页拦截器没有注册到 MyBatis 的拦截器链中。在 Spring Boot 3.x 中正确注册方式如下Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); // 分页插件必须放在多租户插件之后 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }DbType一定要和你实际使用的数据库类型一致。我见过有人在 MySQL 上配了DbType.ORACLE分页 SQL 生成出来是错的查出来的数据直接乱套。多租户插件适合 SaaS 系统每条 SQL 自动追加租户条件。但它有个坑某些场景需要忽略租户条件比如登录时查询用户表。这个时候可以用InterceptorIgnore(tenantLine true)注解标注在 Mapper 方法上或者在调用前用TenantLineInnerInterceptor.ignore(() - {...})包裹。3.3 代码生成器的使用策略MyBatis-Plus 的代码生成器AutoGenerator可以快速生成 Entity、Mapper、Service、Controller 代码但很多人不知道的是新版本中AutoGenerator类被标记为过时官方推荐用FastAutoGenerator。我实际用下来FastAutoGenerator的链式 API 比老版好用得多。FastAutoGenerator.create(jdbc:mysql://localhost:3306/demo, root, password) .globalConfig(builder - builder.author(你的名字).outputDir(System.getProperty(user.dir) /src/main/java)) .packageConfig(builder - builder.parent(com.example.demo)) .strategyConfig(builder - builder.addInclude(sys_user, sys_role)) .execute();我只用它生成 Entity 和 Mapper 接口Service 和 Controller 自己写。原因很简单自动生成的 Service 层太重Javadoc 注释太多而且业务逻辑千篇一律不如自己写来得干净。代码生成器最大的价值是省去 Entity 字段和 Mapper 接口这些无脑代码的时间。3.4 动态 API 数据接口怎么玩有时候业务上需要让用户配置一些数据源和查询语句然后动态生成一个 API 接口出来。这个需求的本质是动态解析 SQL。MyBatis-Plus 的IService接口提供的list、page方法都是静态 SQL无法满足动态条件。我用的方案是在 Mapper 接口中定义一个方法通过Select注解配合${}参数来实现动态 SQL。但Select中不能写script标签以外的东西写得复杂了就会变得很难维护。更优雅的方案是直接用 MyBatis 的SqlSource和SqlSessionpublic interface DynamicMapper { ListMapString, Object executeQuery(Param(sql) String sql); }对应的 XMLselect idexecuteQuery resultTypejava.util.Map ${sql} /select然后通过 Service 层校验 SQL 的合法性、解析参数、执行查询。这里有个红线必须要强调${sql}的方式会直接拼接 SQL存在 SQL 注入风险。动态 API 功能上线前一定要做 SQL 白名单校验至少要把select前缀做强制校验并且加上用户权限控制不然很容易被别人拖库。4. 原理探究自动装配、事务和循环依赖4.1 Spring Boot 自动装配原理一个例子说清楚Spring Boot 自动装配的本质是EnableAutoConfiguration注解。它通过Import(AutoConfigurationImportSelector.class)导入一组配置类而AutoConfigurationImportSelector会读取META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中列出的所有配置类名然后根据ConditionalOnClass、ConditionalOnMissingBean等条件决定是否生效。MyBatis-Plus 的自动装配机制类似。MybatisPlusAutoConfiguration在 classpath 中存在SqlSessionFactory和MybatisPlusInterceptor时自动配置。所以当你的项目中引入了 MyBatis-Plus 依赖但发现它没有生效大概率是依赖缺失导致条件不满足。这个原理在排查问题的时候非常有用。遇到我明明引入了依赖为什么不生效的问题第一步就是打开发布日志搜索ConditionEvaluationReport上面会清清楚楚写着哪些自动配置类被匹配到了、哪些被拒绝了以及原因。4.2 事务失效的 5 个常见场景事务失效是 Spring Boot 项目里非常经典的问题。我整理几个高频场景都是踩过血泪坑总结出来的第一方法内部调用this.xxx()导致事务失效。Spring 的Transactional是基于动态代理的this调用被代理的对象不经过代理事务注解就被绕过了。解决办法是把调用行为拆到另一个 Bean 里或者通过AopContext.currentProxy()获取当前代理对象。第二Transactional和Async在同一方法上失效。Spring 的代理机制会可能只处理一个注解或者因为异步线程不是同一个事务上下文导致事务回滚不了。我的习惯是异步方法只做与事务无关的事情如果需要事务要么在调用方开启事务要么把异步逻辑拆出去。第三异常被吞了。Transactional默认只回滚RuntimeException和Error如果你 catch 住了异常事务自然就不回滚。要么不 catch要么在 catch 后手动TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。第四Transactional标注在 private 方法上。代理机制只能拦截 public 方法private 方法根本没有被代理。第五数据库表存储引擎不支持事务。MySQL 的 MyISAM 引擎不支持事务只有 InnoDB 支持。这个问题在真正上生产之前很难发现本地可能用的都是 InnoDB但迁移到某些云厂商数据库时会被改成 MyISAM事务就静默失效了。4.3 循环依赖什么时候能解决什么时候必须重构Spring Boot 2.6 之前循环依赖默认是允许的通过三级缓存机制可以解决。Spring Boot 2.6 开始默认禁止循环依赖spring.main.allow-circular-references默认改为 false启动时直接报错。这个改动其实是好事逼着你去重构代码结构。循环依赖大多数是设计上的问题常见的规避方式有构造函数注入导致的循环依赖需要重构A 依赖 BB 依赖 A把其中一个依赖改为延迟加载通过ObjectProviderT或Lazy解决。Service 层互相调用导致的循环依赖把公共逻辑抽取到独立的服务中或者通过事件机制解耦。有同事问我能不能直接设置allow-circular-referencestrue让它跑起来我是不建议的。当时项目里有一个循环依赖硬是把配置打开后上线后功能是正常了但每次启动日志里都会有几条循环依赖的警告。排查问题的时候发现两个 Bean 之间确实有隐式的调用链路每次改一个服务的代码都提心吊胆。最终还是花了一天时间把循环依赖拆干净了之后的维护成本下降了一个量级。5. 生产级项目要点文件上传、资源映射和 Docker 部署5.1 如何做大文件的分片上传和下载项目里经常有上传文件的需求几 MB 的直接用 MultipartFile 就搞定了但遇到几百 MB 甚至几个 GB 的文件一次性上传体验非常差还会触发服务器超时。我的做法是分片上传 合并。分片上传的前端逻辑把文件切成 5MB 一片每片单独调用上传接口最后一片传完后调用合并接口。后端的主要逻辑如下PostMapping(/upload/chunk) public Result uploadChunk(RequestParam(file) MultipartFile file, RequestParam(identifier) String identifier, RequestParam(chunkNumber) Integer chunkNumber)在合并时可以直接使用 Java 的文件流进行拼接。这里注意要在临时目录下存储分片并且设置一个定时任务清理过期未合并的分片不然磁盘会被没人要的分片占满。文件上传我一般还会做两层校验一是前端校验文件大小和后缀二是后端校验 MIME 类型。后端校验不能用file.getContentType()因为这个值很容易被伪造要通过读取文件头部的特征字节来判断真实类型。这个方法对图片、PDF 这类格式非常有效但对 ZIP 这类复合格式就不太管用了。5.2 Spring Boot 资源映射如何访问本地磁盘上的文件当你把文件上传到本地磁盘后需要通过 URL 访问这些文件这就涉及资源映射。Spring Boot 默认的静态资源目录是classpath:/static/外部的文件路径默认是访问不到的。解决方案有两种。第一种是配置类实现Configuration public class WebConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadPath); } }第二种是在application.yml中配置spring.web.resources.static-locations。但我更推荐第一种因为配置类更灵活还能同时做 URL 前缀的规则控制。还有一个隐藏思路生产环境强烈建议把这类上传文件放到独立的对象存储OSS、MinIO 等中而不是保存在应用服务器的本地磁盘。原因很简单应用服务器在横向扩容时如果文件分散在不同服务器上用户请求打到不同的机器就会找不到文件。除非你做了共享存储如 NFS否则本地磁盘不是长久之计。5.3 JDK 1.8 打包到 Docker Desktop 的实操很多老项目用的是 JDK 1.8部署的时候喜欢打成 Docker 镜像。Docker Desktop 在 Windows/Mac 上的体验已经很成熟了。有几点值得注意首先镜像基础环境要选对FROM openjdk:8-jdk-alpine WORKDIR /app COPY target/demo.jar app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, app.jar]然后用 Maven 打包注意项目本身的编译环境是 JDK 1.8此时不需要在 Docker 中再安装 Maven直接用宿主机打好的 jar 包就行。但有一个前置条件Docker Desktop 需要在 Dockerfile 中把 Maven 打包和镜像构建分离开避免每次都要在容器里跑一遍 Maven。我推荐的做法是先用 Maven 构建出 jar再单独写 Dockerfile这也符合多阶段构建的理念。启动容器时要注意挂载配置。配置文件如果放在 jar 内每次改配置都要重新打镜像非常痛苦。我通常把配置文件外挂到/app/config目录然后启动命令加上docker run -d -p 8080:8080 \ -e SPRING_PROFILES_ACTIVEprod \ -v /myapp/config:/app/config \ --name demo demo-image这样改配置只需要改宿主机文件重启容器即可不需要重新打镜像。同时日志目录也建议挂载出来方便排查问题。5.4 Spring Boot 请求是单线程还是多线程这个问题经常出现在面试题里。Spring Boot 的请求处理依赖于内置的 Web 容器Tomcat、Jetty 或 Undertow。以 Tomcat 为例它默认维护一个线程池最大线程数默认 200每个请求会从线程池中获取一个线程来处理。所以多个请求是并行处理的但单个请求的生命周期内默认是一个线程从头跑到尾。这带来一个很容易被忽略的坑ThreadLocal的使用。因为线程是复用的线程中用到的ThreadLocal变量如果不主动清理下一个请求可能会读到上一个请求的数据。所以我在做登录用户信息透传时用完ThreadLocal之后必须在 finally 中执行remove()否则在生产环境炸一次就够你忙活半天。还有一点虚拟线程JDK 21 的 Virtual Threads现在成为了新趋势配合 Spring Boot 3.2 可以让server.tomcat.threads.max不用再手动调优。如果你的项目还在 JDK 1.8这个特性暂时用不上但了解这个趋势有助于后续做技术升级规划。6. 高频问题排查实录总结成速查表这里整理几个我在实际项目中踩过、也帮别人排查过的高频问题如果你也遇到类似情况直接对照排查。现象原因解法分页查询返回全量数据分页拦截器未注册或 DbType 配置错误检查 MybatisPlusInterceptor 是否包含 PaginationInnerInterceptorDbType 与数据库类型一致多租户条件下某些表查询不出来没有排除租户字段或者租户条件拼错了用 InterceptorIgnore(tenantLine true) 标注或检查 TenantLineHandler 配置事务不生效方法被 this 调用、异常被吞、private 方法、存储引擎不支持逐步排查代理调用链、异常处理、数据库引擎启动报循环依赖错误Spring Boot 2.6 默认禁止重构循环依赖必要时通过 ObjectProvider 或 Lazy 延迟注入application.yml 无提示缺少配置处理器依赖引入 spring-boot-configuration-processorMyBatis-Plus 3.5.5 在 Spring Boot 2.x 上行为异常版本不匹配Spring Boot 2.x 用 mybatis-plus-boot-starter 3.5.3.x 及以下还有一些细节值得补充多数据源场景下如果使用DS注解切换数据源如 dynamic-datasource-spring-boot-starter注意事务和切换的顺序。先切换数据源再开启事务否则事务可能绑定到主数据源上。MyBatis-Plus 的insert默认不返回主键值如果需要主键可以在实体类主键字段上加TableId(type IdType.AUTO)或者用useGeneratedKeys配置。这在保存后需要拿到主键做关联操作时非常常用。如果你用了逻辑删除注意数据库的唯一索引需要做特殊处理。比如sys_user表的用户名字段有唯一索引做了逻辑删除后删掉一条记录再插入一条同名记录会冲突。解决方案是建立联合唯一索引时把deleted字段也加进去或者使用特殊值如删除时把deleted设置为自增 ID。7. 给还在选型的你几个建议Spring Boot MyBatis-Plus 这套组合放到今天依然是一个非常稳的选择。Spring Boot 本身就是官方主推的快速开发框架生态极其完善遇到任何问题都能在网上找到解决方案。MyBatis-Plus 则帮你把单表 CRUD、分页、逻辑删除这些日常机械操作简化掉让你把精力放在业务上。如果你正在犹豫要不要用这套组合我的建议是中小型项目直接用它不要犹豫。真正到了超大规模互联网场景需要做复杂的多租户、分布式事务、海量数据性能调优的时候可能要考虑更重的 MyBatis纯手写 SQL加上 ShardingSphere 之类的方案。但 90% 的项目实际上用不到那么重的东西。最后分享一个我在实操中的小经验Spring Boot 和 MyBatis-Plus 的组合最关键的并不是把框架跑起来而是团队对框架边界有清晰的认识。什么时候用 MyBatis-Plus 提供的方法什么时候自定义 SQL什么时候上插件这些要有一套约定。我在项目里定了一条规则单表操作允许用 MyBatis-Plus 的ServiceImpl和BaseMapper多表关联查询、复杂条件查询一律写 XML。这套规则执行下来项目代码的维护成本低了很多性能问题也少了很多。如果你刚接触这套组合建议按顺序做三件事第一把官方文档中的 CRUD 接口和条件构造器完整看一遍不用背知道有这些方法就行第二自己动手写一个增删改查页面把分页、逻辑删除、自动填充走一遍第三把自动装配原理搞明白遇到疑难杂症就有一条清晰的排查路径。做完这三件事基本就具备应对日常开发的能力了。本文还有配套的精品资源点击获取