How to Build Custom MyBatis Plugins: A Step‑by‑Step Guide

This article explains how MyBatis plugins work, shows how to create a custom interceptor with Java code, configure it in XML, and details the underlying mechanism that uses dynamic proxies to intercept Executor methods.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
How to Build Custom MyBatis Plugins: A Step‑by‑Step Guide

Overview

MyBatis allows you to intercept method calls at specific points during the execution of mapped statements. By default, the framework provides plugin interception for the following components:

Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)

ParameterHandler (getParameterObject, setParameters)

ResultSetHandler (handleResultSets, handleOutputParameters)

StatementHandler (prepare, parameterize, batch, update, query)

The method signatures inside the parentheses indicate the methods that can be intercepted.

Custom Plugin Development

@Intercepts({ @Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class }) })
public class ExecutorPlugin implements Interceptor {
    private Properties properties = new Properties();

    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println(properties);
        // implement pre‑processing if needed
        Object returnObject = invocation.proceed();
        // implement post‑processing if needed
        return returnObject;
    }

    public void setProperties(Properties properties) {
        this.properties = properties;
    }
}

Configuring the Plugin

<?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.pack.interceptors.ExecutorPlugin">
      <property name="pageSize" value="100" />
    </plugin>
  </plugins>
</configuration>

After adding the above configuration, the custom plugin is active.

Principle Analysis

Entry Point

// Obtain a SqlSession instance via the factory
SqlSession session = sqlSessionFactory.openSession();

Creating the Executor

public class DefaultSqlSessionFactory implements SqlSessionFactory {
    private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
        Transaction tx = null;
        try {
            // ...
            // Create the executor
            final Executor executor = configuration.newExecutor(tx, execType);
            return new DefaultSqlSession(configuration, executor, autoCommit);
        }
        // ...
    }
}

The Configuration#newExecutor method builds the executor:

public class Configuration {
    public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
        executorType = executorType == null ? defaultExecutorType : executorType;
        executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
        Executor executor;
        if (ExecutorType.BATCH == executorType) {
            executor = new BatchExecutor(this, transaction);
        } else if (ExecutorType.REUSE == executorType) {
            executor = new ReuseExecutor(this, transaction);
        } else {
            executor = new SimpleExecutor(this, transaction);
        }
        if (cacheEnabled) {
            executor = new CachingExecutor(executor);
        }
        // Apply interceptors
        executor = (Executor) interceptorChain.pluginAll(executor);
        return executor;
    }
}

Applying Interceptors

public class InterceptorChain {
    private final List<Interceptor> interceptors = new ArrayList<>();

    public Object pluginAll(Object target) {
        // Iterate over interceptor objects
        for (Interceptor interceptor : interceptors) {
            // Core method: execute Interceptor#plugin
            target = interceptor.plugin(target);
        }
        return target;
    }
    // ...
}

Interceptor Interface

public interface Interceptor {
    // Method that custom interceptors must implement
    Object intercept(Invocation invocation) throws Throwable;

    default Object plugin(Object target) {
        // Wrap the target (e.g., Executor) with the custom interceptor
        return Plugin.wrap(target, this);
    }
    // ...
}

Plugin Implementation (Dynamic Proxy)

public class Plugin implements InvocationHandler {
    public static Object wrap(Object target, Interceptor interceptor) {
        // Build a map of signatures from @Intercepts annotation
        Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
        Class<?> type = target.getClass();
        // Get all interfaces of the target that need to be proxied
        Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
        if (interfaces.length > 0) {
            // Create proxy object
            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 (methods != null && methods.contains(method)) {
                // Intercepted method; invoke interceptor
                return interceptor.intercept(new Invocation(target, method, args));
            }
            return method.invoke(target, args);
        } catch (Exception e) {
            throw ExceptionUtil.unwrapThrowable(e);
        }
    }
}

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

    public Invocation(Object target, Method method, Object[] args) {
        this.target = target;
        this.method = method;
        this.args = args;
    }

    public Object proceed() throws InvocationTargetException, IllegalAccessException {
        return method.invoke(target, args);
    }
}

Through the analysis above, MyBatis interceptors achieve method interception by dynamically proxying the concrete Executor object.

End of tutorial.

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.

BackendJavapluginMyBatisORMInterceptor
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

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.