SoFunction
Updated on 2025-03-04

Detailed explanation of the process of implementing custom MyBatis plug-in for MyBatis

First knowing plug-ins

When we execute the query, if SQL does not add paging conditions, the amount of data will cause memory overflow. Therefore, we can intercept SQL through the plug-in mechanism provided by MyBatis and rewrite SQL. MyBatis plug-in is implemented through dynamic proxying and will form a plug-in chain. The principle is similar to an interceptor. It intercepts the objects we need to process, and after customizing the logic, it returns a proxy object to process the next interceptor.

Let’s first look at the template of the next simple plug-in. First, we need to implement an Interceptor interface and implement three methods. And add @Intercepts annotation. Next, we will explain each detail using the paging plug-in as an example.

/**
  * @ClassName : PagePlugin
  * @Description : Pagination plugin
  * @Date: 2020/12/29
  */
@Intercepts({})
public class PagePlugin implements Interceptor {
    
    private Properties properties;
    
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        return ();
    }

    @Override
    public Object plugin(Object target) {
        return (target, this);
    }

    @Override
    public void setProperties(Properties properties) {
         = properties;
    }
}

Intercept objects

When creating plug-in, you need to specify an intercept object.@InterceptsThe annotation specifies the method signature that needs to be intercepted, and the content isSignaturearray of type, andSignatureIt is a description of the intercepted object.

@Documented
@Retention()
@Target()
public @interface Intercepts {
  /**
   * Returns method signatures to intercept.
   *
   * @return method signatures
   */
  Signature[] value();
}

Signature requires specifying a description of the information about the method in the intercept object.

@Documented
@Retention()
@Target({})
public @interface Signature {
  /**
    * Object type
    */
  Class<?> type();

  /**
    * Method name
    */
  String method();

  /**
    * Parameter type
    */
  Class<?>[] args();
}

In MyBatis, we can only intercept the following four types of objects

  • ParameterHandler: Process sql parameters
  • ResultSetHandler: Process the result set object
  • StatementHandler: Processing SQL statements
  • Executor: Executor, perform addition, deletion, modification and search

Now we need to rewrite sql, so we can need to intercept the query method of Executor for intercepting

@Intercepts({@Signature(type = , 
                        method = "query", 
                        args = {, , , })})

Intercept implementation

In addition to specifying the interception method, each plug-in also needs to implement it.Interceptorinterface.InterceptorThere are three methods for the interface. Among them, intercept is the method we must implement, and we need to implement custom logic here. The other two methods give the default implementation.

public interface Interceptor {

  /**
    * Perform interception processing
    * @param invocation
    * @return
    * @throws Throwable
    */
  Object intercept(Invocation invocation) throws Throwable;

  /**
    * Return the proxy object
    * @param target
    * @return
    */
  default Object plugin(Object target) {
    return (target, this);
  }

  /**
    * Set configuration properties
    * @param properties
    */
  default void setProperties(Properties properties) {
    // NOP
  }

}

Therefore, we can implement the intercept method, because we want to rewrite the query SQL statement, so we need to intercept the Executor query method and then modify itlimit in RowBounds parameter, if limit is greater than 1000, we force it to set to 1000.

@Slf4j
@Intercepts({@Signature(type = ,
        method = "query",
        args = {, ,  , })})
public class PagePlugin implements Interceptor {

    private Properties properties;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = ();
        RowBounds rowBounds = (RowBounds)args[2];
        ("Before execution, rowBounds = [{}]", (rowBounds));
        if(rowBounds != null){
            if(() > 1000){
                Field field = ().getDeclaredField("limit");
                (true);
                (rowBounds, 1000);
            }
        }else{
            rowBounds = new RowBounds(0 ,100);
            args[2] = rowBounds;
        }
        ("After execution, rowBounds = [{}]", (rowBounds));
        return ();
    }

    @Override
    public Object plugin(Object target) {
        return (target, this);
    }

    @Override
    public void setProperties(Properties properties) {
         = properties;
    }
}

Loading process

As above, we have implemented a simple plug-in to intercept the query method when executing the query and modify the pagination parameters. However, we have not yet configured plug-in. Only when the plug-in is configured can MyBatis load the plug-in during startup.

xml configuration plugin

existAdd the plugins tag to it and configure the plugins we implemented above

<plugins>
	<plugin interceptor="">
	</plugin>
</plugins>

XMLConfigBuilder loading plugin

In the startup process, use the build method of SqlSessionFactoryBuilder in the plugin loading process. The parse() method in the XMLConfigBuilder parser will read the plugin under the plugins tag and load the InterceptorChain in Configuration.

// SqlSessionFactoryBuilder
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
	SqlSessionFactory var5;
	try {
		XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
		var5 = (());
	} catch (Exception var14) {
		throw ("Error building SqlSession.", var14);
	} finally {
		().reset();

		try {
			();
		} catch (IOException var13) {
		}

	}

	return var5;
}

It can be seen that the parse() method of XMLConfigBuilder is to parse each tag configured in XML.

// XMLConfigBuilder
public Configuration parse() {
	if (parsed) {
	  throw new BuilderException("Each XMLConfigBuilder can only be used once.");
	}
	parsed = true;
	parseConfiguration(("/configuration"));
	return configuration;
}

private void parseConfiguration(XNode root) {
	try {
	  // issue #117 read properties first
	  // parse properties node	  propertiesElement(("properties"));
	  Properties settings = settingsAsProperties(("settings"));
	  loadCustomVfs(settings);
	  loadCustomLogImpl(settings);
	  typeAliasesElement(("typeAliases"));
	  // Record plug-in	  pluginElement(("plugins"));
	  objectFactoryElement(("objectFactory"));
	  objectWrapperFactoryElement(("objectWrapperFactory"));
	  reflectorFactoryElement(("reflectorFactory"));
	  settingsElement(settings);
	  // read it after objectFactory and objectWrapperFactory issue #631
	  environmentsElement(("environments"));
	  databaseIdProviderElement(("databaseIdProvider"));
	  typeHandlerElement(("typeHandlers"));
	  mapperElement(("mappers"));
	} catch (Exception e) {
	  throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
	}
}
XMLConfigBuilder ofpluginElementIt's traversalplugins下ofpluginLoad tointerceptorChainmiddle。

// XMLConfigBuilder
private void pluginElement(XNode parent) throws Exception {
	if (parent != null) {
	  // Iterate through each plugin plugin	  for (XNode child : ()) {
		// Read the plug-in implementation class		String interceptor = ("interceptor");
		// Read plug-in configuration information		Properties properties = ();
		// Create an interceptor object		Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
		(properties);
		// Load into interceptorChain chain		(interceptorInstance);
	  }
	}
}

InterceptorChainIt's oneInterceptor collection, which is equivalent to wrapping layer by layer. The latter plugin is the packaging of the previous plugin and returns a proxy object.

public class InterceptorChain {

  private final List&lt;Interceptor&gt; interceptors = new ArrayList&lt;&gt;();

  // Generate proxy object  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      target = (target);
    }
    return target;
  }

  // Add plugins to the collection  public void addInterceptor(Interceptor interceptor) {
    (interceptor);
  }

  public List&lt;Interceptor&gt; getInterceptors() {
    return (interceptors);
  }

}

Create a plug-in object

Because we need to intercept the intercepted object and wrap it in a layer of package to return a proxy class, when is it processed? Taking Executor as an example, when creating an Executor object, there will be the following code.

// Configuration
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ?  : executorType;
Executor executor;
if ( == executorType) {
  executor = new BatchExecutor(this, transaction);
} else if ( == executorType) {
  executor = new ReuseExecutor(this, transaction);
} else {
  executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
  executor = new CachingExecutor(executor);
}
// Create plugin objectexecutor = (Executor) (executor);
return executor;
}

After creating the Executor object, it will be called()The method actually calls eachInterceptor's plugin() method. plugin() is a proxy to the target object and generates a proxy object to return. and()It is to carry out packaging operations.

// Interceptor
/**
 * Return the proxy object
 * @param target
 * @return
 */
default Object plugin(Object target) {
	return (target, this);
}

Plugin's wrap() mainly performs the following steps:

  • Get the method of interceptor intercepting, using the intercept object as key, and the intercept method collection is value
  • Get the class pair of the target object, such as the Executor object
  • If the intercepted object in the interceptor contains the interface implemented by the target object, the intercepted interface is returned
  • Create a proxy class Plugin object, Plugin implements itInvocationHandlerThe interface will eventually call the target object.Plugin's invocatemethod.
// Plugin
public static Object wrap(Object target, Interceptor interceptor) {
	// Get the method of interceptor intercepting, with the intercept object as key and the intercepting method as value	Map&lt;Class&lt;?&gt;, Set&lt;Method&gt;&gt; signatureMap = getSignatureMap(interceptor);
	// Get the target object's class object	Class&lt;?&gt; type = ();
	// If the interceptor interceptor contains the interface implemented by the target object, the intercepted interface will be returned	Class&lt;?&gt;[] interfaces = getAllInterfaces(type, signatureMap);
	// If the target object is intercepted	if ( &gt; 0) {
	  // Create a proxy class Plugin object	  return (
		  (),
		  interfaces,
		  new Plugin(target, interceptor, signatureMap));
	}
	return target;
}

example

We have already understood the configuration, creation, and implementation process of the MyBatis plug-in. Next, we will use the examples we proposed at the beginning to introduce what should be done to implement a plug-in.

Confirm the intercept object

Because we want to override the query SQL pagination parameters, we can intercept the Executor query method and override the pagination parameters

@Intercepts({@Signature(type = ,
        method = "query",
        args = {, ,  , })})

Implement the intercept interface

accomplishInterceptorInterface and implementinterceptImplement our interception logic

@Slf4j
@Intercepts({@Signature(type = ,
        method = "query",
        args = {, ,  , })})
public class PagePlugin implements Interceptor {

    private Properties properties;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = ();
        RowBounds rowBounds = (RowBounds)args[2];
        ("Before execution, rowBounds = [{}]", (rowBounds));
        if(rowBounds != null){
            if(() &gt; 1000){
                Field field = ().getDeclaredField("limit");
                (true);
                (rowBounds, 1000);
            }
        }else{
            rowBounds = new RowBounds(0 ,100);
            args[2] = rowBounds;
        }
        ("After execution, rowBounds = [{}]", (rowBounds));
        return ();
    }

    @Override
    public Object plugin(Object target) {
        return (target, this);
    }

    @Override
    public void setProperties(Properties properties) {
         = properties;
    }
}

Configuration plug-in

existThe following plug-ins are configured

<plugins>
	<plugin interceptor="">
	</plugin>
</plugins>

test

Added the selectByPage method

List<TTestUser> selectByPage(@Param("offset") Integer offset, @Param("pageSize") Integer pageSize);

mapper/Added the corresponding sql

<select  resultMap="BaseResultMap">
select
<include ref />
from t_test_user
<if test="offset != null">
  limit #{offset}, #{pageSize}
</if>
</select>

In the final test code, we did not specify the paging parameters when querying.

public static void main(String[] args) {
	try {
		// 1. Read the configuration		InputStream inputStream = ("");
		// 2. Create the SqlSessionFactory factory		SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
		// 3. Get sqlSession		SqlSession sqlSession = ();
		// 4. Get the Mapper		TTestUserMapper userMapper = ();
		// 5. Execute interface method		List&lt;TTestUser&gt; list2 = (null, null);
		("list2="+());
		// 6. Submit transaction		();
		// 7. Close the resource		();
		();
	} catch (Exception e){
		((), e);
	}
}

The final printed log is as follows, we can seerowBoundsWe have forced the modification and can only investigate and deal with 1,000 pieces of data.

10:11:49.313 [main] INFO  - Before execution, rowBounds = [{"offset":0,"limit":2147483647}]
10:11:58.015 [main] INFO  - After execution, rowBounds = [{"offset":0,"limit":1000}]
10:12:03.211 [main] DEBUG  - Opening JDBC Connection
10:12:04.269 [main] DEBUG  - Created connection 749981943.
10:12:04.270 [main] DEBUG  - Setting autocommit to false on JDBC Connection [@2cb3d0f7]
10:12:04.283 [main] DEBUG  - ==&gt;  Preparing: select id, member_id, real_name, nickname, date_create, date_update, deleted from t_test_user 
10:12:04.335 [main] DEBUG  - ==&gt; Parameters: 
list2=1000

The above is the detailed explanation of the process of implementing the custom MyBatis MyBatis plug-in. For more information about MyBatis custom MyBatis plug-in, please follow my other related articles!