当前位置: 首页 > news >正文

详细介绍:MyBatis 进阶实战:插件开发与性能优化

MyBatis 作为 Java 主流 ORM 框架,多数开发者仅掌握基础 CRUD 与动态 SQL,但对其核心原理、插件开发、缓存机制、性能优化理解不足,导致代码冗余、查询效率低、扩展性差(如通用分页、数据脱敏无法统一实现)。

本文从 MyBatis 核心原理出发,深入讲解插件开发(拦截器)、缓存机制优化、SQL 执行优化、批量操作技巧,结合实战代码与场景示例,帮你突破 MyBatis 使用瓶颈,写出高效、可扩展的持久层代码。

一、核心认知:MyBatis 核心原理与执行流程

1. 核心架构

MyBatis 架构分为三层,职责清晰:

  • 接口层:通过SqlSession提供 CRUD 操作接口,面向开发者;
  • 核心层:包含配置解析、SQL 解析、参数映射、结果映射、插件拦截、缓存管理,是 MyBatis 核心;
  • 基础层:包含数据源管理、事务管理、日志管理,为核心层提供支撑。

2. 核心执行流程(以查询为例)

  1. 加载配置:读取 MyBatis 配置文件(mybatis-config.xml)、Mapper 接口与 XML 文件,解析为Configuration对象;
  2. 创建 SqlSession:通过SqlSessionFactory创建SqlSession,关联Executor(执行器);
  3. 生成代理对象:Mapper 接口通过 JDK 动态代理生成代理对象,拦截接口方法;
  4. 解析 SQL:代理对象根据方法名匹配 XML 中的 SQL 语句,解析动态 SQL,生成最终执行 SQL;
  5. 执行 SQL:Executor通过StatementHandler执行 SQL,ParameterHandler处理参数,ResultSetHandler处理结果集;
  6. 缓存处理:查询结果先查缓存(一级 / 二级),无缓存则执行数据库查询,结果写入缓存;
  7. 返回结果:将结果集映射为 Java 对象,返回给调用者。

3. 四大核心组件(插件拦截目标)

MyBatis 插件通过拦截四大核心组件的方法,实现功能增强:

  • Executor:执行器,负责 SQL 执行与缓存管理,可拦截queryupdatecommit等方法;
  • StatementHandler:语句处理器,负责 SQL 预编译、参数设置、结果集处理,可拦截prepareparameterize等方法;
  • ParameterHandler:参数处理器,负责将 Java 参数映射为 SQL 参数,可拦截setParameters方法;
  • ResultSetHandler:结果集处理器,负责将 SQL 结果集映射为 Java 对象,可拦截handleResultSets方法。

二、实战:MyBatis 插件开发(拦截器)

MyBatis 插件本质是 JDK 动态代理与责任链模式,通过自定义拦截器,可实现通用功能(如分页、数据脱敏、日志增强、权限控制),无需侵入业务代码。

1. 插件开发核心规范

  • 实现org.apache.ibatis.plugin.Interceptor接口,重写intercept(核心拦截逻辑)、plugin(生成代理对象)、setProperties(读取配置参数)方法;
  • @Intercepts@Signature注解指定拦截的组件、方法与参数;
  • 插件需注册到 MyBatis 配置中(XML / 注解方式),才能生效。

2. 实战 1:通用分页插件(拦截 Executor)

传统分页需在 XML 中写LIMIT语句,通用性差,通过插件拦截Executor.query方法,自动拼接分页 SQL,实现通用分页。

(1)自定义分页拦截器

java

运行

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import java.util.Properties;
// 拦截Executor的query方法(两个重载方法)
@Intercepts({@Signature(type = Executor.class,method = "query",args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),@Signature(type = Executor.class,method = "query",args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})
})
public class PageInterceptor implements Interceptor {// 核心拦截逻辑@Overridepublic Object intercept(Invocation invocation) throws Throwable {// 1. 获取拦截参数Object[] args = invocation.getArgs();MappedStatement ms = (MappedStatement) args[0];Object parameter = args[1];RowBounds rowBounds = (RowBounds) args[2];BoundSql boundSql = ms.getBoundSql(parameter);// 2. 判断是否需要分页(自定义PageParam参数,包含pageNum、pageSize)if (parameter instanceof PageParam pageParam) {int pageNum = pageParam.getPageNum();int pageSize = pageParam.getPageSize();if (pageNum > 0 && pageSize > 0) {// 3. 生成分页SQL(拼接LIMIT,适配MySQL)String originalSql = boundSql.getSql();int offset = (pageNum - 1) * pageSize;String pageSql = originalSql + " LIMIT " + offset + ", " + pageSize;// 4. 重写BoundSql的SQL语句BoundSql newBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter);// 5. 替换原参数中的BoundSql(适配第二个重载方法)if (args.length == 6) {args[5] = newBoundSql;}// 6. 重写RowBounds为默认(避免MyBatis自带分页拦截)args[2] = RowBounds.DEFAULT;}}// 7. 执行原方法return invocation.proceed();}// 生成代理对象(默认实现,无需修改)@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}// 读取插件配置参数(如数据库类型,适配MySQL/Oracle)@Overridepublic void setProperties(Properties properties) {String dbType = properties.getProperty("dbType", "mysql");// 可根据数据库类型调整分页SQL拼接逻辑}
}
(2)分页参数类(PageParam)

java

运行

import lombok.Data;
@Data
public class PageParam {private int pageNum; // 页码(从1开始)private int pageSize; // 每页条数// 可扩展:排序字段、排序方向等
}
(3)注册插件(MyBatis 配置)
① XML 方式(mybatis-config.xml)

xml


② Spring Boot 注解方式

java

运行

@Configuration
public class MyBatisConfig {@Beanpublic PageInterceptor pageInterceptor() {PageInterceptor interceptor = new PageInterceptor();Properties properties = new Properties();properties.setProperty("dbType", "mysql");interceptor.setProperties(properties);return interceptor;}
}
(4)使用方式(业务代码)

java

运行

// Mapper接口
public interface OrderMapper {List selectOrderList(PageParam pageParam);
}
// XML文件(无需写LIMIT,插件自动拼接)

// 服务层调用
@Service
public class OrderService {@Resourceprivate OrderMapper orderMapper;public List getOrderList(int pageNum, int pageSize) {PageParam pageParam = new PageParam();pageParam.setPageNum(pageNum);pageParam.setPageSize(pageSize);return orderMapper.selectOrderList(pageParam);}
}

3. 实战 2:数据脱敏插件(拦截 ResultSetHandler)

对敏感数据(如手机号、身份证号)进行脱敏处理(如手机号显示为 138****8000),通过拦截结果集处理方法,自动脱敏,无需修改业务代码。

(1)自定义脱敏注解(标记需要脱敏的字段)

java

运行

import java.lang.annotation.*;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Sensitive {// 脱敏类型(手机号、身份证、姓名)SensitiveType type();
}
// 脱敏类型枚举
public enum SensitiveType {PHONE, // 手机号ID_CARD, // 身份证号NAME // 姓名
}
(2)脱敏拦截器

java

运行

import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.*;
import java.lang.reflect.Field;
import java.sql.Statement;
import java.util.List;
import java.util.Properties;
@Intercepts({@Signature(type = ResultSetHandler.class,method = "handleResultSets",args = {Statement.class})
})
public class SensitiveInterceptor implements Interceptor {@Overridepublic Object intercept(Invocation invocation) throws Throwable {// 1. 执行原方法,获取结果集List resultList = (List) invocation.proceed();// 2. 对结果集进行脱敏处理for (Object result : resultList) {desensitize(result);}return resultList;}// 脱敏处理核心逻辑private void desensitize(Object result) throws IllegalAccessException {Class clazz = result.getClass();// 遍历所有字段,判断是否有@Sensitive注解Field[] fields = clazz.getDeclaredFields();for (Field field : fields) {if (field.isAnnotationPresent(Sensitive.class)) {field.setAccessible(true); // 允许访问私有字段Sensitive sensitive = field.getAnnotation(Sensitive.class);Object value = field.get(result); // 获取字段值if (value instanceof String strValue && !strValue.isEmpty()) {// 根据脱敏类型处理String desensitizedValue = switch (sensitive.type()) {case PHONE -> desensitizePhone(strValue);case ID_CARD -> desensitizeIdCard(strValue);case NAME -> desensitizeName(strValue);default -> strValue;};field.set(result, desensitizedValue); // 设值脱敏后的值}}}}// 手机号脱敏:保留前3位+后4位,中间替换为****private String desensitizePhone(String phone) {if (phone.length() == 11) {return phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");}return phone;}// 身份证脱敏:保留前6位+后4位,中间替换为****private String desensitizeIdCard(String idCard) {if (idCard.length() == 18) {return idCard.replaceAll("(\\d{6})\\d{8}(\\d{4})", "$1********$2");}return idCard;}// 姓名脱敏:保留姓,名替换为*private String desensitizeName(String name) {if (name.length() >= 2) {return name.substring(0, 1) + "*".repeat(name.length() - 1);}return name;}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic void setProperties(Properties properties) {}
}
(3)使用方式

java

运行

// 实体类标记敏感字段
@Data
public class UserDO {private Long id;@Sensitive(type = SensitiveType.NAME)private String userName; // 姓名脱敏@Sensitive(type = SensitiveType.PHONE)private String phone; // 手机号脱敏@Sensitive(type = SensitiveType.ID_CARD)private String idCard; // 身份证脱敏
}
// 注册插件(同分页插件,添加到MyBatis配置)
@Bean
public SensitiveInterceptor sensitiveInterceptor() {return new SensitiveInterceptor();
}
// 调用后自动脱敏:phone=13800138000 → 138****8000

三、实战:MyBatis 性能优化

1. 缓存机制优化(减少数据库查询)

MyBatis 提供两级缓存,合理使用可大幅减少数据库查询次数,提升性能。

(1)一级缓存(SqlSession 级别,默认开启)
  • 原理:同一SqlSession内,相同 SQL 查询会缓存结果,再次查询直接返回缓存,SqlSession关闭 / 提交后缓存失效;
  • 优化点:
  1. 避免频繁创建SqlSession(如 Spring 整合 MyBatis 时,SqlSession由 Spring 管理,默认单例);
  2. 读写分离场景下,避免一级缓存导致脏读(可关闭一级缓存,localCacheScope=STATEMENT)。
(2)二级缓存(Mapper 级别,需手动开启)
  • 原理:多个SqlSession共享同一 Mapper 的缓存,缓存存储在MapperStatement中,默认存储在内存(可配置第三方缓存如 Redis);
  • 开启方式:
  1. 全局配置开启(mybatis-config.xml):

xml

 
  1. Mapper XML 开启(在对应 XML 中添加<cache>标签):

xml

flushInterval="60000" size="1024" readOnly="true"/> 
  • 优化点:
  1. 仅对高频查询、低频更新的数据开启二级缓存(如商品基础信息);
  2. 避免缓存大对象(如 List<Order>),占用内存过多;
  3. 复杂查询(多表联查)不建议开启二级缓存,易导致脏读;
  4. 整合 Redis 作为二级缓存,解决内存缓存容量有限、集群环境缓存不一致问题。

2. SQL 执行优化

(1)优化动态 SQL,避免冗余
  • <where>替代WHERE 1=1,自动处理 AND/OR 逻辑,减少无效 SQL;
  • <choose><when><otherwise>替代多个<if>,逻辑更清晰,避免冗余判断;
  • 批量操作使用<foreach>,避免循环单条执行(如批量插入、批量更新)。
(2)避免SELECT *,仅查询需要的字段
  • 减少数据传输量与结果集映射时间,同时避免触发不必要的字段脱敏、转换;
  • 示例:SELECT id, user_id, product_id FROM order 替代 `SELECT * FROM `order
(3)优化分页查询,避免LIMIT offset过大
  • 参考 JVM 调优中分页优化逻辑,用主键定位替代偏移量分页,结合 MyBatis 实现:

xml

3. 批量操作优化

(1)批量插入(MyBatis 批量插入技巧)
  • 低效方式:循环调用单条插入(频繁与数据库交互,性能差);
  • 高效方式:使用<foreach>拼接批量插入 SQL,减少数据库交互次数:

xml

INSERT INTO `order` (user_id, product_id, count, create_time)VALUES(#{item.userId}, #{item.productId}, #{item.count}, #{item.createTime})
  • 进阶优化:设置 JDBC 批量提交参数(rewriteBatchedStatements=true),MySQL 会优化批量插入性能:

yaml

spring:datasource:url: jdbc:mysql://localhost:3306/test?rewriteBatchedStatements=true
(2)批量更新(避免循环单条更新)
  • 方式 1:<foreach>拼接 CASE WHEN 语句,单条 SQL 完成批量更新:

xml

UPDATE `order`WHEN id = #{item.id} THENcount = #{item.count}, update_time = #{item.updateTime}WHERE id IN#{item.id}
  • 方式 2:使用 MyBatis-Plus 的updateBatchById方法(简化批量更新代码)。

4. 其他性能优化点

  • 复用SqlSession:Spring 整合 MyBatis 时,默认使用SqlSessionTemplate,自动管理SqlSession生命周期,无需手动创建;
  • 优化参数映射:使用@Param注解明确参数名称,避免参数位置错误,提升映射效率;
  • 减少反射开销:MyBatis 默认使用反射映射结果集,可通过 ASM 字节码框架优化(MyBatis 3.4 + 已默认优化);
  • 定期清理缓存:二级缓存设置合理的刷新间隔,避免缓存数据过期导致脏读。

四、避坑指南

1. 坑点 1:二级缓存导致脏读

  • 表现:多表联查时,关联表数据更新后,二级缓存未刷新,返回旧数据;
  • 解决方案:1. 复杂联查不开启二级缓存;2. 配置flushCache="true",更新时刷新缓存;3. 用 Redis 缓存替代二级缓存,手动控制缓存失效。

2. 坑点 2:插件拦截方法错误,导致 SQL 执行异常

  • 表现:自定义插件拦截方法后,SQL 执行报错(如参数丢失、SQL 拼接错误);
  • 解决方案:1. 明确拦截的组件与方法签名(参数类型、顺序必须与原方法一致);2. 拦截逻辑中保留原方法核心逻辑,仅做增强;3. 调试时打印 SQL 与参数,定位问题。

3. 坑点 3:批量操作 SQL 过长,导致数据库报错

  • 表现:批量插入 / 更新时,拼接的 SQL 过长(超过数据库max_allowed_packet参数),触发报错;
  • 解决方案:1. 拆分批量数据(如每 100 条分一批);2. 增大数据库max_allowed_packet参数(如设置为 16M)。

4. 坑点 4:忽视 ResultMap 优化,导致性能损耗

  • 表现:使用resultType="map"或自动映射,导致字段类型转换频繁、冗余字段映射;
  • 解决方案:自定义ResultMap,明确字段映射关系,避免自动映射的冗余开销,同时解决字段名与属性名不一致问题。

5. 坑点 5:一级缓存导致读写分离脏读

  • 表现:主库更新数据后,从库未同步,一级缓存返回旧数据;
  • 解决方案:1. 关闭一级缓存(localCacheScope=STATEMENT);2. 读写分离场景下,查询走从库,更新走主库,且更新后清空对应缓存。

五、终极总结:MyBatis 进阶的核心是 “理解原理 + 按需扩展”

MyBatis 的强大之处在于其灵活性与可扩展性 —— 基础 CRUD 满足日常需求,插件机制可实现通用功能扩展,缓存与 SQL 优化可大幅提升性能。

落地时需记住:

  1. 先懂原理:理解 MyBatis 执行流程与四大核心组件,再开发插件与优化;
  2. 插件按需开发:通用功能(分页、脱敏)用插件,避免过度开发导致维护成本上升;
  3. 性能优化优先:缓存优化、SQL 优化、批量操作优化是提升持久层性能的核心;
  4. 避坑关键:关注缓存一致性、插件拦截正确性、SQL 合理性,避免引入新问题。

通过本文内容,可突破 MyBatis 基础使用瓶颈,实现从 “会用” 到 “精通” 的跨越,写出高效、可扩展、稳定的持久层代码。

http://www.jsqmd.com/news/395899/

相关文章:

  • RMBG-2.0全解析:这个免费工具如何改变你的修图工作流
  • Cosmos-Reason1-7B大模型在Linux系统上的高效部署方案
  • Qwen2.5-0.5B Instruct实现多语言翻译:支持29种语言
  • 阿里小云语音唤醒模型:智能设备语音控制新方案
  • 40亿参数DASD-4B-Thinking体验:数学推理一键搞定
  • RexUniNLU在软件测试中的应用:自动化测试用例生成
  • 伏羲天气预报实操手册:Gradio输出结果中min/max/mean值的物理场对应关系
  • MusePublic艺术创作引擎LaTeX应用:科技艺术论文排版
  • Xinference-v1.17.1 Anaconda环境配置:Python科学计算一体化方案
  • 代码已死?Redis之父Antirez的AI编程宣言:从“造物主”到“指挥官”的范式革命
  • 春联生成模型-中文-base入门教程:两字关键词生成逻辑与提示词技巧
  • 3步部署LFM2.5-1.2B:ollama上的轻量级大模型
  • 如何评估Qwen2.5性能?吞吐/延迟/显存综合评测教程
  • Lychee-rerank-mm新手指南:从环境配置到批量图片排序实战
  • Nano-Banana拆解屋入门:无需技术背景也能上手的AI工具
  • Qwen3-Reranker-8B效果实测:多语言检索准确率超70%
  • Qwen3-ASR-0.6B在Linux环境下的编译与优化
  • RMBG-2.0模型训练数据揭秘:高质量数据集的构建方法
  • 阿里小云语音唤醒模型在智能音箱中的实际应用案例
  • 零代码玩转GTE模型:Web界面一键体验语义搜索
  • Qwen-Image-Edit-F2P模型在软件测试中的创新应用
  • BS社区物业管理系统信息管理系统源码-SpringBoot后端+Vue前端+MySQL【可直接运行】
  • Gemma-3-270m在VMware虚拟机配置中的智能应用
  • WAN2.2文生视频+SDXL_Prompt风格保姆级教程:ComfyUI中视频后处理节点集成
  • PP-DocLayoutV3在Win11系统上的最佳实践
  • StructBERT中文句子相似度工具:5分钟快速部署与实战体验
  • DeerFlow新手教程:从安装到第一个研究项目
  • LoRA动态切换技巧:让Lingyuxiu MXJ人像风格随心变换
  • SiameseUIE在QT桌面应用中的集成:跨平台解决方案
  • 大模型技术解析:Baichuan-M2-32B医疗专用架构设计