MyBatis与MyBatis Plus混合项目配置实战:解决Mapper扫描冲突与事务管理

📅 2026/8/17 9:49:46
MyBatis与MyBatis Plus混合项目配置实战:解决Mapper扫描冲突与事务管理
1. 项目概述当MyBatis遇上MyBatis Plus在Java后端开发特别是基于Spring Boot的生态里数据持久层框架的选择几乎绕不开MyBatis。它凭借灵活的SQL映射和与原生SQL的紧密结合赢得了大量开发者的青睐。然而随着项目迭代和团队协作的深入我们常常会遇到一个“幸福的烦恼”项目早期可能直接使用了经典的MyBatis编写了大量的XML映射文件和对应的Mapper接口后来为了提升开发效率引入了功能更强大的MyBatis Plus简称MP享受其强大的CRUD封装、条件构造器、分页插件等便利。这就导致了一个项目里既有老旧的纯MyBatis代码也有新写的MyBatis Plus代码两者并存。这种并存状态远不是简单地把两个Jar包扔进pom.xml就能和平共处的。我经历过不止一个项目在引入MP后出现了Mapper扫描冲突、事务管理异常、SQL执行器混乱甚至分页插件完全失效的问题。表面上看项目能启动但一到复杂的业务场景各种诡异的问题就冒出来了。所以今天我想系统性地聊聊“MyBatis和MyBatis Plus并存”这个场景它背后涉及的核心配置原理、常见的坑以及一套经过实战检验的解决方案。无论你是正在做技术栈升级还是接手了一个混合架构的老项目这些经验都能帮你少走弯路。2. 核心冲突原理与架构解析要解决问题首先得理解冲突是怎么产生的。MyBatis Plus虽然名字里带着“Plus”但它并非MyBatis的一个官方扩展包而是一个第三方增强工具。其核心设计是“继承并扩展”MyBatis的核心组件。这就意味着在Spring的IoC容器里MP会尝试去替换或包装一些MyBatis原有的Bean。2.1 关键Bean的接管与冲突最核心的冲突点集中在几个关键的Spring Bean上SqlSessionFactory这是MyBatis的核心负责创建SqlSession。纯MyBatis通常通过Bean方法或SqlSessionFactoryBean来创建。而MP提供了自己的MybatisSqlSessionFactoryBean注意它继承自MyBatis的SqlSessionFactoryBean并会在自动配置中优先使用它。如果项目里同时存在两个SqlSessionFactoryBean的定义Spring会因无法确定注入哪一个而报错。SqlSessionTemplate是SqlSession的线程安全代理。MP同样会提供一个自己的实例。如果配置不当可能导致部分Mapper使用MP的Template另一部分使用原生的从而引发事务不一致等问题。Mapper扫描器 (MapperScannerConfigurer)这是冲突的重灾区。MyBatis-Spring提供了MapperScannerConfigurer来扫描指定包下的接口并注册为Mapper。MP则提供了功能更强的MybatisMapperScannerConfigurer。如果两者都启用且扫描路径有重叠同一个Mapper接口可能会被扫描两次并被注册成两个不同的Bean一个来自原生MyBatis一个来自MP导致BeanDefinitionOverrideExceptionBean定义覆盖异常或者注入时类型冲突。插件 (Interceptor)比如分页插件(PaginationInnerInterceptor)、性能分析插件等。MP通过其MybatisPlusInterceptor统一管理插件。如果你之前为纯MyBatis单独配置了分页插件现在又引入了MP的拦截器可能会造成插件被重复执行或者因为执行顺序问题导致功能异常。2.2 类路径与依赖分析从依赖角度看引入mybatis-plus-boot-starter后它会自动传递引入mybatis-spring-boot-starter和mybatis。这意味着你的项目里其实只有一套MyBatis核心库但有两套“Spring Boot集成”逻辑MyBatis原生的和MP的。Spring Boot的自动配置(AutoConfiguration)会依次生效谁后加载谁的配置就可能覆盖前者。MP的自动配置类通常设计为在MyBatis自动配置之后执行目的就是为了完成上述Bean的替换。理解了这个底层原理我们就能明白所谓“并存”并不是让两套框架独立运行而是要以MyBatis Plus为主导让它来统一管理MyBatis的核心设施同时兼容那些尚未改造的、纯MyBatis风格的Mapper和XML。我们的配置目标就是引导MP正确接管并妥善处理那些“历史遗留”的Mapper。3. 标准配置方案与关键步骤下面我以一个典型的Spring Boot项目为例详细拆解如何正确配置以实现两者和谐共存。假设我们有一个包结构新业务使用MP放在com.example.demo.mp包下老业务使用纯MyBatis放在com.example.demo.mybatis包下。3.1 Maven依赖管理首先确保pom.xml依赖简洁。只引入MP的starter移除原生的mybatis-spring-boot-starter避免依赖冲突和重复的自动配置。dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version最新版本号/version !-- 例如 3.5.6 -- /dependency !-- 移除非必要的 mybatis-spring-boot-starter --3.2 核心配置类实现这是最关键的一步。我们需要通过一个Configuration类显式地配置Mapper扫描明确划清MP Mapper和原生MyBatis Mapper的界限。Configuration MapperScan( basePackages com.example.demo.mp, // MP管理的Mapper包路径 sqlSessionTemplateRef mybatisPlusSqlSessionTemplate // 指向MP的Template ) public class MybatisPlusConfig { /** * 配置MP的SqlSessionFactory。 * 注意这里使用MybatisSqlSessionFactoryBean而不是原生的SqlSessionFactoryBean。 * 它会自动应用MP的各项功能如插件、全局配置。 */ Bean public SqlSessionFactory mybatisPlusSqlSessionFactory(DataSource dataSource) throws Exception { MybatisSqlSessionFactoryBean factoryBean new MybatisSqlSessionFactoryBean(); factoryBean.setDataSource(dataSource); // 设置MP的全局配置如逻辑删除、字段填充等 MybatisConfiguration configuration new MybatisConfiguration(); configuration.setMapUnderscoreToCamelCase(true); // 下划线转驼峰 configuration.setCacheEnabled(true); factoryBean.setConfiguration(configuration); // 添加MP拦截器分页、乐观锁等 MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // 分页 factoryBean.setPlugins(interceptor); // 设置XML映射文件位置MP的XML和原生MyBatis的XML可以放在一起也可分开 factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources(classpath*:mapper/**/*.xml)); return factoryBean.getObject(); } /** * 创建专用于MP的SqlSessionTemplate。 * 指定使用上面的SqlSessionFactory。 */ Bean public SqlSessionTemplate mybatisPlusSqlSessionTemplate(Qualifier(mybatisPlusSqlSessionFactory) SqlSessionFactory sqlSessionFactory) { return new SqlSessionTemplate(sqlSessionFactory); } }3.3 原生MyBatis Mapper的兼容配置对于老的原生MyBatis Mapper我们需要另一个配置类将它们与MP体系隔离开。Configuration // 使用原生的MapperScan并指定一个唯一的sqlSessionTemplateRef MapperScan( basePackages com.example.demo.mybatis, sqlSessionTemplateRef vanillaMybatisSqlSessionTemplate ) public class VanillaMybatisConfig { /** * 为原生MyBatis Mapper创建一个独立的SqlSessionFactory。 * 关键这里使用Spring原生的SqlSessionFactoryBean而不是MP的。 * 同时要设置其Configuration与MP的区分开避免插件冲突。 */ Bean public SqlSessionFactory vanillaMybatisSqlSessionFactory(DataSource dataSource) throws Exception { SqlSessionFactoryBean factoryBean new SqlSessionFactoryBean(); // 注意是原生的Bean factoryBean.setDataSource(dataSource); // 为原生MyBatis创建一个干净的Configuration org.apache.ibatis.session.Configuration configuration new org.apache.ibatis.session.Configuration(); configuration.setMapUnderscoreToCamelCase(true); configuration.setCacheEnabled(true); // 特别注意不要在这里添加MP的MybatisPlusInterceptor // 如果你有为纯MyBatis定制的插件可以在这里addInterceptor // factoryBean.setPlugins(new YourVanillaPlugin()); factoryBean.setConfiguration(configuration); // 可以指定原生MyBatis专用的XML路径也可以和MP共用如果XML写法兼容 factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources(classpath*:mybatis-mapper/**/*.xml)); return factoryBean.getObject(); } /** * 创建专用于原生MyBatis的SqlSessionTemplate。 */ Bean public SqlSessionTemplate vanillaMybatisSqlSessionTemplate(Qualifier(vanillaMybatisSqlSessionFactory) SqlSessionFactory sqlSessionFactory) { return new SqlSessionTemplate(sqlSessionFactory); } }核心要点这个方案的精髓在于隔离。通过两个独立的SqlSessionFactory和SqlSessionTemplate将MP管理的Mapper和原生MyBatis管理的Mapper物理隔离开。它们共用同一个DataSource数据库连接池但在MyBatis会话层面互不干扰。MP的插件不会影响老Mapper老Mapper的复杂XML查询也不会被MP的条件构造器干扰。3.4 application.yml 配置在application.yml中配置可以大幅简化大部分设置已经在上面的Java Config中完成了。只需要保留一些基本设置并关闭MyBatis原生的自动配置防止它干扰。spring: datasource: url: jdbc:mysql://localhost:3306/your_db username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: # 全局配置这里的设置会被MybatisPlusConfig中的覆盖或补充 configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 输出SQL日志调试用 global-config: db-config: logic-delete-field: deleted # 全局逻辑删除字段名如果使用 logic-delete-value: 1 logic-not-delete-value: 0 # 注意这里不再需要配置 mapper-locations因为在Config类中已指定 # mapper-locations: classpath*:mapper/**/*.xml # 可选如果你发现原生MyBatis的自动配置仍在扫描可以显式关闭 # spring.autoconfigure.exclude: org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration4. 典型问题场景与实战解决方案即使配置得当在混合使用的过程中还是会遇到一些具体的问题。下面是我总结的几个高频问题及其解决方法。4.1 Mapper扫描冲突与Bean重复定义问题现象项目启动时报错BeanDefinitionOverrideException提示xxxMapperBean定义被覆盖。根因分析这是最常见的问题。MapperScan被重复执行或者MP和原生MyBatis的扫描器扫到了同一个接口包。解决方案严格包路径隔离如上文配置所示确保MybatisPlusConfig和VanillaMybatisConfig中的basePackages没有重叠。这是最根本的解决办法。检查第三方依赖有些公司内部的公共组件包也可能内置了MapperScan注解。你需要检查所有被引用的Configuration类确保没有隐藏的全局扫描。使用Mapper注解如果无法严格隔离包路径可以在不希望被MP扫描的原生Mapper接口上使用MyBatis原生的Mapper注解并在MP的配置中设置MapperScan的annotationClass属性使其只扫描继承BaseMapper的接口或带有特定注解的接口。4.2 分页插件在原生MyBatis Mapper中失效问题现象在纯MyBatis的Mapper方法中使用RowBounds或分页参数发现分页逻辑没有生效。根因分析MP的分页插件(PaginationInnerInterceptor)是配置在MybatisPlusInterceptor中并只被注册到了mybatisPlusSqlSessionFactory里。而为原生MyBatis创建的vanillaMybatisSqlSessionFactory并没有添加这个拦截器。解决方案方案一推荐统一使用MP分页。即使是在老的原生Mapper接口中也改用MP提供的Page对象进行分页查询。这需要修改老接口的方法签名和XML。例如在Service层使用PageYourEntity page new Page(1, 10);然后将page对象作为参数传入Mapper。MP的分页插件会自动识别并改写SQL。方案二为原生工厂单独添加分页插件。如果老代码无法改动可以在VanillaMybatisConfig的vanillaMybatisSqlSessionFactory方法中手动添加一个纯MyBatis版本的分页插件例如PageHelper但要注意两个分页插件不要混用否则容易混乱。// 在VanillaMybatisConfig中 factoryBean.setPlugins(new PageInterceptor()); // 假设使用PageHelper注意这会使项目依赖两个分页库增加复杂度不推荐。4.3 事务管理不一致问题现象在同一个Transactional方法中分别调用了MP的Mapper方法和原生MyBatis的Mapper方法发现只有一部分操作回滚了。根因分析Spring的事务管理是基于DataSourceTransactionManager和SqlSession的。如果两个Mapper使用了不同的SqlSessionTemplate进而可能使用不同的SqlSession而Spring默认的事务管理器可能只关联了其中一个DataSource虽然数据源相同但会话可能不同。在极端情况下它们可能不在同一个数据库连接上导致无法形成统一的事务。解决方案确保使用同一个SqlSessionFactory这是最彻底的方案。如果业务允许逐步将老的原生Mapper迁移到MP体系下统一使用mybatisPlusSqlSessionFactory。这样所有操作都在同一个MyBatis会话上下文中。验证事务管理器在Spring Boot中默认的DataSourceTransactionManager是与主数据源绑定的。只要两个SqlSessionFactory使用的是同一个DataSource并且没有配置额外的事务管理器理论上事务是统一的。问题更可能出在Transactional的传播行为上。确保你的方法传播级别是REQUIRED默认。进行事务测试编写一个单元测试在一个事务方法内同时调用两种Mapper执行操作然后主动抛出异常检查数据是否全部回滚。这是验证事务一致性的最可靠方法。4.4 实体类注解混淆问题现象老项目中的实体类可能使用了JPA的Table、Column注解或者MyBatis原生的ResultMap。引入MP后MP的注解如TableName、TableField可能不生效或产生冲突。解决方案统一注解对于准备纳入MP管理的实体类逐步将老注解替换为MP的注解。MP的注解功能更强大特别是TableField的fill属性用于自动填充condition属性用于条件构造等。注解共存MP的TableName和JPA的Table理论上可以共存但MP的注解优先级可能更高。为了避免歧义建议清理掉不用的注解。可以使用TableName和TableField为主。使用MP的全局配置对于数据库字段名和实体类属性名的映射如果数据库是下划线风格实体是驼峰风格可以在配置中统一设置mapUnderscoreToCamelCase: true这样可以减少大量TableField注解的使用。5. 高级技巧与最佳实践在解决了基本共存问题后一些高级技巧可以让你在混合架构下游刃有余。5.1 自定义全局配置策略你可以在MybatisPlusConfig中通过MybatisConfiguration和GlobalConfig进行非常精细的控制。例如设置全局的ID生成策略雪花算法、元对象处理器用于自动填充create_time,update_time、逻辑删除处理器等。这些配置只对通过mybatisPlusSqlSessionFactory执行的MP Mapper生效不会影响原生MyBatis Mapper实现了完美的隔离配置。5.2 利用MP的代码生成器逆向改造老表对于老项目表结构已经存在。MP提供了一个强大的代码生成器AutoGenerator你可以用它来为已有的老表快速生成Entity、Mapper接口、Service和Controller代码。生成时可以选择让新的Mapper接口继承BaseMapper从而快速将老表的CRUD操作纳入MP的管理范畴享受条件查询、分页等便利。这是一个渐进式改造的利器。5.3 监控与SQL调优在混合环境下SQL监控尤为重要。可以使用p6spy这类SQL拦截工具或者利用MP自带的性能分析插件有性能损耗建议开发环境使用将所有Mapper无论是MP还是原生执行的SQL及其耗时都打印出来。这样可以帮助你发现N1查询问题、慢SQL以及对比两种方式生成的SQL效率。5.4 关于removeBatchByIds与removeByIds的抉择这是一个来自热搜词的具体问题。在MP的IService接口中removeByIds(Collection? idList)根据ID集合批量删除。removeBatchByIds(Collection? idList)也是批量删除但它在名称上更强调“批量”操作。实际上在MP的默认实现中removeByIds内部就是调用的removeBatchByIds。查看源码可以发现removeBatchByIds是真正执行批量删除的方法而removeByIds是其一个便捷包装。在大多数数据库如MySQL下MP的批量删除默认会生成DELETE FROM table WHERE id IN (?, ?, ...)这样的SQL是一次请求删除多条。选择建议直接使用removeByIds即可因为它语义更清晰是IService的标准API。removeBatchByIds可能用于更底层的定制或某些特定场景。无需过度纠结了解其底层是单条SQL的IN语句而非多次执行即可。6. 迁移路线图与长期规划让两者并存终究是过渡状态。一个健康的项目应该有一个清晰的迁移路线。第一阶段共存与稳定采用本文的隔离配置方案确保现有功能稳定。新功能一律使用MP开发。第二阶段逐步迁移在每次迭代开发或代码重构时选择性地将某个老业务模块的Mapper和XML改造为MP风格。可以利用MP代码生成器重新生成Entity和Mapper接口然后逐步替换老的DAO层调用。这个过程中可以一个Mapper一个Mapper地迁移风险可控。第三阶段统一与收尾当绝大部分核心业务都迁移到MP后可以考虑废弃VanillaMybatisConfig将所有Mapper统一由MP管理。最终清理掉原生的SqlSessionFactoryBean和相关配置实现架构的统一。在整个过程中完善的单元测试和集成测试是安全感的唯一来源。每次修改配置或迁移代码都要跑一遍测试用例确保数据操作的正确性和事务的一致性。从我个人的实践经验来看MyBatis和MyBatis Plus的并存并不可怕可怕的是对底层原理一无所知就胡乱配置。只要理解了MP是对MyBatis核心组件的增强和接管并通过明确的配置进行职责隔离就能搭建一个稳定、高效的混合持久层。这套方案已经在多个中型项目中得到验证平稳支撑了从传统MyBatis到MyBatis Plus的完整过渡期。