Master MyBatis Plugins: Configuration, Development, and Execution Explained

This article explains how MyBatis supports plugins, covering configuration, how to write an Interceptor, the runtime mechanism, registration timing, initialization process, and the principles behind pagination plugins, with detailed code examples and annotations.

Programmer DD
Programmer DD
Programmer DD
Master MyBatis Plugins: Configuration, Development, and Execution Explained

1. Plugin Configuration

MyBatis plugins are defined inside the configuration element; during initialization they are read and stored in the InterceptorChain of the Configuration object.

<?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>
    <plugins>
        <plugin interceptor="com.mybatis3.interceptor.MyBatisInterceptor">
            <property name="value" value="100" />
        </plugin>
    </plugins>
</configuration>
public class Configuration {
    protected final InterceptorChain interceptorChain = new InterceptorChain();
}

The source of org.apache.ibatis.plugin.InterceptorChain shows a list of interceptors that are applied sequentially:

public class InterceptorChain {
    private final List<Interceptor> interceptors = new ArrayList<>();
    public Object pluginAll(Object target) {
        for (Interceptor interceptor : interceptors) {
            target = interceptor.plugin(target);
        }
        return target;
    }
    public void addInterceptor(Interceptor interceptor) {
        interceptors.add(interceptor);
    }
    public List<Interceptor> getInterceptors() {
        return Collections.unmodifiableList(interceptors);
    }
}

The loop ensures every plugin is executed in a chain, similar to an interceptor.

2. How to Write a Plugin

A plugin must implement the org.apache.ibatis.plugin.Interceptor interface:

public interface Interceptor {
    Object intercept(Invocation invocation) throws Throwable;
    Object plugin(Object target);
    void setProperties(Properties properties);
}

The three methods serve distinct purposes:

intercept() : contains the logic to be executed when the target method is intercepted.

plugin() : decides whether to wrap the target object and trigger intercept().

setProperties() : receives configuration properties from XML.

Example of a custom interceptor using annotations:

@Intercepts({
    @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
    @Signature(type = Executor.class, method = "close", args = {boolean.class})
})
public class MyBatisInterceptor implements Interceptor {
    private Integer value;
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        return invocation.proceed();
    }
    @Override
    public Object plugin(Object target) {
        System.out.println(value);
        // Plugin.wrap creates a JDK dynamic proxy that triggers intercept()
        return Plugin.wrap(target, this);
    }
    @Override
    public void setProperties(Properties properties) {
        value = Integer.valueOf((String) properties.get("value"));
    }
}
Why are the @Intercepts and @Signature annotations required? What do they mean?

MyBatis requires these annotations to specify which methods of which interfaces should be intercepted. Each @Signature defines a target type, method name, and argument list.

What does Plugin.wrap(target, this) do?

It creates a JDK dynamic proxy for the target object, enabling method interception and invoking intercept() when the specified method is called.

The source of org.apache.ibatis.plugin.Plugin shows how the proxy is built and how the signatureMap caches reflective method look‑ups to avoid repeated reflection costs:

public class Plugin implements InvocationHandler {
    private Object target;
    private Interceptor interceptor;
    private Map<Class<?>, Set<Method>> signatureMap;
    private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
        this.target = target;
        this.interceptor = interceptor;
        this.signatureMap = signatureMap;
    }
    public static Object wrap(Object target, Interceptor interceptor) {
        Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
        Class<?> type = target.getClass();
        Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
        if (interfaces.length > 0) {
            return Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap));
        }
        return target;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            Set<Method> methods = signatureMap.get(method.getDeclaringClass());
            // If the method is in the signature map, invoke interceptor
            if (methods != null && methods.contains(method)) {
                return interceptor.intercept(new Invocation(target, method, args));
            }
            return method.invoke(target, args);
        } catch (Exception e) {
            throw ExceptionUtil.unwrapThrowable(e);
        }
    }
}

3. Which MyBatis Interfaces Can Be Intercepted?

MyBatis can intercept methods of four core interfaces: ParameterHandler, ResultSetHandler, StatementHandler, and Executor. The InterceptorChain.pluginAll() method registers the interceptor for each of these objects during their creation.

public ParameterHandler newParameterHandler(MappedStatement ms, Object param, BoundSql bs) {
    ParameterHandler ph = ms.getLang().createParameterHandler(ms, param, bs);
    ph = (ParameterHandler) interceptorChain.pluginAll(ph); // 1
    return ph;
}
public ResultSetHandler newResultSetHandler(Executor ex, MappedStatement ms, RowBounds rb, ParameterHandler ph, ResultHandler rh, BoundSql bs) {
    ResultSetHandler rsh = new DefaultResultSetHandler(ex, ms, ph, rh, bs, rb);
    rsh = (ResultSetHandler) interceptorChain.pluginAll(rsh); // 2
    return rsh;
}
public StatementHandler newStatementHandler(Executor ex, MappedStatement ms, Object param, RowBounds rb, ResultHandler rh, BoundSql bs) {
    StatementHandler sh = new RoutingStatementHandler(ex, ms, param, rb, rh, bs);
    sh = (StatementHandler) interceptorChain.pluginAll(sh); // 3
    return sh;
}
public Executor newExecutor(Transaction tx, ExecutorType type) {
    // ... create executor based on type ...
    executor = (Executor) interceptorChain.pluginAll(executor); // 4
    return executor;
}

Registration occurs when the objects are created; actual interception happens later when the registered methods are invoked.

4. Invocation

public class Invocation {
    private Object target;
    private Method method;
    private Object[] args;
}

The intercept(Invocation invocation) method receives this object, allowing the plugin to proceed with the original method call via invocation.proceed().

5. Initialization of Plugins

During configuration parsing, MyBatis reads the <plugins> element, creates interceptor instances, calls setProperties(), and adds them to the configuration:

pluginElement(root.evalNode("plugins"));
private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
        for (XNode child : parent.getChildren()) {
            String interceptor = child.getStringAttribute("interceptor");
            Properties properties = child.getChildrenAsProperties();
            Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
            // setProperties is invoked here
            interceptorInstance.setProperties(properties);
            configuration.addInterceptor(interceptorInstance);
        }
    }
}

All plugins share the same Interceptor type; MyBatis relies on annotations to determine which methods to intercept.

6. Pagination Plugin Principle

MyBatis performs logical pagination by default. Physical pagination plugins intercept the StatementHandler ’s query method, obtain the SQL string from the BoundSql object, and rewrite it.

public interface StatementHandler {
    <E> List<E> query(Statement stmt, ResultHandler resultHandler) throws SQLException;
    BoundSql getBoundSql();
}
public class BoundSql {
    public String getSql() {
        return sql;
    }
}

By creating a StatementHandler interceptor that modifies the SQL before execution, developers can achieve physical pagination.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaBackend DevelopmentpluginMyBatisORMInterceptor
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.