当前位置: 代码网 > it编程>编程语言>Java > 一文教你如何自定义一个MyBatis插件

一文教你如何自定义一个MyBatis插件

2026年09月15日 Java 我要评论
面试官您好,我来回答一下如何自定义 mybatis 插件这个问题。先讲核心原理mybatis 插件本质上是基于jdk 动态代理 + 责任链模式实现的,它允许我们在 mybatis 执行 sql 的关键

面试官您好,我来回答一下如何自定义 mybatis 插件这个问题。

先讲核心原理 

mybatis 插件本质上是基于jdk 动态代理 + 责任链模式实现的,它允许我们在 mybatis 执行 sql 的关键节点进行拦截和增强。

mybatis 定义了四大核心对象可以被拦截:

拦截对象主要作用常用拦截方法
executor执行增删改查的调度器update、query、commit、rollback
statementhandler处理 sql 语句构建prepare、parameterize、batch、update、query
parameterhandler处理 sql 参数映射setparameters
resultsethandler处理结果集映射handleresultsets、handleoutputparameters

自定义插件的完整步骤

我用一个流程图来展示完整的开发流程:

1. 基础骨架代码

@intercepts({
    @signature(
        type = statementhandler.class,
        method = "prepare",
        args = {connection.class, integer.class}
    )
})
public class sqllogplugin implements interceptor {
    
    @override
    public object intercept(invocation invocation) throws throwable {
        // 增强逻辑
        return invocation.proceed();
    }
    
    @override
    public object plugin(object target) {
        return plugin.wrap(target, this);
    }
    
    @override
    public void setproperties(properties properties) {
    }
}

2. 注册插件

方式一:xml 配置

<configuration>
    <plugins>
        <plugin interceptor="com.example.sqllogplugin"/>
    </plugins>
</configuration>

方式二:springboot 自动配置

@configuration
public class mybatisconfig {
    @bean
    public sqllogplugin sqllogplugin() {
        return new sqllogplugin();
    }
}

关键技术点

  1. @intercepts 和 @signature 注解:精确指定要拦截哪个对象的哪个方法
  2. invocation 对象:包含了被代理对象、方法和参数,调用proceed()执行原方法
  3. metaobject 工具类:mybatis 提供的反射工具,方便获取对象的私有属性
  4. plugin.wrap():自动判断是否需要生成代理对象,只有匹配的对象才会被代理

常见应用场景

  • sql 执行日志打印(带参数)
  • 分页插件(如 pagehelper)
  • 数据权限过滤
  • 慢 sql 监控
  • 读写分离
  • 公共字段自动填充

注意事项和坑点

  1. 拦截顺序问题:多个插件会形成责任链,先注册的先拦截,后注册的先执行
  2. 不要随意修改原对象:除非你非常清楚后果,否则可能导致不可预期的问题
  3. 性能影响:拦截器会增加额外的方法调用开销,不要在拦截器中做耗时操作
  4. 异常处理:拦截器中的异常会影响 sql 执行,需要妥善处理
  5. 版本兼容性:不同版本的 mybatis 方法签名可能会有变化

核心代码与技术亮点

我给您展示一个实际项目中使用的数据权限过滤插件,这是我觉得最能体现技术深度的示例:

/**
 * 数据权限过滤插件(技术亮点版)
 * 基于用户id自动过滤数据,只允许查看自己创建的数据
 */
@intercepts({
    @signature(
        type = statementhandler.class,
        method = "prepare",
        args = {connection.class, integer.class}
    )
})
@component
@order(1) // ✅ 技术亮点1:指定执行顺序,确保在分页插件之前执行
public class datapermissionplugin implements interceptor {

    // 不需要权限过滤的方法白名单
    private static final set<string> exclude_methods = set.of(
        "getbyid", "listall", "countall", "adminquery"
    );

    @override
    public object intercept(invocation invocation) throws throwable {
        // 1. 获取mybatis内部核心对象
        statementhandler statementhandler = (statementhandler) invocation.gettarget();
        // ✅ 技术亮点2:使用官方metaobject工具安全反射,避免手写反射代码
        metaobject metaobject = systemmetaobject.forobject(statementhandler);
        mappedstatement ms = (mappedstatement) metaobject.getvalue("delegate.mappedstatement");
        
        // 2. 白名单过滤,不需要权限控制的方法直接放行
        string methodid = ms.getid();
        string methodname = methodid.substring(methodid.lastindexof(".") + 1);
        if (exclude_methods.contains(methodname)) {
            return invocation.proceed();
        }

        // 3. 获取当前登录用户(从统一安全上下文获取,与业务完全解耦)
        long currentuserid = securitycontextholder.getcontext().getuserid();
        if (currentuserid == null) {
            throw new accessdeniedexception("用户未登录,无法进行数据权限校验");
        }

        // 4. 获取原始sql(此时动态sql已完全解析完成)
        boundsql boundsql = statementhandler.getboundsql();
        string originalsql = boundsql.getsql().trim();

        // ✅ 技术亮点3:使用jsqlparser解析sql抽象语法树,安全修改sql
        // 彻底解决字符串替换导致的sql语法错误问题(如子查询、多表联查、or条件)
        ccjsqlparsermanager parsermanager = new ccjsqlparsermanager();
        select select = (select) parsermanager.parse(new stringreader(originalsql));
        plainselect plainselect = (plainselect) select.getselectbody();

        // 5. 动态添加数据权限条件
        expression originalwhere = plainselect.getwhere();
        equalsto permissioncondition = new equalsto();
        permissioncondition.setleftexpression(new column("t.create_user_id"));
        permissioncondition.setrightexpression(new longvalue(currentuserid));

        if (originalwhere == null) {
            plainselect.setwhere(permissioncondition);
        } else {
            // 智能拼接and条件,不破坏原有where逻辑
            plainselect.setwhere(new andexpression(originalwhere, permissioncondition));
        }

        // 6. 将修改后的sql写回mybatis
        string newsql = select.tostring();
        metaobject.setvalue("delegate.boundsql.sql", newsql);

        // 7. 执行原方法
        return invocation.proceed();
    }

    @override
    public object plugin(object target) {
        return plugin.wrap(target, this);
    }

    @override
    public void setproperties(properties properties) {
    }
}

核心技术亮点总结

  1. 精确执行顺序控制:通过@order注解明确指定插件执行顺序,完美解决与分页插件的冲突
  2. 安全的 sql 修改方式:基于抽象语法树(ast)修改 sql,比字符串替换更可靠、更健壮
  3. 优雅的反射操作:使用 mybatis 官方metaobject工具,一行代码获取 / 修改任何私有属性
  4. 灵活的白名单机制:支持按方法名排除不需要权限控制的接口
  5. 完全解耦的设计:从统一安全上下文获取用户信息,插件与业务代码零耦合

常见技术难点与解决方案

我整理了实际开发中最容易遇到的 7 个技术难点,以及对应的解决方案:

技术难点核心问题解决方案关键代码示例
sql 修改导致语法错误简单字符串替换无法处理复杂 sql(子查询、or 条件、多表联查)使用jsqlparser解析 sql 抽象语法树,动态修改表达式节点select select=(select) parsermanager.parse(new stringreader(sql));
多插件执行顺序混乱分页插件在数据权限插件之前执行,导致分页后再过滤数据实现ordered接口或使用@order注解规则:数值越小,优先级越高@order(1) // 数据权限@order(2) // 分页
复杂参数无法获取无法获取嵌套对象、集合、@param注解的参数值使用metaobject递归获取参数值,支持 ognl 表达式object deptid=metaobject.getvalue("user.dept.id");
批量操作无法拦截批量插入 / 更新时,拦截器只执行一次拦截executor的batch方法,遍历所有待执行语句@signature(type=executor.class, method="batch", args={mappedstatement.class, object.class})
动态 sql 修改无效拦截时机不对,动态 sql 还未解析完成拦截statementhandlerprepare方法,此时 sql 已完全解析@signature(type=statementhandler.class, method="prepare", args={connection.class, integer.class})
版本兼容性问题不同 mybatis 版本的内部对象结构不同使用反射获取方法和属性,避免直接调用版本相关 apimethod getboundsql=target.getclass().getmethod("getboundsql");
无法区分不同 mapper 方法所有方法都被拦截,无法精确控制通过mappedstatement的id获取方法全名,进行白名单 / 黑名单过滤string methodid=ms.getid();

回答总结

自定义 mybatis 插件的核心就是:实现 interceptor 接口,通过注解指定拦截点,在 intercept 方法中编写增强逻辑,最后注册插件

它是 mybatis 提供的最强大的扩展机制之一,让我们可以在不修改框架源码的情况下,实现很多通用的横切功能。实际开发中,建议优先使用基于抽象语法树的 sql 修改方式,并通过@order注解明确控制多插件的执行顺序,这样能避免 90% 以上的坑。

真实面试模拟

面试官:你对 mybatis 应该比较熟悉了,那如果让你自定义一个插件,实现“监控所有执行超过1秒的慢sql”,你会怎么设计和实现?

我(候选人):嗯,这个问题我先从原理说起,再落到代码上,最后提一下实际开发中容易踩的坑,您看行吗?

面试官:可以,就这么讲。

我:

好嘞。首先 mybatis 插件的核心思想就是 责任链模式 + jdk 动态代理。它允许我们在四大对象的方法调用路径上嵌入自定义逻辑,这四大对象是:

  • executor:执行器,调度sql执行
  • statementhandler:处理statement的创建和参数设置
  • parameterhandler:参数处理器
  • resultsethandler:结果集映射

这些对象被创建时,都会经过 plugin.wrap() 方法,如果发现当前插件配置的拦截点和目标对象匹配,就用 jdk 动态代理生成一个代理对象,以后调目标方法,都会先进代理的 invoke(),然后再进咱们的 intercept()。多个插件就连成一条链了,像这样:

面试官:好,原理清楚了。那如果让你写代码实现这个慢sql监控插件,你怎么写?

我:

我会分三步走:实现接口、配置拦截点、注册插件。

首先拦截点我选 executorqueryupdate 方法,因为这里能直接拿到 mappedstatement,方便获取sql和参数。代码大概这样:

@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 slowsqlinterceptor implements interceptor {

    private long threshold = 1000; // 默认1秒

    @override
    public object intercept(invocation invocation) throws throwable {
        long start = system.currenttimemillis();
        object result = invocation.proceed(); // ⚡ 一定要调原方法
        long cost = system.currenttimemillis() - start;

        if (cost > threshold) {
            mappedstatement ms = (mappedstatement) invocation.getargs()[0];
            object param = invocation.getargs()[1];
            boundsql boundsql = ms.getboundsql(param);
            system.err.printf("🐢 慢sql [%dms] | %s\n", cost, boundsql.getsql());
        }
        return result;
    }

    @override
    public object plugin(object target) {
        return plugin.wrap(target, this); // 🔗 生成代理
    }

    @override
    public void setproperties(properties properties) {
        string th = properties.getproperty("threshold");
        if (th != null) this.threshold = long.parselong(th);
    }
}

面试官:代码结构挺清晰,那怎么注册这个插件呢?

我:

两种常见方式。

传统 xml 配置:在 mybatis-config.xml<plugins> 里加上:

<plugin interceptor="com.xxx.slowsqlinterceptor">
    <property name="threshold" value="2000"/>
</plugin>

spring boot 环境更简单,只要给拦截器类加上 @component,mybatis 的自动配置会自动把它加到 sqlsessionfactory 里,也可以手动通过 @bean 注入。

面试官:明白了。那你在这个插件里有没有踩过什么坑,或者有什么设计上的注意点?

我:

还真有几个点得留心:

  1. 方法签名必须严格匹配@signature 里的方法名和参数类型一个字母都不能差,比如 executor.update 有两个重载,我只拦带 mappedstatement 的那个,否则插件不生效。
  2. 别在拦截器里做重操作intercept() 会包在每个sql外面,我就做了个时间计算和条件打印,要是在里面做io或者复杂计算,整个业务都得变慢。
  3. 不要丢掉 invocation.proceed():必须调用并返回它的结果,除非你故意要拦截返回值,不然数据库操作直接没了。
  4. 插件顺序有讲究:多个插件按配置顺序层层包裹,像剥洋葱,先进后出。
  5. 要拿“动态参数替换后”的sql得谨慎:现在只打印原sql,如果要替换 ?,得用 metaobject 反射 statementhandler 里的 parameterhandler,但这会引入额外开销,监控场景一般不需要。

面试官:那除了慢sql监控,mybatis插件还能用在哪些场景?

我:

原理完全一样,只是增强逻辑不同。比如:

  • 分页插件:拦截 executor.query,判断是否需要分页,改写 sql 并设置分页参数。
  • 读写分离:根据方法名或注解判断走主库还是从库,动态切换数据源。
  • 乐观锁:在 update 方法执行前,自动给 sql 加上 version 判断逻辑。
  • 数据脱敏:在 resultsethandler 里对查询结果做统一脱敏处理。

面试官:刚刚你讲了监控慢sql插件的原理和思路,现在能把核心代码亮出来吗?最好说下你认为的技术亮点。

我:

没问题,我直接贴核心部分,边贴边讲亮点。

// ✨ 亮点1:精准的注解驱动配置
@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 slowsqlmonitor implements interceptor {
    
    // ✨ 亮点2:可外部化配置的阈值,通过setproperties动态注入
    private long threshold = 1000;

    @override
    public object intercept(invocation invocation) throws throwable {
        long starttime = system.currenttimemillis();
        // 保证原逻辑一定执行
        object result = invocation.proceed();
        long costtime = system.currenttimemillis() - starttime;

        if (costtime > threshold) {
            mappedstatement mappedstatement = (mappedstatement) invocation.getargs()[0];
            object parameter = invocation.getargs()[1];
            // ✨ 亮点3:直接从mappedstatement拿到boundsql,零侵入获取sql
            boundsql boundsql = mappedstatement.getboundsql(parameter);
            string sql = boundsql.getsql();
            
            // 生产可用日志框架,这里演示用标准错误流
            system.err.printf("🐢 慢sql [%dms] | %s | 参数: %s%n", 
                costtime, sql, parameter);
        }
        return result;
    }

    @override
    public object plugin(object target) {
        // ✨ 亮点4:利用官方工具生成代理,简洁且遵循规范
        return plugin.wrap(target, this);
    }

    @override
    public void setproperties(properties properties) {
        // ✨ 亮点5:阈值可通过xml或spring属性配置,提升灵活性
        if (properties.containskey("threshold")) {
            this.threshold = long.parselong(properties.getproperty("threshold"));
        }
    }
}

技术亮点总结

  • 注解精确匹配:通过 @intercepts@signature 声明拦截点,避免了硬编码判断,清晰且易维护。
  • 可配置阈值:利用 setproperties 接收外部参数,不改代码就能调灵敏度。
  • 轻量级监控:只做耗时统计和日志输出,不修改查询结果,不会污染业务数据。
  • 代理生成规范plugin.wrap(target, this) 是 mybatis 官方推荐写法,自动生成动态代理,避免手写代理的复杂度和错误。

面试官:代码挺漂亮。那在实际开发这个插件的过程中,你觉得有哪些技术难点?又是怎么解决的?

我:

确实有几个点需要小心处理,我一并整理下:

技术难点 产生原因解决方案 
拦截方法签名精确匹配难executor 等接口有多个重载方法,@signature 中方法名或参数类型写错一个字母就不会生效通过源码确认目标方法完整签名,拦截最基础的那个方法(如带 mappedstatementquery/update),避免覆盖不到或覆盖过度
获取完整可读sql不易boundsql.getsql() 拿到的是带 ? 的预编译sql,参数值在 parameterhandler 中,直接输出不直观监控场景下打印预编译sql+参数对象即可,若业务要求必须完整sql,可通过 metaobject 反射 parameterhandler 取出参数替换 ?,但要注意性能开销类型处理(如字符串加引号),建议用已有工具类(如 mybatis 自带的 generictokenparser)或开启 debug 日志
性能损耗控制拦截器包裹了每次数据库操作,逻辑稍重就会拖慢整体业务intercept 中只保留时间计算、条件判断和日志打印,绝不做 io、网络调用等耗时操作;同时采用异步日志(如 logback 的 asyncappender)降低写日志阻塞
多插件顺序引起的行为差异多个插件按配置顺序层层代理,像分页插件和慢sql监控插件如果顺序颠倒,可能导致耗时计算包含分页sql改写过程明确插件执行顺序,将基础监控类插件放在最外层(靠前配置),将改写sql的业务插件放在内层;必要时在文档中约定顺序
线程安全问题插件实例是单例的,如果里面存储了有状态数据(如累积统计数据),多线程并发会出错插件本身设计为无状态,若需统计,使用线程安全的 concurrenthashmapatomiclong,且做好清理策略,避免内存泄漏
与spring boot集成的配置复杂性早期 mybatis-spring 版本插件注册需要手动 interceptor bean,且 setproperties 不生效目前新版 mybatis-spring-boot-starter 只要把插件类加上 @component,并可通过 @configurationproperties 或 xml 的 `` 注入参数,直接用 application.yml 自定义配置映射即可

我:另外补充一个小技巧,如果想在插件里拿到 spring 容器中的 bean(比如告警服务),可以让插件实现 applicationcontextaware,或者用 @component 注入,但要注意别把插件逻辑搞得太重,保持纯粹。

以上就是一文教你如何自定义一个mybatis插件的详细内容,更多关于mybatis自定义插件的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com