乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 MyBatis概述与快速上手

    • 本章定位
    • MyBatis简介
    • 环境搭建
    • 第一个MyBatis程序
    • SqlSessionFactoryBuilder与openSession重载
    • SqlSessionFactory与SqlSession
    • SqlSession核心方法
    • 不使用 XML 构建 SqlSessionFactory
    • Mapper接口与映射方式
    • Java API 目录结构
  • 第2章 全局配置文件详解

    • 本章定位
    • properties
    • settings
    • typeAliases
    • typeHandlers
    • objectFactory
    • plugins
    • environments
    • transactionManager
    • dataSource
    • databaseIdProvider
    • mappers
    • 日志配置
  • 第3章 SQL映射文件基础

    • 本章定位
    • select
    • insert
    • update
    • delete
    • 参数传递与占位符
    • 主键生成策略
    • resultType
    • resultMap
    • 自动映射详解
    • sql片段
    • SQL 语句构建器
  • 第4章 动态SQL

    • 本章定位
    • if
    • choose、when、otherwise
    • where
    • set
    • foreach
    • trim
    • bind
    • script 元素:在注解映射器中启用动态 SQL
    • _databaseId 与动态 SQL 的多数据库支持
    • 动态 SQL 中插入脚本语言
  • 第5章 结果映射与关联查询

    • 本章定位
    • resultMap详解
    • association
    • collection
    • discriminator
    • N+1查询问题
    • 延迟加载
  • 第6章 MyBatis注解开发

    • 本章定位
    • @Select
    • @Insert
    • @Update
    • @Delete
    • @Param
    • @Options
    • @SelectKey
    • @Results
    • @Result
    • @One
    • @Many
    • @SelectProvider
  • 第7章 缓存与性能优化

    • 本章定位
    • 一级缓存
    • 二级缓存
    • 缓存配置详解
    • 自定义缓存
    • Executor执行器类型
    • 分页插件

plugins

导学

本节学习目标:

  • 理解 plugins 是 MyBatis 提供的"横切干预"机制,可在 SQL 执行全链路中插入自定义逻辑
  • 掌握四大可拦截对象:Executor、StatementHandler、ParameterHandler、ResultSetHandler
  • 能够使用 @Intercepts 和 @Signature 注解编写一个完整的插件
  • 了解插件的注册方式与拦截链执行顺序

定义

在实际项目中,我们经常需要在不修改原有 Mapper 和业务代码的前提下,为 SQL 执行增加通用功能:

  • 打印执行的 SQL 语句及耗时,方便调试
  • 对慢 SQL 进行监控和告警
  • 实现物理分页逻辑
  • 对查询结果进行数据脱敏或权限过滤

MyBatis 的 plugins 机制通过动态代理拦截四大核心接口的方法调用,让开发者可以在 SQL 执行前后插入自定义逻辑,是实现上述需求的官方标准方式。


适用位置与核心属性

plugins 位于 mybatis-config.xml 中 objectFactory 之后、environments 之前。

<plugins>
  <plugin interceptor="com.feixiang.plugin.SqlExecutionTimePlugin">
    <property name="slowThreshold" value="1000"/>
  </plugin>
</plugins>
属性是否必填说明
interceptor必填插件类的全限定名,必须实现 Interceptor 接口

子标签 property 用于向插件实例注入配置属性。

核心注解

注解作用
@Intercepts声明这是一个拦截器,内部包含一个或多个 @Signature
@Signature声明具体拦截哪个接口的哪个方法,包含 type(接口类)、method(方法名)、args(参数类型数组)

核心原理

插件拦截链流程图

动态代理机制:MyBatis 使用 JDK 动态代理为四大对象创建代理实例。每个插件按注册顺序层层包裹目标对象,形成责任链。调用时从外层插件依次向内执行,返回时从内层依次向外执行。

可拦截的四大对象及常用方法:

接口常用拦截方法典型应用场景
Executorquery, update, commit, rollback分页、缓存、事务监控
StatementHandlerprepare, parameterize, batch, update, querySQL 改写、打印
ParameterHandlersetParameters参数加密、参数校验
ResultSetHandlerhandleResultSets, handleOutputParameters结果脱敏、数据过滤

完整示例

场景说明

乐途公司员工管理系统的开发团队需要监控所有 SQL 的执行耗时,当某条 SQL 执行超过 1000ms 时,在日志中输出慢查询告警。要求不修改任何 Mapper XML 和业务代码。

操作前的状态

未配置插件时,MyBatis 仅输出 DEBUG 级别的 SQL 语句,无法统计执行耗时,也无法识别慢查询。

完整配置代码

步骤一:实现 SQL 执行耗时监控插件

package com.feixiang.plugin;

import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;

import java.sql.Connection;
import java.util.Properties;

@Intercepts({
    @Signature(
        type = StatementHandler.class,
        method = "prepare",
        args = {Connection.class, Integer.class}
    )
})
public class SqlExecutionTimePlugin implements Interceptor {

    private long slowThreshold = 1000; // 慢查询阈值,单位毫秒

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 获取目标 StatementHandler
        StatementHandler target = (StatementHandler) invocation.getTarget();
        MetaObject metaObject = SystemMetaObject.forObject(target);

        // 获取即将执行的 SQL(通过 BoundSql)
        String sql = (String) metaObject.getValue("delegate.boundSql.sql");
        sql = sql.replaceAll("[\s]+", " "); // 压缩多余空白

        long start = System.currentTimeMillis();
        try {
            // 执行目标方法
            return invocation.proceed();
        } finally {
            long cost = System.currentTimeMillis() - start;
            if (cost > slowThreshold) {
                System.out.println("【慢查询告警】SQL: " + sql + " | 耗时: " + cost + "ms");
            } else {
                System.out.println("【SQL监控】SQL: " + sql + " | 耗时: " + cost + "ms");
            }
        }
    }

    @Override
    public Object plugin(Object target) {
        // 使用 MyBatis 提供的 Plugin.wrap 创建代理
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        String threshold = properties.getProperty("slowThreshold");
        if (threshold != null) {
            this.slowThreshold = Long.parseLong(threshold);
        }
        System.out.println("【SqlExecutionTimePlugin】初始化完成,慢查询阈值: " + slowThreshold + "ms");
    }
}

步骤二:在 mybatis-config.xml 中注册插件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

  <properties resource="db.properties"/>
  <settings>
    <setting name="mapUnderscoreToCamelCase" value="true"/>
  </settings>
  <typeAliases>
    <package name="com.feixiang.entity"/>
  </typeAliases>

  <!-- 插件配置 -->
  <plugins>
    <plugin interceptor="com.feixiang.plugin.SqlExecutionTimePlugin">
      <property name="slowThreshold" value="1000"/>
    </plugin>
  </plugins>

  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="${driver}"/>
        <property name="url" value="${url}"/>
        <property name="username" value="${username}"/>
        <property name="password" value="${password}"/>
      </dataSource>
    </environment>
  </environments>

  <mappers>
    <mapper resource="mapper/EmployeeMapper.xml"/>
  </mappers>

</configuration>

步骤三:Java 测试代码

public class PluginDemo {
    public static void main(String[] args) throws Exception {
        InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);

        try (SqlSession session = factory.openSession()) {
            EmployeeMapper mapper = session.getMapper(EmployeeMapper.class);
            
            // 执行查询
            Employee emp = mapper.selectById(1);
            System.out.println("查询结果: " + emp.getEmployeeName());
        }
    }
}

实际效果/结果

控制台输出:

【SqlExecutionTimePlugin】初始化完成,慢查询阈值: 1000ms
【SQL监控】SQL: SELECT employee_id, employee_name, department_code, create_time FROM employee WHERE employee_id = ? | 耗时: 15ms
查询结果: 王五

若模拟慢查询(如在数据库端添加 SLEEP(2)):

【慢查询告警】SQL: SELECT employee_id, employee_name, department_code, create_time FROM employee WHERE employee_id = ? | 耗时: 2015ms

分析

  • @Signature 必须精确匹配方法的参数类型数组,包括顺序。StatementHandler.prepare(Connection, Integer) 的参数是 Connection.class 和 Integer.class,不能写错
  • Plugin.wrap(target, this) 是 MyBatis 提供的标准代理创建方式,它会检查目标对象是否属于 @Signature 中声明的接口类型,只有匹配时才创建代理
  • invocation.proceed() 是调用链继续向下执行的关键,若忘记调用则目标方法不会执行
  • 多个插件按 XML 中注册顺序层层包裹,先注册的插件在外层,后注册的在内层

易错场景/常见误区

误区错误表现正解
@Signature 的 args 写错参数类型插件完全不生效,目标方法直接执行必须与被拦截方法的参数类型、顺序完全一致。注意包装类与基本类型的区别(如 Integer.class 而非 int.class)
忘记调用 invocation.proceed()SQL 不执行,返回 null 或空结果intercept 方法中必须通过 invocation.proceed() 将调用传递给下一个插件或目标对象
使用 Plugin.wrap 但目标对象未实现 @Signature 中的接口代理未创建,插件不生效Plugin.wrap 内部会检查接口匹配性,确保 type 属性与目标对象类型一致
多个插件顺序不当导致逻辑混乱分页插件在慢查询插件之后,导致耗时统计不包含分页改写时间理解代理链顺序:先注册在外层。根据业务需求调整 XML 中 <plugin> 的排列顺序
试图拦截 SqlSession 的方法@Signature(type = SqlSession.class) 不生效插件只能拦截 Executor、StatementHandler、ParameterHandler、ResultSetHandler 四个接口
在 plugin() 方法中直接返回 this类型转换异常,启动失败必须返回 Plugin.wrap(target, this) 创建的代理对象,而非拦截器自身

面试考点

Q1:MyBatis 插件可以拦截哪些对象?分别适用于什么场景?

A:可以拦截四大核心对象:

  • Executor:拦截增删改查和事务操作,适用于分页、缓存、全局事务监控
  • StatementHandler:拦截 SQL 的预编译和执行,适用于 SQL 打印、改写、耗时监控
  • ParameterHandler:拦截参数设置,适用于参数加密、脱敏、校验
  • ResultSetHandler:拦截结果集处理,适用于结果脱敏、数据权限过滤、结果转换

Q2:多个插件同时注册时,它们的执行顺序是怎样的?

A:MyBatis 按照 XML 中 <plugin> 的注册顺序,使用责任链模式层层包裹目标对象。先注册的插件在外层,后注册的在内层。方法调用时从外层向内层传递,返回时从内层向外层回溯。因此外层插件的 "前置逻辑" 最先执行,"后置逻辑" 最后执行。

Q3:为什么 MyBatis 插件基于 JDK 动态代理而不是 CGLIB?

A:因为被拦截的四大对象(Executor、StatementHandler 等)都是接口实现类,JDK 动态代理足以满足需求。使用 JDK 代理更轻量,且避免了 CGLIB 可能带来的类加载和字节码操作问题。MyBatis 的 Plugin.wrap() 内部正是使用 Proxy.newProxyInstance() 实现。

Q4:如何确保插件只拦截特定的方法而不影响其他方法?

A:通过 @Signature 精确声明 type(接口类)、method(方法名)和 args(参数类型数组)。Plugin.wrap() 在创建代理时会检查当前目标对象是否实现了 @Signature 中声明的接口,以及当前方法是否匹配签名,只有完全匹配才会进入 intercept 方法。


小结

plugins 是 MyBatis 扩展能力的"瑞士军刀",它通过责任链动态代理机制,在不侵入业务代码的前提下实现了 SQL 监控、分页、脱敏等横切需求。编写插件的关键在于:精确声明 @Signature、正确调用 invocation.proceed()、使用 Plugin.wrap() 创建代理。理解拦截链的顺序和四大对象的分工,是写出高质量插件的基础。


下一章引子

插件让我们能够干预 SQL 执行的内部链路,但 SQL 最终要落到具体的数据库上执行。不同的环境(开发、测试、生产)需要连接不同的数据库,甚至同一应用需要支持多套数据源。MyBatis 的 environments 元素提供了多环境配置与切换的能力,接下来我们将学习如何配置和管理数据库环境。

上一页
objectFactory
下一页
environments