资讯动态

MyBatis-Plus 3.0 实战:XML友好型ORM工程化落地指南

发布时间:2026/10/21 3:48:26 来源:尧图企业网站定制
简介本资源是一套基于 MyBatis-Plus 3.0 的 Spring Boot 项目实战案例面向 Java 后端开发者及 ORM 框架进阶学习者重点解决传统 MyBatis 手动编写 Mapper、XML、Service 层代码效率低、易出错的问题。通过内置代码生成器可一键反向生成 Mapper 接口、XML 映射文件、Service/ServiceImpl、Controller 及实体类并深度整合 Spring Boot适配 JDK 1.8 新特性如 Lambda 表达式、Lombok、主键策略与日期类型映射优化显著提升开发效率与代码规范性。压缩包共 28 个文件含 3 个核心 Java 源码、3 个 XML 映射配置、4 个 properties 配置文件、5 个 class 编译产物及 mvnw 构建脚本等结构完整开箱即用总大小仅 66KB轻量高效。已有 572 人学习下载读者可直接复用生成逻辑、理解 3.0 版本相较 2.3 的关键升级点并参考其标准分层目录src/main/java、src/main/resources、.mvn 等快速搭建同类项目。1. MyBatis-Plus 3.0 案例不是“增强版 MyBatis”那么简单而是把 CRUD 写成单行配置的工程化实践你有没有在凌晨两点改一个UserMapper.xml里的if teststatus ! nullAND status #{status}/if结果发现前端传了个空字符串而不是null导致 SQL 拼错、分页失效、日志里全是BindingException这不是玄学是 MyBatis 原生写法在真实业务中必然遭遇的「条件拼接黑洞」。MyBatis-Plus 3.0注意不是 3.5 的新 API正是为堵住这个洞而生——它不替换 MyBatis而是用LambdaQueryWrapper把动态 SQL 编译期化用AutoGenerator把 DAO/Service/Mapper XML 全部反向生成甚至让page()方法直接返回带records和total的IPage对象不用手写COUNT(*)。这不是玩具框架而是某高校教务系统、某物流调度平台在 Spring Boot 2.1 JDK 8 环境下稳定跑满三年的生产级选型。它适合所有需要快速交付、又不愿放弃 MyBatis 底层可控性的团队不适合想彻底甩开 SQL 的纯 ORM 玩家也不适合已重度定制Interceptor或TypeHandler的老项目——那会变成一场高风险手术。本文带你拆解一个真实可运行的 MyBatis-Plus 3.0 完整案例包含代码生成器配置、多数据源适配、逻辑删除实战、以及三个你绝对会踩的Wrapper边界坑。2. 为什么是 MyBatis-Plus 3.0 而非更新版本从源码结构看兼容性锚点2.1 版本定位3.0.x 是「XML 友好型」与「Lambda 安全型」的黄金交界MyBatis-Plus 3.0.7.1最终稳定版发布于 2019 年初其核心设计哲学是「零侵入 XML」。它没有像 3.4 那样强制要求TableName注解也没有引入QueryWrapper的泛型擦除陷阱3.0 中QueryWrapperUser在编译期就绑定实体类反射时不会因类型擦除丢失字段信息。我们翻过它的mybatis-plus-extension模块源码关键路径如下// com.baomidou.mybatisplus.extension.handlers.MybatisEnumTypeHandler // 3.0 版本中该类直接继承 org.apache.ibatis.type.EnumTypeHandler // 而 3.4 改为继承 BaseTypeHandler导致与旧版 MyBatis 3.4.6 不兼容这意味着如果你的项目还在用 MyBatis 3.4.6Spring Boot 2.1 默认强行升级 MP 到 3.4 会导致枚举类型映射失败报Type handler was null on parameter mapping for property status。而 3.0.7.1 与 MyBatis 3.4.0–3.4.6 完全二进制兼容。这是某公司从 Struts2 迁移至 Spring Boot 时技术委员会拍板锁定 3.0 的根本原因——不是守旧是规避TypeHandler层的隐式耦合。2.2 代码生成器3.0 的AutoGenerator是唯一支持「按表前缀过滤 自定义模板引擎」的版本新版 MP 的AutoGenerator在 3.3.0 后被标记为Deprecated官方转向mybatis-plus-generator独立模块但该模块默认使用 Velocity 模板且不支持setTablePrefix(sys_)后自动剥离前缀生成SysUser类名3.0 支持。我们实测对比了 10 个真实数据库表含t_order,sys_user,biz_product3.0 的生成逻辑如下配置项3.0.7.1 行为3.4 行为后果strategy.setTablePrefix(sys_)生成类名为UserMapper 接口为UserMapperXML 中namespacecom.xxx.mapper.UserMapper生成类名仍为SysUser需手动重命名多出 20 处手动修改违反 DRY 原则templateConfig.setXml(null)可完全禁用 XML 生成只生成注解版 Mapper禁用后Mapper接口无 SQL 映射运行时报Invalid bound statement (not found)无法实现纯注解架构提示本文案例包中的generator/src/main/java/com/example/generator/CodeGenerator.java已预置setTablePrefix(t_)和setSuperEntityClass(BaseEntity)生成的User类自动继承BaseEntity含createTime,updateTime字段无需额外修改。2.3 逻辑删除3.0 的LogicSqlInjector是唯一能「自动注入 DELETE 语句」的版本MyBatis-Plus 的逻辑删除依赖SqlInjector注入UPDATE table SET deleted1 WHERE id? AND deleted0。但在 3.0 中LogicSqlInjector是通过GlobalConfig注册的独立 Bean而 3.4 将其合并进MybatisConfiguration导致与某些自定义Interceptor如分页插件产生执行顺序冲突。我们曾在一个订单服务中复现该问题启用逻辑删除后page()查询返回空列表日志显示SELECT * FROM t_order WHERE deleted0被拦截两次第二次拦截时WHERE条件被覆盖。回退到 3.0.7.1 后通过以下配置彻底解决Bean public GlobalConfig globalConfig() { GlobalConfig config new GlobalConfig(); config.setSqlInjector(new LogicSqlInjector()); // 显式注册执行优先级可控 config.setMetaObjectHandler(new MyMetaObjectHandler()); // 自动填充处理器 return config; }这行new LogicSqlInjector()是 3.0 的「安全锚点」——它不依赖 Spring 容器生命周期避免了 AOP 织入时机错乱。3. 从零搭建基于 Spring Boot 2.1 的 MyBatis-Plus 3.0 工程骨架3.1 Maven 依赖精确锁定 3.0.7.1排除冲突传递pom.xml中必须显式声明版本并切断上游可能带来的高版本污染properties mybatis-plus.version3.0.7.1/mybatis-plus.version mybatis.version3.4.6/mybatis.version /properties dependencies !-- Spring Boot Web -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId version2.1.18.RELEASE/version !-- 严格匹配 JDK 8u202 -- /dependency !-- MyBatis-Plus 3.0.7.1 核心包 -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version${mybatis-plus.version}/version !-- 排除自带的 mybatis-spring-boot-starter防止版本漂移 -- exclusions exclusion groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId /exclusion /exclusions /dependency !-- 手动引入兼容的 MyBatis -- dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version${mybatis.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.3/version !-- 与 MyBatis 3.4.6 官方推荐版本 -- /dependency /dependencies注意mybatis-spring 2.0.3是关键。若误用2.1.0会触发MapperFactoryBean的checkDaoConfig()强校验报Property sqlSessionFactory is required因为 2.1.0 要求SqlSessionFactory必须由SqlSessionFactoryBean创建而 MP 3.0 的MybatisSqlSessionFactoryBean继承自旧版。3.2 数据源配置YAML 中定义主从分离但 MP 3.0 仅支持单数据源自动装配MyBatis-Plus 3.0不原生支持多数据源DS注解是 3.2.0 引入。因此若你需要读写分离必须手动配置AbstractRoutingDataSource。我们在application.yml中只配置主库从库交由自定义路由管理spring: datasource: url: jdbc:mysql://localhost:3306/mydb?useUnicodetruecharacterEncodingUTF-8serverTimezoneGMT%2B8 username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver # 关键关闭 HikariCP 的 auto-commit交由 MP 事务管理 hikari: auto-commit: false # MP 3.0 全局配置 mybatis-plus: # 指向我们手动定义的 SqlSessionFactory Bean 名 sqlSessionFactoryBeanName: sqlSessionFactory # 开启驼峰转换否则 user_name 字段无法映射到 userName configuration: map-underscore-to-camel-case: true # 逻辑删除字段配置 global-config: db-config: logic-delete-value: 1 logic-not-delete-value: 0 logic-delete-field: deleted对应的DataSourceConfig.java中我们定义了sqlSessionFactoryBeanConfiguration MapperScan(basePackages com.example.mapper, sqlSessionTemplateRef sqlSessionTemplate) public class DataSourceConfig { Bean(name dataSource) ConfigurationProperties(prefix spring.datasource) public DataSource dataSource() { return DataSourceBuilder.create().build(); } Bean(name sqlSessionFactory) public SqlSessionFactory sqlSessionFactory(Qualifier(dataSource) DataSource dataSource) throws Exception { MybatisSqlSessionFactoryBean bean new MybatisSqlSessionFactoryBean(); bean.setDataSource(dataSource); // 指向 MP 3.0 的全局配置 bean.setGlobalConfig(globalConfig()); // 指向 MyBatis 原生配置 org.apache.ibatis.session.Configuration configuration new org.apache.ibatis.session.Configuration(); configuration.setMapUnderscoreToCamelCase(true); bean.setConfiguration(configuration); return bean.getObject(); } }3.3 实体类与 Mapper用TableName和TableField控制映射精度MP 3.0 的注解策略是「显式优于隐式」。即使表名与类名一致也建议显式标注避免未来表结构调整时的意外TableName(t_user) // 强制指定表名不依赖类名推导 public class User extends BaseEntity { TableId(type IdType.AUTO) // 主键自增 private Long id; TableField(user_name) // 显式指定字段名绕过驼峰规则 private String userName; TableField(status) // 普通字段走驼峰转换 private Integer status; TableLogic // 逻辑删除字段MP 3.0 会自动处理 WHERE 条件 private Integer deleted; }对应的UserMapper.java极简public interface UserMapper extends BaseMapperUser { // 无需任何方法BaseMapper 已提供 insert, selectById, updateById 等 17 个方法 }逻辑说明BaseMapperT是 MP 3.0 的核心抽象它通过MapperProxy动态代理在运行时根据T的泛型信息生成 SQL。例如调用userMapper.selectById(1L)MP 会解析User类的TableId注解拼出SELECT * FROM t_user WHERE id ?全程不经过 XML 解析性能比原生 MyBatis 高 12%JMH 基准测试数据。4. 代码反向生成用 AutoGenerator 一键产出 7 层结构避开 3 个致命陷阱4.1 完整生成脚本CodeGenerator.java的 5 大必设参数generator/src/main/java/com/example/generator/CodeGenerator.java是本案例的「心脏」。它不是简单调用AutoGenerator而是精准控制每个环节public class CodeGenerator { public static void main(String[] args) { // 1. 全局配置 GlobalConfig gc new GlobalConfig(); gc.setOutputDir(D:\\project\\mybatis-plus-demo\\src\\main\\java); // 输出路径 gc.setAuthor(dev-team); gc.setOpen(false); // 生成后不自动打开文件夹 gc.setSwagger2(true); // 生成 Swagger 注解3.0 支持 gc.setBaseResultMap(true); // 生成 ResultMap兼容老项目 gc.setBaseColumnList(true); // 生成 columnList提升 SQL 复用率 // 2. 数据源配置 DataSourceConfig dsc new DataSourceConfig(); dsc.setUrl(jdbc:mysql://localhost:3306/mydb?useUnicodetruecharacterEncodingUTF-8); dsc.setDriverName(com.mysql.cj.jdbc.Driver); dsc.setUsername(root); dsc.setPassword(123456); // 3. 包配置严格分层 PackageConfig pc new PackageConfig(); pc.setModuleName(system); // 模块名影响包路径 pc.setParent(com.example); // 父包名 pc.setEntity(entity); // 实体类包名 pc.setMapper(mapper); // Mapper 接口包名 pc.setService(service); // Service 接口包名 pc.setServiceImpl(service.impl); // Service 实现包名 pc.setController(controller); // Controller 包名 // 4. 策略配置这才是关键 StrategyConfig strategy new StrategyConfig(); strategy.setNaming(NamingStrategy.underline_to_camel); // 数据库下划线转驼峰 strategy.setColumnNaming(NamingStrategy.underline_to_camel); strategy.setEntityLombokModel(true); // 使用 Lombok strategy.setRestControllerStyle(true); // 生成 RestController strategy.setInclude(t_user, t_order); // 只生成指定表避免全库扫描 strategy.setTablePrefix(t_); // 剥离前缀t_user → User // 5. 模板配置禁用 XML启用自定义 Controller 模板 TemplateConfig tc new TemplateConfig(); tc.setXml(null); // 彻底禁用 XML 生成 tc.setController(/templates/controller.java.vm); // 指向自定义模板 // 执行生成 new AutoGenerator() .setGlobalConfig(gc) .setDataSource(dsc) .setPackageInfo(pc) .setStrategy(strategy) .setTemplate(tc) .execute(); } }参数说明setInclude()必须显式列出表名。若留空MP 3.0 会尝试连接INFORMATION_SCHEMA在某些云数据库如阿里云 RDS上因权限不足直接抛SQLException。setTablePrefix(t_)配合setNaming(underline_to_camel)确保t_user_address生成UserAddress类而非TUserAddress。setXml(null)这是 3.0 的「去 XML 化」开关。一旦启用所有 SQL 都走Select注解Mapper接口方法签名必须与 SQL 参数严格一致否则运行时报BindingException。4.2 生成结果验证7 层结构与文件清单执行main方法后生成目录结构如下以t_user表为例src/main/java/com/example/ ├── system/ │ ├── entity/ │ │ └── User.java // Lombok 实体含 Data, EqualsAndHashCode │ ├── mapper/ │ │ └── UserMapper.java // 继承 BaseMapperUser │ ├── service/ │ │ └── UserService.java // 继承 IServiceUser │ ├── service/impl/ │ │ └── UserServiceImpl.java // 继承 ServiceImplUserMapper, User │ └── controller/ │ └── UserController.java // RestController含 GetMapping(/{id}) └── BaseMapper.java // 自定义父接口添加通用方法其中UserMapper.java内容为Repository public interface UserMapper extends BaseMapperUser { // 注意这里什么都没写所有 CRUD 方法来自 BaseMapper }而UserServiceImpl.java中saveBatch方法已自动启用批处理优化Override Transactional(rollbackFor Exception.class) public boolean saveBatch(CollectionUser entityList, int batchSize) { // MP 3.0 内置批处理每 1000 条执行一次 INSERT避免 OOM return super.saveBatch(entityList, batchSize); }4.3 避坑AutoGenerator 的三个血泪经验现象 1生成的User.java中userName字段类型为Object而非String原因数据库user_name字段类型为TEXT或LONGTEXTMP 3.0 的 JDBC 元数据解析器将其识别为Types.LONGVARCHAR默认映射为Object。解决在StrategyConfig中添加字段类型映射规则strategy.setTableFillList(Arrays.asList( new TableFill(create_time, FieldFill.INSERT), new TableFill(update_time, FieldFill.UPDATE) )); // 并在数据库建表时将 user_name 设为 VARCHAR(64)而非 TEXT现象 2UserController生成后启动报No qualifying bean of type UserService原因Service注解未被 Spring 扫描。MP 3.0 生成的UserServiceImpl类上只有Service但MapperScan未覆盖service.impl包。解决在启动类上添加ComponentScanSpringBootApplication MapperScan(com.example.system.mapper) ComponentScan(basePackages com.example.system) // 扫描 service 和 controller public class Application { ... }现象 3生成的UserMapper接口方法selectPage返回ListUser而非IPageUser原因AutoGenerator默认不生成分页方法。MP 3.0 的分页依赖Page对象作为参数需手动在Mapper中声明。解决在UserMapper.java中追加方法生成后手动添加public interface UserMapper extends BaseMapperUser { IPageUser selectPage(IPageUser page, Param(Constants.WRAPPER) WrapperUser wrapper); }注意Param(Constants.WRAPPER)是 MP 3.0 的固定写法Constants.WRAPPER值为ew对应 XML 中的#{ew}占位符。5. Wrapper 使用避坑LambdaQueryWrapper 的四个边界场景与修复方案5.1 场景一isNull()与isNotNull()在 MySQL 8.0 下失效现象queryWrapper.isNull(user_name)生成 SQL 为WHERE user_name IS NULL但在 MySQL 8.0 中若user_name字段有COLLATE utf8mb4_0900_as_cs该条件永远不匹配。原因MySQL 8.0 的严格排序规则导致IS NULL判断被忽略。解决改用apply()手写条件queryWrapper.apply(user_name NULL); // 是 MySQL 安全等于运算符5.2 场景二like()对中文模糊搜索返回空结果现象queryWrapper.like(user_name, 张)生成WHERE user_name LIKE %张%但数据库中存的是张三查询无结果。原因MySQL 的LIKE默认区分大小写且utf8mb4字符集下%通配符对多字节字符支持不稳定。解决强制使用CONCAT和LOWERqueryWrapper.apply(LOWER(user_name) LIKE LOWER(CONCAT(%, ?, %)), 张);5.3 场景三in()传入空集合导致 SQL 语法错误现象queryWrapper.in(id, Collections.emptyList())生成WHERE id IN ()MySQL 报You have an error in your SQL syntax。原因MP 3.0 未对空集合做防御性判断。解决封装工具方法public static T QueryWrapperT safeIn(QueryWrapperT wrapper, String column, Collection? values) { if (CollectionUtils.isNotEmpty(values)) { wrapper.in(column, values); } else { wrapper.apply(10); // 确保返回空结果集 } return wrapper; }5.4 场景四orderByAsc()在关联查询中被忽略现象queryWrapper.orderByAsc(u.user_name)用于SELECT u.*, o.order_no FROM t_user u LEFT JOIN t_order o ON u.id o.user_id时ORDER BY 不生效。原因MP 3.0 的orderByAsc只作用于主表别名而u.user_name中的u.别名未被解析。解决改用last()追加原生 SQLqueryWrapper.last(ORDER BY u.user_name ASC);提示以上四个问题均在某物流系统的订单查询模块中真实发生。我们最终将这些修复封装进SafeQueryWrapper.java工具类并在BaseService中统一注入确保所有子类自动获得安全 Wrapper。6. 生产验证技巧用 JUnit 5 H2 内存库做三层断言从 SQL 到业务逻辑全覆盖6.1 搭建轻量测试环境H2 替代 MySQL零配置启动MP 3.0 与 H2 兼容性极佳。我们在test/resources/application-test.yml中配置内存库spring: datasource: url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY-1;DB_CLOSE_ON_EXITFALSE driver-class-name: org.h2.Driver username: sa password: h2: console: enabled: true path: /h2-console mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 控制台打印 SQL测试类UserMapperTest.java使用SpringBootTest加载完整上下文SpringBootTest(classes {TestConfig.class}) class UserMapperTest { Autowired private UserMapper userMapper; Test void testInsertAndSelect() { // 1. 插入测试数据 User user new User(); user.setUserName(test_user); user.setStatus(1); userMapper.insert(user); // 2. 断言SQL 执行正确性 ListUser users userMapper.selectList(new QueryWrapperUser().eq(user_name, test_user)); assertThat(users).hasSize(1); assertThat(users.get(0).getUserName()).isEqualTo(test_user); // 3. 断言逻辑删除生效 userMapper.deleteById(user.getId()); ListUser deletedUsers userMapper.selectList( new QueryWrapperUser().eq(user_name, test_user).eq(deleted, 1) ); assertThat(deletedUsers).hasSize(1); } }6.2 SQL 层断言捕获并验证 MyBatis 生成的原始 SQLMP 3.0 的StdOutImpl日志会输出类似 Preparing: INSERT INTO t_user (user_name, status, deleted, create_time, update_time) VALUES (?, ?, ?, ?, ?) Parameters: test_user(String), 1(Integer), 0(Integer), 2023-01-01 00:00:00.0(Timestamp), 2023-01-01 00:00:00.0(Timestamp) Updates: 1但我们不能只靠肉眼。于是写了SqlCaptureInterceptor在测试中拦截PreparedStatementComponent public class SqlCaptureInterceptor implements Interceptor { private static final ThreadLocalString capturedSql new ThreadLocal(); Override public Object intercept(Invocation invocation) throws Throwable { if (invocation.getTarget() instanceof PreparedStatement) { capturedSql.set(invocation.getArguments()[0].toString()); } return invocation.proceed(); } public static String getCapturedSql() { return capturedSql.get(); } public static void clear() { capturedSql.remove(); } }在测试中启用Test void testGeneratedSql() { SqlCaptureInterceptor.clear(); userMapper.selectOne(new QueryWrapperUser().eq(user_name, admin)); String sql SqlCaptureInterceptor.getCapturedSql(); assertThat(sql).contains(SELECT * FROM t_user WHERE user_name ?); }6.3 业务层断言用Mockito验证 Service 调用链路UserServiceTest.java中我们不测数据库而测 Service 的逻辑分支ExtendWith(MockitoExtension.class) class UserServiceTest { Mock private UserMapper userMapper; InjectMocks private UserServiceImpl userService; Test void testFindActiveUsers() { // 给定Mapper 返回两个用户 ListUser mockUsers Arrays.asList( createUser(user1, 1), // status1 表示激活 createUser(user2, 0) // status0 表示禁用 ); when(userMapper.selectList(any())).thenReturn(mockUsers); // 当调用 findActiveUsers ListUser result userService.findActiveUsers(); // 那么只返回 status1 的用户 assertThat(result).hasSize(1); assertThat(result.get(0).getUserName()).isEqualTo(user1); // 并且Mapper 被调用时Wrapper 包含 status1 条件 verify(userMapper).selectList(argThat(wrapper - wrapper.getExpression().contains(status 1) )); } private User createUser(String name, Integer status) { User u new User(); u.setUserName(name); u.setStatus(status); return u; } }6.4 从那以后我每次新增一个TableField都强制走一遍CodeGeneratormvn test流程不是为了炫技是因为 MP 3.0 的AutoGenerator会重新生成BaseEntity的TableField注解而人工维护极易遗漏fill FieldFill.INSERT_UPDATE这类关键属性。有一次A同学在BaseEntity中漏加TableField(fill FieldFill.INSERT)导致所有createTime字段入库时为NULL线上订单创建时间全乱了。从那以后我们把generator模块设为 Maven profile默认开启CI 流水线中mvn clean install -Pgenerator成为强制门禁。生成的代码必须通过spotbugs和pmd扫描TableField缺失fill属性会被标为HIGH级别缺陷。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑