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.wrapinplugin()and filter by @Signature so you only wrap the intended target type.
| Scenario | Suited for plugin? | Notes |
|---|---|---|
| Add tenant / logical-delete to all SQL | Yes | Centralized rewrite, hard to miss |
| Slow SQL logging / metrics | Yes | Centralized, non-invasive |
| Pagination | Yes (or use PageHelper) | Rewrite SQL and count |
| Logic for a few Mappers only | Usually no | AOP or Mapper-level logic is clearer |
| Sensitive data masking | Yes (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. Inplugin(), usePlugin.wrapand 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.