MyBatisPlus高级特性与实战技巧全解析

MyBatisPlus高级特性与实战技巧全解析

1. MyBatisPlus隐藏功能全景扫描

作为MyBatis的增强工具,MyBatisPlus在日常开发中确实能极大提升效率。但很多开发者仅仅停留在基础CRUD操作,忽略了框架提供的诸多高级特性。根据官方文档和实际项目经验,我梳理了12个最容易被忽视的实用功能,这些功能在特定场景下能发挥惊人效果。

注意:本文基于MyBatisPlus 3.5.x版本,部分特性在旧版本可能不完全支持。建议先通过mybatis-plus.version确认当前项目使用的版本号。

1.1 动态表名处理器

在多租户系统或分表场景中,动态表名是刚需。MyBatisPlus提供了DynamicTableNameInnerInterceptor拦截器,通过简单配置即可实现:

public class MyTableNameHandler implements TableNameHandler { @Override public String dynamicTableName(String sql, String tableName) { return "t_" + TenantContext.getCurrentTenant() + "_" + tableName; } } // 配置拦截器 @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); DynamicTableNameInnerInterceptor dynamicTableNameInterceptor = new DynamicTableNameInnerInterceptor(); dynamicTableNameInterceptor.setTableNameHandler(new MyTableNameHandler()); interceptor.addInnerInterceptor(dynamicTableNameInterceptor); return interceptor; }

实战技巧

  • 结合ThreadLocal保存租户信息,避免每次请求都传递参数
  • 对于分表场景,可以通过日期或ID哈希计算表名后缀
  • 注意SQL注入风险,动态拼接的表名需要做白名单校验

1.2 自动填充策略进阶用法

大多数项目只用到了基础的@TableField(fill = FieldFill.INSERT),其实填充策略可以更灵活:

public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { // 根据当前字段值决定是否填充 Object status = getFieldValByName("status", metaObject); if(status == null) { this.strictInsertFill(metaObject, "status", String.class, "ACTIVE"); } // 支持SpEL表达式 this.strictInsertFill(metaObject, "createBy", String.class, SecurityUtils.getCurrentUser() + "#" + System.getProperty("user.name")); } }

避坑指南

  • 避免在填充器中进行数据库查询操作,可能导致循环依赖
  • 对于分布式系统,建议填充逻辑幂等且不依赖本地环境变量
  • 使用strictInsertFill代替setFieldValByName可以获得更好的类型安全

2. 拦截器深度应用技巧

2.1 自定义SQL拦截改写

通过InnerInterceptor可以拦截执行的SQL语句进行改写,这在处理遗留系统或特殊需求时非常有用:

public class MySqlInterceptor implements InnerInterceptor { @Override public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { String sql = boundSql.getSql(); // 替换所有is_deleted=0为逻辑删除条件 if(sql.contains("where")) { sql = sql.replace("where", "where is_deleted = 0 and"); } else { sql += " where is_deleted = 0"; } resetSql(ms, boundSql, sql); } private void resetSql(MappedStatement ms, BoundSql boundSql, String newSql) { Field field = ReflectUtil.getField(BoundSql.class, "sql"); ReflectUtil.setFieldValue(boundSql, field, newSql); } }

典型应用场景

  • 强制添加租户隔离条件
  • 统一添加数据权限过滤
  • SQL方言自动转换(如MySQL到Oracle)
  • 敏感字段自动加解密

2.2 执行Mapper方法信息获取

在拦截器中获取当前执行的Mapper接口和方法信息:

public class MyInterceptor implements InnerInterceptor { @Override public void beforeExecute(Executor executor, MappedStatement ms, Object parameter) { String mapperName = ms.getId().substring(0, ms.getId().lastIndexOf(".")); String methodName = ms.getId().substring(ms.getId().lastIndexOf(".") + 1); // 通过反射获取方法注解 Class<?> mapperClass = Class.forName(mapperName); Method method = Arrays.stream(mapperClass.getMethods()) .filter(m -> m.getName().equals(methodName)) .findFirst() .orElse(null); if(method != null && method.isAnnotationPresent(DataAuth.class)) { // 处理数据权限逻辑 } } }

3. 高级查询功能揭秘

3.1 多表关联查询方案

虽然MyBatisPlus主打单表操作,但通过Wrapper也能实现优雅的多表查询:

// 使用QueryWrapper实现联表 List<UserDTO> list = userMapper.selectJoinList( new QueryWrapper<User>() .select("u.*, d.dept_name") .eq("u.status", 1) .like("d.dept_name", "技术") .apply("u.dept_id = d.id") ); // 对应Mapper方法 @Select("select ${ew.sqlSelect} from user u, department d ${ew.customSqlSegment}") List<UserDTO> selectJoinList(@Param(Constants.WRAPPER) Wrapper<User> wrapper);

性能优化建议

  • 对于复杂联表,建议还是使用XML方式编写SQL
  • 可以使用@SqlParser(filter=true)注解跳过SQL解析
  • 大数据量查询时注意关闭MyBatisPlus的自动count优化

3.2 自定义TypeHandler处理JSON字段

处理数据库JSON类型字段时,可以创建通用TypeHandler:

@MappedTypes({Object.class}) @MappedJdbcTypes(JdbcType.VARCHAR) public class JsonTypeHandler extends BaseTypeHandler<Object> { private static final ObjectMapper mapper = new ObjectMapper(); @Override public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) { ps.setString(i, toJson(parameter)); } @Override public Object getNullableResult(ResultSet rs, String columnName) { return parse(rs.getString(columnName)); } private String toJson(Object obj) { try { return mapper.writeValueAsString(obj); } catch (JsonProcessingException e) { throw new RuntimeException(e); } } private Object parse(String json) { try { return mapper.readValue(json, Object.class); } catch (IOException e) { throw new RuntimeException(e); } } } // 实体类使用 @TableName(autoResultMap = true) public class User { @TableField(typeHandler = JsonTypeHandler.class) private Map<String, Object> attributes; }

4. 扩展机制深度应用

4.1 自定义全局方法注入

在BaseMapper基础上扩展通用方法:

public interface MyBaseMapper<T> extends BaseMapper<T> { /** * 批量插入(MySQL语法) */ @Insert("<script>insert into ${tableName} " + "<foreach collection='list' item='item' separator=','>" + "(#{item.name},#{item.age})" + "</foreach></script>") int mysqlBatchInsert(@Param("tableName") String tableName, @Param("list") List<T> list); /** * 根据ID列表查询并返回Map */ @Select("<script>select * from ${tableName} " + "where id in <foreach collection='ids' item='id' " + "open='(' separator=',' close=')'>#{id}</foreach></script>") @MapKey("id") Map<Long, T> selectMapByIds(@Param("tableName") String tableName, @Param("ids") List<Long> ids); } // 使用自定义SQL注入器 public class MySqlInjector extends DefaultSqlInjector { @Override public List<AbstractMethod> getMethodList(Class<?> mapperClass) { List<AbstractMethod> methodList = super.getMethodList(mapperClass); methodList.add(new MysqlBatchInsert()); methodList.add(new SelectMapByIds()); return methodList; } }

4.2 突破单页500条限制

默认情况下,MyBatisPlus会限制单次查询返回500条记录。可以通过以下方式修改:

// 1. 全局配置 @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(); paginationInterceptor.setMaxLimit(1000L); // 修改为1000 interceptor.addInnerInterceptor(paginationInterceptor); return interceptor; } // 2. 单次查询覆盖 Page<User> page = new Page<>(1, 1000); page.setMaxLimit(1000L); userMapper.selectPage(page, queryWrapper);

性能警告

  • 大数据量分页查询应该使用优化方案(如游标分页)
  • 避免在前端一次性请求过多数据
  • 考虑使用@SqlParser(filter=true)跳过count查询

5. 企业级特性实战

5.1 多租户方案完整实现

完整的多租户解决方案需要结合多个组件:

// 租户上下文 public class TenantContext { private static final ThreadLocal<String> CURRENT_TENANT = new ThreadLocal<>(); public static void setCurrentTenant(String tenant) { CURRENT_TENANT.set(tenant); } public static String getCurrentTenant() { return CURRENT_TENANT.get(); } public static void clear() { CURRENT_TENANT.remove(); } } // 租户拦截器 public class TenantInterceptor implements InnerInterceptor { @Override public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { String tenantId = TenantContext.getCurrentTenant(); if(StringUtils.isNotBlank(tenantId)) { String sql = boundSql.getSql(); sql = sql.replace("where", "where tenant_id = '" + tenantId + "' and"); resetSql(ms, boundSql, sql); } } } // 自动填充租户ID public class TenantMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, "tenantId", String.class, TenantContext.getCurrentTenant()); } }

5.2 字段加解密集成

敏感字段自动加解密方案:

public class EncryptTypeHandler extends BaseTypeHandler<String> { private static final String KEY = "your-encryption-key"; @Override public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) { ps.setString(i, encrypt(parameter)); } @Override public String getNullableResult(ResultSet rs, String columnName) { return decrypt(rs.getString(columnName)); } private String encrypt(String content) { // 实现加密逻辑 return "encrypted:" + content; } private String decrypt(String content) { // 实现解密逻辑 return content.replace("encrypted:", ""); } } // 实体类使用 public class User { @TableField(typeHandler = EncryptTypeHandler.class) private String mobile; @TableField(typeHandler = EncryptTypeHandler.class) private String idCard; }

6. 性能优化与监控

6.1 SQL执行监控

通过拦截器实现SQL监控:

public class SqlMonitorInterceptor implements InnerInterceptor { @Override public void beforeExecute(Executor executor, MappedStatement ms, Object parameter) { long start = System.currentTimeMillis(); RequestContextHolder.getRequestAttributes() .setAttribute("sql_start_time", start, RequestAttributes.SCOPE_REQUEST); } @Override public void afterExecute(Executor executor, MappedStatement ms, Object parameter, Object result) { long end = System.currentTimeMillis(); long start = (long) RequestContextHolder.getRequestAttributes() .getAttribute("sql_start_time", RequestAttributes.SCOPE_REQUEST); if(end - start > 1000) { // 慢SQL阈值1秒 log.warn("Slow SQL detected: {}ms - {}", (end - start), ms.getId()); } } }

6.2 二级缓存优化

MyBatisPlus与MyBatis二级缓存整合:

@Configuration public class MybatisPlusCacheConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 必须先添加缓存拦截器 interceptor.addInnerInterceptor(new CachingInnerInterceptor()); // 其他拦截器 interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); return interceptor; } @Bean public CacheKeyGenerator cacheKeyGenerator() { return (target, method, params) -> { StringBuilder key = new StringBuilder(); key.append(target.getClass().getSimpleName()).append(":"); key.append(method.getName()).append(":"); for (Object param : params) { if(param != null) { if(param instanceof BaseQuery) { key.append(((BaseQuery)param).cacheKey()); } else { key.append(param.toString()); } } key.append("|"); } return key.toString(); }; } }

7. 与SpringBoot深度集成

7.1 自动配置扩展

自定义MyBatisPlus自动配置:

@AutoConfigureAfter(MybatisPlusAutoConfiguration.class) public class MyMybatisPlusAutoConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor( List<InnerInterceptor> innerInterceptors) { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); innerInterceptors.forEach(interceptor::addInnerInterceptor); return interceptor; } @Bean public InnerInterceptor tenantInterceptor() { return new TenantInterceptor(); } @Bean public InnerInterceptor sqlMonitorInterceptor() { return new SqlMonitorInterceptor(); } }

7.2 多数据源集成

结合dynamic-datasource实现多数据源:

@Configuration @MapperScan(basePackages = "com.xxx.mapper") public class DataSourceConfig { @Bean @ConfigurationProperties("spring.datasource.druid.master") public DataSource masterDataSource() { return DruidDataSourceBuilder.create().build(); } @Bean @ConfigurationProperties("spring.datasource.druid.slave") public DataSource slaveDataSource() { return DruidDataSourceBuilder.create().build(); } @Bean public DataSource dynamicDataSource() { Map<Object, Object> dataSourceMap = new HashMap<>(); dataSourceMap.put("master", masterDataSource()); dataSourceMap.put("slave", slaveDataSource()); DynamicDataSource dynamicDataSource = new DynamicDataSource(); dynamicDataSource.setDefaultTargetDataSource(masterDataSource()); dynamicDataSource.setTargetDataSources(dataSourceMap); return dynamicDataSource; } @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 注意分页拦截器需要放在最前面 interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); // 多租户拦截器 interceptor.addInnerInterceptor(new TenantInterceptor()); return interceptor; } }

8. 生产环境最佳实践

8.1 代码生成器定制

定制化代码生成器模板:

public class MyGenerator { public static void main(String[] args) { AutoGenerator generator = new AutoGenerator(); // 全局配置 GlobalConfig gc = new GlobalConfig(); gc.setOutputDir(System.getProperty("user.dir") + "/src/main/java"); gc.setAuthor("yourname"); gc.setOpen(false); gc.setSwagger2(true); generator.setGlobalConfig(gc); // 数据源配置 DataSourceConfig dsc = new DataSourceConfig(); dsc.setUrl("jdbc:mysql://localhost:3306/test?useSSL=false"); dsc.setDriverName("com.mysql.cj.jdbc.Driver"); dsc.setUsername("root"); dsc.setPassword("123456"); generator.setDataSource(dsc); // 包配置 PackageConfig pc = new PackageConfig(); pc.setParent("com.example"); pc.setEntity("domain"); pc.setMapper("dao"); generator.setPackageInfo(pc); // 策略配置 StrategyConfig strategy = new StrategyConfig(); strategy.setNaming(NamingStrategy.underline_to_camel); strategy.setColumnNaming(NamingStrategy.underline_to_camel); strategy.setEntityLombokModel(true); strategy.setRestControllerStyle(true); strategy.setInclude("user", "role"); // 生成的表 strategy.setControllerMappingHyphenStyle(true); strategy.setTablePrefix("t_"); // 表前缀 generator.setStrategy(strategy); // 自定义模板 TemplateConfig templateConfig = new TemplateConfig(); templateConfig.setEntity("templates/entity.java"); templateConfig.setMapper("templates/mapper.java"); generator.setTemplate(templateConfig); generator.execute(); } }

8.2 生产环境配置清单

推荐的生产环境配置:

mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl cache-enabled: true default-executor-type: REUSE local-cache-scope: STATEMENT global-config: db-config: id-type: ASSIGN_ID logic-delete-field: isDeleted logic-not-delete-value: 0 logic-delete-value: 1 banner: false mapper-locations: classpath*:mapper/**/*.xml

关键配置说明

  • log-impl: 使用SLF4J代替StdOutImpl
  • default-executor-type: REUSE比SIMPLE性能更好
  • local-cache-scope: STATEMENT级别避免内存泄漏
  • banner: 生产环境关闭启动banner

9. 常见问题解决方案

9.1 类型处理器不生效问题

现象@TableField(typeHandler = XXXTypeHandler.class)配置后不生效

排查步骤

  1. 检查实体类是否添加@TableName(autoResultMap = true)
  2. 确认TypeHandler是否实现了正确的泛型类型
  3. 检查MyBatis配置中是否注册了该TypeHandler
  4. 对于查询操作,确认ResultMap是否正确生成

9.2 分页查询性能问题

优化方案

  1. 对于大数据量分页,使用@SqlParser(filter=true)跳过count查询
  2. 考虑使用游标分页代替传统分页:
@Select("select * from user where #{ew.sqlSegment}") @Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = 1000) @ResultType(User.class) void selectByCursor(@Param(Constants.WRAPPER) Wrapper<User> wrapper, ResultHandler<User> handler);
  1. 添加合适的索引,特别是order by字段

9.3 Lambda表达式NPE问题

安全写法

// 不安全的写法 queryWrapper.lambda().eq(User::getName, user.getName()); // 安全的写法 queryWrapper.lambda() .eq(user != null && user.getName() != null, User::getName, user.getName()) .eq(User::getStatus, 1);

10. 与MyBatis的混合使用策略

10.1 XML与注解混合开发

推荐的项目结构:

src/main/java ├── com.xxx.mapper │ ├── UserMapper.java // 接口定义 │ └── CustomMapper.java // 自定义扩展接口 └── com.xxx.domain └── User.java // 实体类 src/main/resources ├── mapper │ ├── UserMapper.xml // 复杂SQL │ └── CustomMapper.xml // 自定义SQL └── application.yml

最佳实践

  • 简单CRUD使用MyBatisPlus提供的方法
  • 复杂查询、多表操作使用XML方式
  • 通过@MapperScan同时扫描接口和XML

10.2 自定义SQL注入

扩展BaseMapper功能:

public interface AllSqlInjector extends ISqlInjector { @Override void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) { // 先注入BaseMapper的方法 super.inspectInject(builderAssistant, mapperClass); // 注入自定义方法 addSelectMysqlPage(builderAssistant, mapperClass); } private void addSelectMysqlPage(MapperBuilderAssistant assistant, Class<?> mapperClass) { SqlSource sqlSource = languageDriver.createSqlSource( configuration, "select * from ${tableName} ${ew.customSqlSegment}", Object.class); String methodName = "selectMysqlPage"; assistant.addMappedStatement( methodName, sqlSource, StatementType.PREPARED, SqlCommandType.SELECT, null, null, null, null, null, null, null, true, true, false, null, null, null, configuration.getDatabaseId(), languageDriver, null); } }

11. 插件生态整合

11.1 MybatisX插件高级用法

IntelliJ IDEA的MybatisX插件提供了强大功能:

  • 从表结构生成实体类(支持Lombok、Swagger等)
  • XML与Mapper接口方法快速跳转
  • SQL自动补全与语法检查
  • 一键生成CRUD方法

实用技巧

  • 使用Alt+Enter快速生成@TableId注解
  • 通过"Generate MyBatis Sql"动作生成复杂查询
  • 配置自定义模板生成DTO、VO等类

11.2 Mybatis-Mate企业组件

Mybatis-Mate提供了更多企业级特性:

<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-mate-annotation</artifactId> <version>1.2.5</version> </dependency>

包含功能:

  • 数据权限(行级、列级)
  • 字段加密(AES、RSA等)
  • 数据审计(创建人、修改人等)
  • 数据绑定(枚举转换等)

12. 未来版本特性预览

虽然MyBatisPlus保持API稳定性,但一些新特性值得关注:

  1. 响应式编程支持:与Spring WebFlux深度集成
  2. GraalVM原生镜像支持:提升启动速度和内存效率
  3. 增强的分布式事务:与Seata更深度整合
  4. 更智能的代码生成:基于DDL自动生成领域模型

对于生产环境,建议保持对1-2个次要版本的跟进,既享受新特性又避免兼容性问题。可以通过官方GitHub的Milestone了解路线图。