How MyBatis Interceptor Plugins Really Work—and How to Build One Correctly

Published:

org.apache.ibatis.plugin.Interceptor is the foundation of MyBatis’s plugin mechanism. Every custom MyBatis plugin starts here.

What this interface gives you is a way to step into key parts of MyBatis’s execution flow through dynamic proxies, then add your own behavior without touching the framework source code. That is why features such as pagination, SQL logging, performance measurement, permission filtering, parameter encryption, and result decryption are commonly implemented as plugins.

In practical terms, a MyBatis plugin is not just “a class with custom logic.” It must implement Interceptor, declare exactly what it wants to intercept through annotations and configuration, and let MyBatis wrap the target component with a proxy when the application starts.

What Interceptor is responsible for

MyBatis plugins are essentially dynamic-proxy enhancements applied to four internal execution components. The Interceptor interface defines the common contract for that process, covering three things:

  • how interception rules are matched
  • where the actual interception logic is written
  • how plugin-specific configuration parameters are received

From that design, several capabilities follow naturally:

  1. It can intercept key execution points in MyBatis, such as before SQL execution, after parameters are handled, or before results are returned.
  2. It allows both pre-processing and post-processing around the intercepted method—for example, rewriting SQL before execution or logging elapsed time afterward.
  3. It can read custom settings from configuration so the plugin behavior is adjustable.
  4. Multiple plugins can be chained together and executed in order.

The hard limit: MyBatis only allows plugins on four core components

A common misunderstanding is that MyBatis plugins can intercept anything inside the framework. They cannot.

The plugin system is hard-coded to work only with specific methods on four core components. When MyBatis creates these components during startup, it uses Interceptor to decide whether they should be wrapped in a proxy. That proxy then replaces the original instance.

The four components that can actually be intercepted

<table> <thead> <tr> <th>Component</th> <th>Role</th> <th>Typical use cases</th> </tr> </thead> <tbody> <tr> <td>Executor</td> <td>The SQL executor, responsible for query/update operations and cache handling</td> <td>Global pagination, execution monitoring, custom cache behavior</td> </tr> <tr> <td>StatementHandler</td> <td>Handles JDBC Statement preparation, SQL construction, parameter setting, and execution</td> <td>SQL rewriting, pagination SQL assembly, SQL logging</td> </tr> <tr> <td>ParameterHandler</td> <td>Converts Java parameters into JDBC-ready values</td> <td>Parameter encryption, filling default values for null parameters</td> </tr> <tr> <td>ResultSetHandler</td> <td>Maps JDBC result sets into Java objects</td> <td>Result decryption, data masking, post-processing result sets</td> </tr> </tbody> </table>

These components each have interfaces and concrete implementations—for example, Executor may be SimpleExecutor, ReuseExecutor, or BatchExecutor. Plugins target the interface contract, not a specific implementation class.

The Interceptor interface: three methods, one full lifecycle

The interface itself is small, but each method has a clear place in the plugin lifecycle:

package org.apache.ibatis.plugin;

import java.util.Properties;

public interface Interceptor {
    // 核心:拦截逻辑的实现方法
    Object intercept(Invocation invocation) throws Throwable;

    // 生成目标对象的代理对象(MyBatis 调用,用于将插件织入目标对象)
    default Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    // 加载插件时,设置配置文件中的自定义参数(可选)
    default void setProperties(Properties properties) {
        // 空实现,子类可重写
    }
}

Object intercept(Invocation invocation) throws Throwable

This is the heart of every plugin. Whenever MyBatis reaches a method that your plugin is configured to intercept, it calls this method.

The Invocation argument contains the details of the current call and also gives you a way to continue the original flow.

Important methods on Invocation include:

  • Object getTarget() — returns the intercepted target instance, such as a StatementHandler or Executor
  • Method getMethod() — returns the specific method being intercepted
  • Object[] getArgs() — returns the method arguments
  • Object proceed() throws Throwable — executes the original target method

That last one is critical. If proceed() is never called, the original MyBatis logic is blocked.

The return value of intercept is the return value of the original call, unless you intentionally modify it before returning. Exceptions thrown here bubble up and are handled by higher layers in MyBatis.

default Object plugin(Object target)

This method is used when MyBatis creates the proxy for a target object.

During startup, when one of the four core components is instantiated, MyBatis calls this method to determine whether the current interceptor should wrap that object. If it should, the method returns a proxy; if not, it simply returns the original target.

The default implementation is:

  • Plugin.wrap(target, this)

That is the standard implementation provided by MyBatis, built on JDK dynamic proxies. In most cases, there is no reason to override it.

default void setProperties(Properties properties)

This method is for plugin initialization through configuration.

When MyBatis loads the plugin, it passes any configured key-value parameters into setProperties. If your plugin needs custom settings—such as thresholds, modes, or formatting options—you can read them here and store them for later use.

The default implementation is empty, so if your plugin does not need external parameters, you can ignore it.

Declaring interception rules: @Intercepts and @Signature

Implementing Interceptor alone is not enough. MyBatis also needs to know exactly what the plugin should intercept.

That is what @Intercepts and @Signature are for. They live under org.apache.ibatis.plugin, and they must be used together.

@Intercepts

This annotation marks the class as a MyBatis plugin and holds one or more @Signature definitions.

A single plugin can intercept multiple methods across one or more supported components.

@Signature

This annotation defines one interception rule. It must specify all three of the following:

<table> <thead> <tr> <th>Property</th> <th>Type</th> <th>Meaning</th> </tr> </thead> <tbody> <tr> <td>type</td> <td>Class<?></td> <td>The core component interface to intercept: one of Executor, StatementHandler, ParameterHandler, or ResultSetHandler</td> </tr> <tr> <td>method</td> <td>String</td> <td>The method name to intercept</td> </tr> <tr> <td>args</td> <td>Class<?>[]</td> <td>The exact parameter type list for that method</td> </tr> </tbody> </table>

The match must be exact. If the method name, parameter order, parameter count, or parameter types do not align perfectly with the target interface definition, the plugin will not be triggered.

For example, if you want to intercept StatementHandler.prepare, first look at the method signature in the interface:

// StatementHandler 接口中的 prepare 方法
Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException;

Then the plugin declaration must match it exactly:

import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.executor.statement.StatementHandler;

// 标记为MyBatis插件,声明拦截规则
@Intercepts({
    @Signature(
        type = StatementHandler.class,  // 拦截StatementHandler组件
        method = "prepare",             // 拦截prepare方法
        args = {Connection.class, Integer.class}  // 方法参数类型(顺序一致)
    )
})
public class MySqlPlugin implements Interceptor {
    // 实现接口方法...
}

A detail that often causes silent failures: the types in args must be the original parameter types declared on the interface, not subclasses, and the order must remain unchanged.

Building a custom plugin: a full example

A good way to understand the mechanism is to build a simple SQL performance monitoring plugin. The idea is straightforward: intercept StatementHandler.prepare, record elapsed time, and print a warning for slow SQL.

Step 1: add the MyBatis dependency

In a Spring Boot application using MyBatis, the common setup looks like this:

<!-- Spring Boot + MyBatis 整合包(主流) -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.3.0</version>
</dependency>
<!-- 数据库驱动(以MySQL为例) -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

Step 2: implement Interceptor and declare the target method

The core logic lives in intercept. This example extracts the final SQL from StatementHandler, records the start time, executes the original method, then logs the elapsed time in a finally block so that timing is captured even if an exception occurs.

import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.Configuration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

/**
 * MyBatis插件:SQL执行性能监控(拦截StatementHandler的prepare方法,记录SQL执行耗时)
 */
// 声明拦截规则:拦截StatementHandler的prepare方法
@Intercepts({
    @Signature(
        type = StatementHandler.class,
        method = "prepare",
        args = {Connection.class, Integer.class}
    )
})
public class SqlPerformanceInterceptor implements Interceptor {
    // 日志打印
    private static final Logger log = LoggerFactory.getLogger(SqlPerformanceInterceptor.class);
    // 自定义参数:慢SQL阈值(单位:ms),默认500ms
    private long slowSqlThreshold = 500;

    /**
     * 核心拦截逻辑:记录SQL执行开始时间,执行后计算耗时,打印日志
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 1. 获取目标对象:StatementHandler(SQL处理器)
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        // 2. 获取当前执行的SQL语句(MyBatis已完成参数解析的最终SQL)
        String sql = statementHandler.getBoundSql().getSql().replaceAll("\\s+", " ");
        // 3. 记录SQL执行开始时间
        long startTime = System.currentTimeMillis();
        try {
            // 执行原目标方法(必须调用,否则SQL不会执行)
            return invocation.proceed();
        } finally {
            // 4. 计算执行耗时(finally保证无论是否异常都能统计)
            long costTime = System.currentTimeMillis() - startTime;
            // 5. 打印日志:慢SQL标红提示
            if (costTime > slowSqlThreshold) {
                log.warn("【慢SQL警告】执行耗时:{}ms,SQL:{}", costTime, sql);
            } else {
                log.info("【SQL执行】执行耗时:{}ms,SQL:{}", costTime, sql);
            }
        }
    }

    /**
     * 接收配置文件中的自定义参数(如慢SQL阈值)
     */
    @Override
    public void setProperties(Properties properties) {
        // 从properties中获取slowSqlThreshold参数,若配置则覆盖默认值
        String threshold = properties.getProperty("slowSqlThreshold");
        if (threshold != null && !threshold.isEmpty()) {
            this.slowSqlThreshold = Long.parseLong(threshold);
        }
    }

    // plugin方法使用默认实现(Plugin.wrap),无需重写
}

This example uses a default slow-SQL threshold of 500 ms and allows it to be overridden through configuration.

Step 3: register the plugin

A plugin does nothing until MyBatis loads it at startup. The registration style depends on the project setup.

Spring Boot + MyBatis

In a Spring Boot project, the usual approach is to register the plugin as a bean. MyBatis-Spring will pick it up automatically.

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Properties;

@Configuration
@MapperScan("com.xxx.mapper") // 扫描Mapper接口
public class MyBatisConfig {

    /**
     * 注册SQL性能监控插件
     */
    @Bean
    public SqlPerformanceInterceptor sqlPerformanceInterceptor() {
        SqlPerformanceInterceptor plugin = new SqlPerformanceInterceptor();
        // 设置自定义参数:慢SQL阈值为1000ms
        Properties properties = new Properties();
        properties.setProperty("slowSqlThreshold", "1000");
        plugin.setProperties(properties);
        return plugin;
    }
}

Plain MyBatis with XML configuration

If you are not using Spring Boot integration, configure the plugin directly in 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>
    <!-- 配置MyBatis插件 -->
    <plugins>
        <plugin interceptor="com.xxx.plugin.SqlPerformanceInterceptor">
            <!-- 自定义参数:慢SQL阈值1000ms -->
            <property name="slowSqlThreshold" value="1000"/>
        </plugin>
    </plugins>

    <!-- 其他配置:environments、mappers等 -->
</configuration>

Step 4: verify the behavior

Once the application starts, any database operation that reaches the intercepted method will produce timing logs. SQL slower than the configured threshold will be marked as slow SQL.

【SQL执行】执行耗时:12ms,SQL:select id, name, age from user where id = ?
【慢SQL警告】执行耗时:1200ms,SQL:select * from order where create_time between ? and ?

How multiple plugins are executed

When more than one MyBatis plugin is configured, MyBatis wraps the target component in multiple proxy layers according to configuration order. Invocation then follows a chain-of-responsibility style.

Suppose two plugins are registered in this order:

// 配置顺序1:性能监控插件
@Bean
public SqlPerformanceInterceptor sqlPerformanceInterceptor() { ... }
// 配置顺序2:数据脱敏插件
@Bean
public DataDesensitizeInterceptor dataDesensitizeInterceptor() { ... }

The call flow becomes:

proxy 2 (desensitization)proxy 1 (performance monitoring)original StatementHandler → SQL execution → return to original StatementHandlerproxy 1 post-processingproxy 2 post-processing → final result

If execution order matters, change the order of bean registration in Spring Boot or the <plugin> order in XML.

Common pitfalls and things worth watching closely

1. Only the four supported components are interceptable

If the target is not one of Executor, StatementHandler, ParameterHandler, or ResultSetHandler, the plugin will never take effect.

2. @Signature must match the method exactly

This includes:

  • method name
  • parameter count
  • parameter order
  • parameter types
  • case sensitivity

Any mismatch means no interception.

3. invocation.proceed() is usually mandatory

Skipping it interrupts the original MyBatis flow. That can prevent SQL execution, parameter binding, result mapping, or other expected framework behavior.

4. Avoid intercepting more than necessary

Because the mechanism is proxy-based, each plugin layer adds a small amount of overhead. Intercepting broad or unnecessary methods can introduce avoidable cost.

5. Be careful when changing arguments or return values

You can alter invocation.getArgs() or the returned result, but the modified values still need to remain valid for MyBatis’s internal expectations. Otherwise, subtle runtime errors may follow.

6. Plugin instances are singletons

MyBatis plugin instances are shared, so any member state must be thread-safe. Mutable shared fields should be handled carefully, and stateful designs should be avoided unless synchronization is deliberate.

7. Version differences may matter

Method definitions on the core components can vary slightly across MyBatis versions. A plugin written against one version may need adjustment for another, especially around the StatementHandler family in newer 3.5+ releases.

8. Watch for conflicts with third-party plugins

If the project already uses plugins such as PageHelper or general Mapper extensions, interception rules may overlap. For example, two plugins targeting Executor.query may both affect behavior. In many cases, ordering resolves the issue.

Typical real-world uses of Interceptor

The value of Interceptor is that it extends MyBatis without invasive changes. In enterprise projects, the most common scenarios include:

  1. Pagination plugins — for example, intercepting Executor.query to inject pagination SQL such as limit or rownum
  2. SQL logging and performance monitoring — intercepting StatementHandler.prepare to log SQL text, timing, and slow-query warnings
  3. Parameter processing — intercepting ParameterHandler.setParameters for encryption, default value filling, or parameter validation
  4. Result processing — intercepting ResultSetHandler.handleResultSets for masking, conversion, or permission-based filtering
  5. SQL interception and rewriting — intercepting StatementHandler.prepare to add tenant conditions or data-permission filters
  6. Transaction-related enhancement — intercepting Executor.update or Executor.query for custom transaction handling or distributed tracing hooks

What matters most to remember

Interceptor is the central extension point behind MyBatis plugins. Every custom plugin depends on it.

Its implementation is based on dynamic proxies, but that power is intentionally limited to four core MyBatis components. To make a plugin work, you need both the interface implementation and an exact interception declaration through @Intercepts and @Signature.

Among the three interface methods, intercept holds the actual logic, plugin creates the proxy layer—usually through the default Plugin.wrap—and setProperties receives configuration parameters.

Once registered, plugins can be combined, ordered, and used for tasks such as pagination, SQL monitoring, parameter transformation, result processing, masking, and other framework-level enhancements.