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.@Intercepts
The annotation specifies the method signature that needs to be intercepted, and the content isSignature
array of type, andSignature
It 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.Interceptor
interface.Interceptor
There 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); } } }
InterceptorChain
It'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<Interceptor> interceptors = new ArrayList<>(); // 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<Interceptor> 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 it
InvocationHandler
The interface will eventually call the target object.Plugin's invocate
method.
// 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<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); // Get the target object's class object Class<?> type = (); // If the interceptor interceptor contains the interface implemented by the target object, the intercepted interface will be returned Class<?>[] interfaces = getAllInterfaces(type, signatureMap); // If the target object is intercepted if ( > 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
accomplishInterceptor
Interface and implementintercept
Implement 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(() > 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<TTestUser> list2 = (null, null); ("list2="+()); // 6. Submit transaction (); // 7. Close the resource (); (); } catch (Exception e){ ((), e); } }
The final printed log is as follows, we can seerowBounds
We 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 - ==> Preparing: select id, member_id, real_name, nickname, date_create, date_update, deleted from t_test_user 10:12:04.335 [main] DEBUG - ==> 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!