资讯动态

MyBatis插件机制全解析:四大核心对象拦截与实战案例

发布时间:2026/10/13 22:11:22 来源:尧图企业网站定制
MyBatis的插件体系说穿了就是给四大核心对象挂代理Executor、StatementHandler、ParameterHandler、ResultSetHandler。日常开发里经常听到的“拦截器”或者“插件”比如分页插件、字段自动填充插件、数据权限插件本质上都是在这四个对象上做文章。这篇文章不打算把官方文档复述一遍而是从“这东西到底怎么跑起来”的角度把拦截、包装、代理、签名匹配这些概念讲清楚再用三个能直接上手的实战案例带你走一遍从定义拦截器到注册配置的完整流程。如果你是刚接触MyBatis不久的新人这一篇可以作为第二篇阅读资料——第一篇建议先把MyBatis的基本CRUD流程过一遍。如果你已经写过几年Mapper但对插件内部始终停留在“会用PageHelper”的状态这篇文章应该能帮你把这块拼图补上。下面要讲的内容全部基于模拟项目X的真实开发过程代码我做了一定简化核心链路保留完整。1. MyBatis插件机制的核心原理1.1 拦截点到底在哪四大核心对象MyBatis一条SQL从接口方法走到数据库要经过几个固定环节。Mapper接口的代理对象把请求转给SqlSessionSqlSession把活儿派给ExecutorExecutor负责调度整个执行过程包括调用StatementHandler去创建JDBC PreparedStatement用ParameterHandler给SQL里的问号占位符一一绑定参数最后用ResultSetHandler把数据库返回的结果集映射成你想要的实体对象。这一条链路看似简单但每一环都留下了可插入的缝隙。Executor作为最顶层的执行器所有增删改查都绕不开它所以查询拦截、分页逻辑、SQL重写这类需求首选拦截它。StatementHandler承担SQL语句的解析和预编译想改SQL本身、或者给语句加额外的处理逻辑时它会派上用场。ParameterHandler管参数绑定恰好适合做参数级别的写入前处理比如字段加密。ResultSetHandler负责结果映射查询结果的脱敏、解密、字段加工都在这层做。这就是MyBatis插件的基础认知与其说插件是一种功能扩展不如说它是在框架的核心执行路径上安插了可编程的拦截点。选择在哪个对象上做文章基本决定了你能影响哪一段执行行为。不少刚接触插件机制的同学容易陷入一个误区总想着去拦截自己的Mapper接口方法实际上插件体系根本够不到那一层你能动手的只有这四个内建对象。1.2 三个注解和方法搞定一个插件MyBatis里定义一个插件结构上很简单。你需要实现Interceptor接口接口里只有三个方法intercept、plugin、setProperties。intercept方法是拦截逻辑真正执行的地方每当MyBatis调用被代理的方法时都会进入这里plugin方法是包装目标对象的入口通常直接调用Plugin.wrap就行setProperties方法用于接收配置里传入的初始化参数。真正定义“拦截谁、拦什么方法、参数长什么样”的是作用在类上的Intercepts和Signature注解。Intercepts里放一组Signature每个Signature由三个要素组成type表示目标对象的类型method表示要拦截的方法名args表示该方法的参数类型列表。这三者必须和目标对象真实声明的参数列表完全匹配否则签名匹配不上插件会静默失效。这里顺便说一个新手最容易踩的坑args参数必须是全限定参数类名顺序也不能乱。以Executor.query方法为例它的完整签名是query(MappedStatement, Object, RowBounds, ResultHandler)你写签名时就必须按这个顺序列出四个参数的Class对象。在很多历史版本里MyBatis还提供过带CacheKey参数的重载query方法如果只拦截了四参数的版本某些执行路径就拦不到排查起来特别迷惑。提示不要把拦截点想当然地放在Mapper接口层。MyBatis的插件机制只支持四大核心对象拦截别的类或者自定义接口签名匹配不会生效代码也不会报错但就是永远走不进intercept。1.3 代理与调用链多个插件如何叠加MyBatis单个插件能干活多个插件就必须理清“谁包谁”的关系。每创建一个会话MyBatis都会调用Configuration.newExecutor、newStatementHandler等方法来创建核心对象随后它会遍历所有注册的拦截器逐个调用plugin方法对目标对象做包装。这个包装动作的本质是应用JDK动态代理每个拦截器都尝试给目标对象生成一层代理后注册的拦截器会包在最外层每次调用都会从外层代理一层一层往里穿透直到抵达真实的Executor对象。理解这一点对排查多层插件互相影响非常关键。假设你先注册了分页插件再注册数据权限插件那数据权限插件的代理就在外层它会先执行分页插件的代理在内层后执行。如果你的权限插件改写了SQL而分页插件拿到的SQL是基于改写后的那就得多留个心眼确认分页的count统计接的是不是改动后的完整SQL。插件之间的顺序在mybatis-config.xml里由配置顺序决定调整插件声明的先后顺序就能控制这道代理链的复合效果。更多时候我们需要的动作职责单一一个插件只干一件事把分页、审计、加密拆成独立插件通过顺序组织它们的关系而不是把一堆逻辑糊在一个intercept里。这个设计原则在后续的实战案例里会反复体现。2. 从零手写第一个插件跑通拦截流程2.1 环境准备与项目结构动手之前先把基础环境确认一下。我在模拟项目X里用的是Spring Boot 2.7 MyBatis 3.5.xMapper接口和XML映射文件都是标准写法数据库用H2做测试生产环境是MySQL。插件开发和环境无关只要你的项目里已经引入了MyBatis的依赖就能跑Spring Boot项目需要额外把自定义插件注册到SqlSessionFactory里。项目里新建一个plugin的包把后续所有拦截器类放在一起。这个包建议保持干净不要和业务Service混在一起因为插件属于框架层的横切逻辑单独成包既方便配置扫描也方便以后抽取成公共模块复用。包内至少需要三类文件拦截器类、插件注册配置、以及一个用于承载拦截结果的辅助类。2.2 写一个打印执行耗时的SQL监控插件第一个案例不求复杂先跑通完整的“声明-拦截-注册”三连流程。我们要实现的目标很简单拦截Executor的query和update方法记录每条SQL的执行耗时超过阈值就输出告警日志。这可能是最能直接感受到“我在框架核心链路里插入了一段逻辑”的例子。先定义签名。Executor的关键方法有两个query的签名是query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler)update的签名是update(MappedStatement ms, Object parameter)。这里需要注意update在Executor接口里只有两参数的版本没有多余的重载但query还有带CacheKey的五参数版本实际拦截时四参数版本基本覆盖了绝大多数场景如果你希望拦截彻底可以把两个签名都列出来。intercept方法内部拿invocation.getArgs()获取方法入参第一个参数永远是MappedStatement从它身上可以拿到BoundSql再从BoundSql里取出最终的SQL字符串和参数对象。需要说明的是这里的SQL是经过动态SQL标签渲染后的最终SQL和你XML里写的语句可能不太一样打印出来的才是真正发往数据库的那一条。核心代码如下Intercepts({ Signature(type Executor.class, method query, args {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}), Signature(type Executor.class, method update, args {MappedStatement.class, Object.class}) }) public class SqlCostInterceptor implements Interceptor { private long slowThresholdMs 1000; Override public Object intercept(Invocation invocation) throws Throwable { long start System.currentTimeMillis(); try { return invocation.proceed(); } finally { long cost System.currentTimeMillis() - start; MappedStatement ms (MappedStatement) invocation.getArgs()[0]; BoundSql boundSql ms.getBoundSql(invocation.getArgs()[1]); String sql boundSql.getSql().replaceAll(\\s, ).trim(); if (cost slowThresholdMs) { log.warn(slow sql cost {} ms, sql: {}, cost, sql); } } } Override public Object plugin(Object target) { return Plugin.wrap(target, this); } Override public void setProperties(Properties properties) { if (properties ! null) { String threshold properties.getProperty(slowThresholdMs); if (threshold ! null StringUtils.isNumeric(threshold)) { slowThresholdMs Long.parseLong(threshold); } } } }这里有一个很多人第一次写时会忽略的细节intercept方法里必须调用invocation.proceed()它的作用是让调用链继续往下走。如果漏掉这行SQL不会执行查询直接返回null而且不抛异常排查起来特别诡异。proceed内部会调用MethodInvocation的实现逻辑按代理链顺序进入下一个拦截器或者真正的方法体。2.3 注册插件的完整姿势Spring Boot项目里最直接的注册方式是自己定义一个Configuration类注入SqlSessionFactory然后调用factory.getConfiguration().addInterceptor(interceptor)把插件加进去。这种方式的优势是插件对象的创建完全由Spring管理可以放心注入数据源、Redis客户端或者其他Spring Bean。Configuration public class MybatisPluginConfig { Bean public SqlCostInterceptor sqlCostInterceptor() { SqlCostInterceptor interceptor new SqlCostInterceptor(); Properties props new Properties(); props.setProperty(slowThresholdMs, 800); interceptor.setProperties(props); return interceptor; } Configuration public static class MybatisConfigurer { Resource private ListInterceptor interceptors; PostConstruct public void registerPlugins() { for (SqlSessionFactory factory : getSqlSessionFactories()) { for (Interceptor interceptor : interceptors) { if (!factory.getConfiguration().getInterceptors().contains(interceptor)) { factory.getConfiguration().addInterceptor(interceptor); } } } } } }如果你没有使用Spring Boot而是在纯MyBatis环境下运行注册逻辑就是在XML配置文件里的plugins标签里声明拦截器类或者通过Configuration.addInterceptor编程式加入。纯XML方式里setProperties接收的是property子标签的值这更方便运维同事在不动代码的情况下调整阈值。注意同一个Interceptor对象不要重复add否则代理链中会出现同一层拦截两次的情况日志会重复输出数据权限插件甚至会导致SQL片段被重复拼接。3. 实战案例自研分页插件的完整实现3.1 为什么还要自研分页插件现在业界最常用的分页插件是PageHelper绝大多数场景开箱即用。那我为什么还要在文章里带着大家写一遍分页插件呢两个原因。第一分页插件是理解“SQL重写”这一类逻辑的最佳载体它涉及从原始SQL里提取count语句、拼接数据库方言分页片段、还原参数绑定这些常见操作学会了它权限插件、租户插件、逻辑删除插件都是同一条路。第二在很多公司内部分页插件被封装进自研的ORM中间件或者因为SQL规范限制不能引入第三方依赖这时候你会真的需要手写一套轻量分页。另外自研分页时你顺便还能解决一个PageHelper风格下的经典痛点当查询参数不是纯POJO而是带有分页参数的DTO对象时不需要借助ThreadLocal就可以直接把分页信息从参数里传递进去避免多线程环境下的上下文串位。3.2 分页逻辑的关键设计自研分页的核心是在Executor.query的intercept里做三件事。第一步从入参里解析分页对象。我习惯定义统一的PageParam基类包含pageNum、pageSize、orders几个字段查询传入的DTO继承它即可。拦截器拿到参数对象后用MetaObject反射检查它是否有分页字段没有就直接放行。第二步改写SQL。分页最关键的是要生成一条正确的count语句和一条方言分页语句。count语句不能简单地在原SQL外面包一层SELECT COUNT(*)因为原SQL里可能含有ORDER BY、GROUP BY、DISTINCT有的数据库对大子查询包count效率极差。简单安全的做法是先对SQL做规范化处理去掉末尾的order by片段然后外包一层private String buildCountSql(String originalSql) { String sql originalSql.trim(); // 粗略去除最后一段ORDER BY实际使用中建议用SQL解析器 int orderByIndex sql.toLowerCase().lastIndexOf(order by); if (orderByIndex 0) { sql sql.substring(0, orderByIndex); } return SELECT COUNT(*) FROM ( sql ) tmp_count; }诚实地讲用字符串截取处理order by看起来很朴素我平时开发中确实有项目是这么干的对内部规范化的SQL够用。但如果你要把它做成通用组件还是老老实实引入SQL解析库把SQL解析成语法树再做增删改稳定性完全不是一个级别。我之前在一个报表系统里吃过亏原SQL里带了子查询子查询内部也有order by简单的lastIndexOf直接把子查询内的order by截掉了count结果倒是没错但分页数据却被截得乱七八糟。后来换成解析器这种问题一次都没再出现过。第三步执行分页查询。count查询和实际查询不能走同一个MappedStatement因为SQL语句不同但参数对象是同一个。分页插件里通常会把原始MappedStatement的id后面加个后缀创建出一个新的MappedStatement把改写后的SQL放进去再传递给invocation.proceed()执行。当数据库是MySQL时分页片段就简单了LIMIT offset, pageSize两个参数需要额外绑定到参数对象里。3.3 完整代码与执行链路演示下面这段代码是分页插件的骨架我为模拟项目X写的实际版本做了简化去掉了缓存和方言适配器但核心链路都在Intercepts({ Signature(type Executor.class, method query, args {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}) }) public class PageInterceptor implements Interceptor { private DatabaseDialect dialect new MySqlDialect(); Override public Object intercept(Invocation invocation) throws Throwable { Object parameter invocation.getArgs()[1]; PageParam pageParam resolvePageParam(parameter); if (pageParam null) { return invocation.proceed(); } MappedStatement ms (MappedStatement) invocation.getArgs()[0]; BoundSql boundSql ms.getBoundSql(parameter); String originalSql boundSql.getSql(); long total queryCount(ms, parameter); if (total 0) { return Collections.emptyList(); } MappedStatement pageMs buildPageMappedStatement(ms, dialect.buildPageSql(originalSql, pageParam)); List? result doQuery(pageMs, parameter); return new PageResult(result, total, pageParam.getPageNum(), pageParam.getPageSize()); } }讲到这里有一个绕不开的技术点当你构造出新的MappedStatement后不能只改BoundSql里的sql字符串因为MyBatis的BoundSql和MappedStatement之间通过SqlSource关联必须重新生成SqlSource并把MappedStatement里的sqlSource属性替换掉。而MappedStatement是final字段居多的对象常规手段改不了典型的做法是通过反射或者重新new一个MappedStatement.Builder来构建新实例。这个细节是自研分页插件最繁琐的地方也是它和普通业务代码最不一样的地方。每次执行count查询时我建议使用的是同一个参数对象下的count结果避免两遍SQL之间因为参数不一致导致结果对不上。有的团队为了极致的性能会在count查询里额外传递一个缓存参数把count结果塞进内存缓存短时间内的分页请求可以直接读缓存减少一次数据库往返但对一致性要求高的业务不建议这么玩。3.4 分页插件和RowBounds的关系很多人以为自己写分页插件就必须依赖RowBounds其实RowBounds只是MyBatis内存分页的一个内置对象如果查询的Mapper方法没有声明RowBounds参数invocation.getArgs()[2]拿到的就是默认值是一个无用对象。真正的分页参数应该在业务参数对象里或者用ThreadLocal传递。使用ThreadLocal有一个隐形风险就是并发线程复用时如果没清理会串数据这也是我推荐把分页参数放进DTO参数里的原因。如果你的历史代码里大量使用了ThreadLocal方式每个线程使用完务必要在finally块里remove。这里还要提醒一个分页插件的经典副作用如果你的Mapper查询里有自动联表查询比如一对多关联导致结果集行数膨胀分页插件基于原始SQL的count统计会把关联展开后的行数也算进去用户看到的总数会偏大。要解决这个问题要么约束业务SQL不要用关联集合映射要么在count逻辑里对主表做去重具体取舍要看业务数据的特性。4. 实战案例审计字段自动填充插件4.1 需求背景与设计思路有一类业务场景几乎是所有企业内部系统的标配每张表都约定有创建时间、修改时间、创建人、修改人四个审计字段但开发同学在写Mapper时经常忘填导致报表统计、问题追溯、操作日志记录全部失真。解决思路有两条一条是数据库层面用触发器或默认值另一条就是在应用层通过MyBatis插件自动填充。触发器对数据库侵入性太强维护困难我见到的大多数团队最后都选择了应用层插件。审计字段自动填充有两个必须面对的分支insert和update的字段差异。insert要把四个字段全补上update只需要更新修改时间和修改人如果update也去覆盖创建时间那就污染了历史数据。所以拦截器里要解析MappedStatement的SqlCommandType区分INSERT和UPDATE再决定填充哪些字段。4.2 拦截器的实现细节我选择的拦截点是Executor.update方法原因是它同时覆盖insert和update两条路径而且入参里的MappedStatement能拿到SQL命令类型和参数映射关系。这里有一个隐藏的麻烦Executor.update拿到的参数对象可能是一个很复杂的结构有时候是整个实体对象有时候是零散的多个参数包装成的ParamMap直接修改实体对象的属性更可靠。为了让代码不去硬编码实体类里的字段名我的做法是先定义接口让需要自动填充的实体类实现AuditAware接口接口里声明setCreateTime、setUpdateTime这些方法。这样拦截器只需通过instanceof判断然后向下转型就能填充与具体业务类解耦。这种做法虽然要求业务实体主动实现接口但换来的是填充逻辑的极度稳定。Intercepts({ Signature(type Executor.class, method update, args {MappedStatement.class, Object.class}) }) public class AuditFieldInterceptor implements Interceptor { Override public Object intercept(Invocation invocation) throws Throwable { MappedStatement ms (MappedStatement) invocation.getArgs()[0]; Object parameter invocation.getArgs()[1]; SqlCommandType commandType ms.getSqlCommandType(); if (parameter instanceof AuditAware) { AuditAware audit (AuditAware) parameter; String operator OperatorContextHolder.getCurrentUser(); if (commandType SqlCommandType.INSERT) { if (audit.getCreateTime() null) { audit.setCreateTime(LocalDateTime.now()); } audit.setUpdateTime(LocalDateTime.now()); audit.setCreateBy(operator); audit.setUpdateBy(operator); } else if (commandType SqlCommandType.UPDATE) { // 更新操作永远只动update两个字段 audit.setUpdateTime(LocalDateTime.now()); audit.setUpdateBy(operator); } } return invocation.proceed(); } }如果你们团队不想让实体类引入接口依赖也可以退回到MetaObject反射的方式按约定字段名自动识别。两种方案我都用过前者更适合新项目强制约束后者适合老项目里已经存在的大量实体的无侵入改造。MetaObject可以通过反射很方便地读写属性例如SystemMetaObject.forObject(parameter).setValue(createTime, now)但遇到私有属性时还需要设置reflectorFactory配置允许访问非public字段。4.3 参数绑定阶段的另一个拦截点除了修改参数对象还有一种更底层的做法是拦截ParameterHandler.setParameters在参数真正绑定到PreparedStatement之前往参数列表里追加值。这个思路的优势是哪怕参数对象被包装成了ParamMap你也能为指定的parameter mapping添加新值。缺点是这时候要修改BoundSql的参数映射列表操作对象从“业务实体”变成了“MyBatis内部机制”代码复杂度和踩坑概率都高很多。我把这两种方式的适用场景给你交个底通常业务系统里用修改参数对象就足够只有做那种对业务透明、要求所有Mapper无感生效的框架级组件时才需要深入到ParameterHandler层。你在评估方案时可依据一条标准你的组件是要交付给一个团队使用还是只为自己项目服务。真实项目里还有一个常见问题某个INSERT语句里业务代码已经手动设置了创建时间插件又覆盖成当前时间导致时间不准确。针对这个我建议在插件里做一次判断如果属性有值且非null就不覆盖。别小看这个细节很多线上数据的时间错乱排查到头发现不是程序Bug而是自动填充插件和显式赋值打架。5. 实战案例数据权限行级过滤插件5.1 插件的SQL拼接能力如何实现第三个案例是很多中后台系统都需要的数据权限控制。举个例子某个管理系统里的销售员只能看到自己名下客户部门经理能看到本部门客户而管理员能看到全部。如果每个查询都在业务代码里手动加WHERE条件不仅容易漏后期还很难统一收口。数据权限插件的核心思路是拦截Executor.query在所有查询SQL上自动拼接一段权限条件。实现上并不复杂。先从一个上下文类里拿到当前登录用户及其部门、角色信息再通过配置的规则库判断当前用户应该拥有的数据范围。比如只允许本人数据拼接条件是AND owner_id #{authUserId}允许本部门数据拼接条件是AND dept_id #{authDeptId}。拼接的位置要放在整个WHERE子句之后最好用预留的占位符我习惯给业务方规定一条公约所有需要权限过滤的查询SQL都必须以特定注释标记结尾插件定位到这个标记后在它前面插入过滤条件。这种约定方式比通用SQL解析要可靠得多因为它不依赖复杂的语法树解析。5.2 防止权限插件破坏其他组件的细节数据权限插件在代理链中的位置极其敏感。如果分页插件在它外层就会先执行分页改写再进入权限拼接这样权限插件拼接条件时拿到的SQL已经带上了LIMIT子句条件就会被拼错。反过来分页插件在权限插件里层的话权限条件拼好后分页插件再做count统计统计的就是过滤后的数据逻辑才对。因此配置顺序上必须数据权限插件在前分页插件在后。这一点一定要在项目文档里标注清楚否则后续新人调整插件顺序就会埋下一条SQL权限绕过隐患。权限过滤条件里的参数绑定也需要留意。拼接WHERE条件时动态值不能直接写进SQL字符串里而应该为BoundSql增加额外的ParameterMapping并把值设置到参数对象中。如果直接字符串拼接轻则每次查询都要硬解析SQL重则直接导入SQL注入风险。这一步的实操方案是使用MetaObject操作additionalParameters把新参数和对应的映射结构放进去MyBatis在执行时自然会完成预编译绑定。5.3 数据权限与缓存策略的冲突添加了行级权限之后二级缓存的影响必须重新评估。MyBatis的二级缓存是以namespace为单位查询结果缓存后如果不同用户持有的权限范围不同后访问的人拿到的可能是别人过滤后的结果集。这是数据权限插件最容易踩的坑我在实际项目里就遇到过一次管理员查全量数据缓存后普通销售员再查同一方法竟然拿到了全量数据。应对办法有两个。一是在缓存key里掺入权限维度信息比如把userId或权限指纹拼进key二是数据权限插件直接禁用该namespace下的二级缓存需要缓存的地方全部改用业务应用层缓存并自己控制缓存粒度和权限维度。考虑到绝大多数中后台系统的查询即点即查我推荐实用优先仅对少量高频场景做应用层缓存不要依赖MyBatis二级缓存处理权限查询。6. 插件开发中的常见坑与排查清单6.1 插件不生效的几类原因插件不生效是群里问得最多的问题十个里面有八个是签名不匹配。面试过一些候选人问起插件为什么没生效第一反应往往是怀疑注入方式有问题但实际95%的情况是Signature里的args写错了要么参数类型不对要么参数顺序不对。我的排查顺序是先确认MyBatis版本再确认Executor方法的完整签名然后用临时日志在plugin方法里打印target.getClass()如果代理类确实生成了再检查签名。还有一种隐蔽的情况项目里同时存在多个SqlSessionFactory比如多数据源配置时插件只注册到了其中一个factory自然只对走那个数据源的Mapper生效。排查方法是在注册逻辑里打印factory对象的标识逐个确认。6.2 代理链顺序导致的逻辑错乱多个插件同时存在时最麻烦的是互相影响。我复盘过一个线上事故租户插件拼接了tenant_id条件分页插件统计count时拿到的是拼接后的SQL理论上是正确的但租户插件里用字符串截取去寻找WHERE关键字恰巧分页SQL外包了一层临时表别名截取逻辑直接误伤导致租户条件丢了。这个问题的根源不是分页插件而是租户插件对SQL结构的假设过于脆弱。给一线开发的建议是插件里的SQL改写代码尽量使用解析器而不是字符串硬处理如果团队暂不具备引入解析器的条件至少要写充分的集成测试把插件叠加场景、SQL多样性场景都覆盖进去。不要指望在线上靠日志还原SQL语法的各种变体。6.3 MetaObject与反射访问限制用MetaObject修改参数对象时最常遇到的异常是ReflectionException提示找不到setter或者非法访问。这通常是因为实体类的字段没有对应的set方法或者字段是final。遇到这种情况我会先检查实体类是否使用了编译期注解生成器生成的链式setter链式setter的方法名和参数列表其实兼容但如果你用的是其他代码生成工具生成的字段就得具体看了。另一个常见配置坑是MyBatis的mapUnderscoreToCamelCase设置。它只影响结果映射和参数映射的属性名转换不影响你通过MetaObject访问属性但如果你在插件里通过字符串去匹配列名就要额外考虑列名到底用的是驼峰还是下划线风格统一用实体属性名做标准。6.4 分页插件中count查询的性能隐患自研分页插件最容易出现性能问题的就是count查询。最朴素的包裹式count在数据量大、多表关联时性能衰减非常厉害。改进方向有两个第一尽量让count查询去掉非必要的LEFT JOIN只有确需过滤时才保留关联纯where过滤的表子查询或exists能明显降低开销第二给规则化的SQL配置count专用语句即一个查询对应一条手工优化过的count语句适合高并发核心接口。我见过有些团队做的分页组件干脆把count语句维护成XML里的一段独立配置插件发现存在配置就优先用配置没有配置才走自动化生成。这样把性能敏感的部分交给最熟悉业务的人去优化是一种很务实的折中方案。7. 插件本身的测试与发布心得7.1 集成测试怎么搭插件代码和普通业务代码的测试方式不一样它不是常驻业务代码的一部分高度依赖MyBatis运行时环境所以老老实实写集成测试。我在模拟项目X里通常用一个独立的测试启动类内存数据库加载全部Mapper XML然后注册一个待测试插件逐个跑Mapper方法。断言内容要跟着插件行为走分页插件要断言返回结果的条数、count数值、以及SQL日志里出现LIMIT关键字审计插件要断言插入后的实体对象里createTime非空数据权限插件要断言带权限上下文的查询结果以及无权限上下文时是否直接放行。这些测试不复杂但胜在能锁定回归我愿称之为插件改造的“安全网”。7.2 插件发布前一定要做的自查清单写插件和写业务方法心态完全不同。业务方法出问题最多影响一个接口插件出问题往往影响全局所有SQL所以上线前一定要做几件事第一检查是否所有新拦截的方法都调用了proceed第二确认插件顺序符合业务预期特别是涉及SQL改写的两个插件第三用临时开关或配置中心开关控制插件是否启用万一线上出问题可以即时关闭第四压测时关注加了插件之后的吞吐量退化尤其是打印日志的插件会放大IO开销。这份清单这几年帮我拦下了不止一次线上故障现在每次交付插件我都是按这个顺序过一遍。关于MyBatis插件的实践我就聊到这里。最后分享一个个人体会插件是把双刃剑用得好的时候一个几百行的拦截器能替代一整个横切系统里的散落逻辑但用得不好也会因为代理链复杂、SQL改写隐蔽而变成排查黑洞。我自己的习惯是给每个插件都配上可观测性日志一次性输出拦截器名称、方法名、SQL原文和改写后SQL线上排查时打开日志基本十分钟内能定位问题。如果你也在写自己的第一个插件从耗时监控那个例子开始是最安全的。

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

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

免费获取报价 →
↑