Plugin/Interceptor - When to Use

MyBatis plugins (Interceptors) use JDK dynamic proxies to intercept methods on Executor, ParameterHandler, ResultSetHandler, and StatementHandler. They run before or after SQL execution for cross-cutting logic like pagination, multi-tenancy, masking, or slow-SQL logging. This article explains the mechanism, common use cases, and implementation notes with examples.

Overview

  • Interceptable components: Executor (execution), ParameterHandler (parameters), ResultSetHandler (results), StatementHandler (statement). Most common for plugins: StatementHandler (SQL modification, logging) and Executor (cache, batch).
  • When to use: When you need the same behavior for all SQL — e.g. add tenant conditions, logical delete filters, pagination, sensitive-field masking, slow-SQL metrics, or parameter/result encryption — and you do not want to repeat it in every Mapper.
  • When not to use: For simple or Mapper-specific logic, AOP or dynamic SQL in the Mapper is clearer. Plugins affect all SQL that goes through the component; be aware of scope and performance.

Example

Example 1: Slow SQL logging plugin

Java
@Intercepts({
    @Signature(type = StatementHandler.class, method = "query",
               args = {Statement.class, ResultHandler.class})
})
public class SqlLogPlugin implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler sh = (StatementHandler) invocation.getTarget();
        BoundSql boundSql = sh.getBoundSql();
        String sql = boundSql.getSql();
        long start = System.currentTimeMillis();
        Object result = invocation.proceed();
        long cost = System.currentTimeMillis() - start;
        if (cost > 1000) {
            log.warn("Slow SQL: {} ms, sql: {}", cost, sql);
        }
        return result;
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }
}
  • Register in mybatis-config.xml. All query executions go through the plugin. In production, consider sampling or logging only slow queries. Mask parameters if they contain sensitive data.

Example 2: Multi-tenant — add tenant_id condition

  • Before StatementHandler prepares the statement, read the current tenant id from ThreadLocal, parse the SQL, add tenant_id = ? to the WHERE clause, and add the parameter to BoundSql. Requires SQL parsing (e.g. jsqlparser). Handle subqueries and JOINs; decide which tables need the tenant filter. Add tests to avoid incorrect or missing rewrites.

Example 3: Pagination plugin (concept)

  • Intercept Executor or StatementHandler, detect pagination (e.g. by MappedStatement id or parameter type), wrap the original SQL as SELECT * FROM (original) tmp LIMIT ?, ? (or DB-specific dialect), and add a count query. PageHelper is a well-known example; custom plugins must handle multiple data sources and dialects.

Example 4: Plugin registration

XML
<plugins>
  <plugin interceptor="com.example.SqlLogPlugin"/>
</plugin>
  • Plugins form a chain in configuration order. Use Plugin.wrap in plugin() and filter by @Signature so you only wrap the intended target type.
ScenarioSuited for plugin?Notes
Add tenant / logical-delete to all SQLYesCentralized rewrite, hard to miss
Slow SQL logging / metricsYesCentralized, non-invasive
PaginationYes (or use PageHelper)Rewrite SQL and count
Logic for a few Mappers onlyUsually noAOP or Mapper-level logic is clearer
Sensitive data maskingYes (ResultSetHandler)Centralized at result layer

Core Mechanism / Behavior

  • Proxy chain: MyBatis wraps the four components with proxies. Multiple plugins wrap in order. invocation.proceed() calls the next plugin or the real object.
  • @Signature: Specifies interface, method name, and argument types. Only matching invocations go to intercept. In plugin(), use Plugin.wrap and filter by @Signature so you only wrap the right types.
  • SQL rewriting: Get BoundSql from StatementHandler; replace SQL and parameters. Ensure placeholder count matches parameters. For complex rewrites, use a SQL parser; avoid fragile regex.

Key Rules

  • Plugins suit cross-cutting, uniform logic (tenant, pagination, logging, masking). Keep Mapper-specific logic in Mappers or services.
  • When rewriting SQL, consider subqueries, UNION, and multi-statement. Test thoroughly; validate in staging before production.
  • In plugin(), use Plugin.wrap and filter by @Signature to avoid wrapping the wrong types and affecting performance or behavior.

What's Next

See Parameter Binding and Dynamic SQL for condition and security handling. Plugins sit above them as a global policy. If a plugin rewrites SQL, verify that cache keys still make sense with First/Second Level Cache.