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代替StdOutImpldefault-executor-type: REUSE比SIMPLE性能更好local-cache-scope: STATEMENT级别避免内存泄漏banner: 生产环境关闭启动banner
9. 常见问题解决方案
9.1 类型处理器不生效问题
现象:@TableField(typeHandler = XXXTypeHandler.class)配置后不生效
排查步骤:
- 检查实体类是否添加
@TableName(autoResultMap = true) - 确认TypeHandler是否实现了正确的泛型类型
- 检查MyBatis配置中是否注册了该TypeHandler
- 对于查询操作,确认ResultMap是否正确生成
9.2 分页查询性能问题
优化方案:
- 对于大数据量分页,使用
@SqlParser(filter=true)跳过count查询 - 考虑使用游标分页代替传统分页:
@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);- 添加合适的索引,特别是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稳定性,但一些新特性值得关注:
- 响应式编程支持:与Spring WebFlux深度集成
- GraalVM原生镜像支持:提升启动速度和内存效率
- 增强的分布式事务:与Seata更深度整合
- 更智能的代码生成:基于DDL自动生成领域模型
对于生产环境,建议保持对1-2个次要版本的跟进,既享受新特性又避免兼容性问题。可以通过官方GitHub的Milestone了解路线图。