This series of documents is summarized in the process of learning the source code of Mybatis. It may not be friendly to readers. Please combine my source code comments( Mybatis source code analysis GitHub address,Mybatis spring source code analysis GitHub address,Spring boot starter source code analysis GitHub address )Read
MyBatis version: 3.5.2
Mybatis spring version: 2.0.3
Mybatis spring boot starter version: 2.1.4
For other documents in this series, please see: "Jingjin MyBatis source code analysis - article guide"
Plug in mechanism
Open source frameworks generally provide plug-ins or other forms of extension points for developers to expand by themselves to increase the flexibility of the framework
Of course, MyBatis also provides a plug-in mechanism. Based on it, developers can extend and enhance the functions of MyBatis, such as paging, SQL analysis and monitoring. This paper will describe the principle of MyBatis plug-in mechanism and how to implement a custom plug-in
When writing a plug-in, we need to make the plug-in class implement org apache. ibatis. plugin. The interceptor interface also needs to annotate the interception point of the plug-in, that is, the methods that the plug-in needs to enhance. MyBatis only provides methods defined in the following classes that can be enhanced:
-
Executor: executor
-
ParameterHandler: parameter handler
-
ResultSetHandler: result set handler
-
StatementHandler: Statement handler
Embedded plug-in logic
stay SQL execution process of MyBatis In a series of documents, it is mentioned that when creating Executor, ParameterHandler, ResultSetHandler and StatementHandler objects, the pluginAll method of InterceptorChain will be called to traverse all plug-ins, and the plugin method of Interceptor plug-in will be called to implant corresponding plug-in logic. Therefore, in MyBatis, only the methods in the above four objects can be enhanced
The code is as follows:
// Configuration.java public Executor newExecutor(Transaction transaction, ExecutorType executorType) { // <1> Get actuator type executorType = executorType == null ? defaultExecutorType : executorType; executorType = executorType == null ? ExecutorType.SIMPLE : executorType; // <2> Create the Executor object corresponding to the implementation 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); } // <3> If caching is enabled, a cachengexecution object is created and wrapped if (cacheEnabled) { executor = new CachingExecutor(executor); } // <4> Application plug-in executor = (Executor) interceptorChain.pluginAll(executor); return executor; } public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { // Create ParameterHandler object ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql); // Application plug-in parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler); return parameterHandler; } public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) { // Create DefaultResultSetHandler object ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds); // Application plug-in resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler); return resultSetHandler; } public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql); // Apply all plug-ins in Configuration global Configuration to StatementHandler statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler); return statementHandler; }
Paging plug-in example
Let's take a look at a simple plug-in example. The code is as follows:
@Intercepts({ @Signature( type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class} ) }) public class ExamplePlugin implements Interceptor { // Query method of Executor: // public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) @Override public Object intercept(Invocation invocation) throws Throwable { Object[] args = invocation.getArgs(); RowBounds rowBounds = (RowBounds) args[2]; if (rowBounds == RowBounds.DEFAULT) { // No paging required return invocation.proceed(); } /* * Set the RowBounds input parameter of the query method to an empty object * That is, close the paging implemented inside MyBatis (logical paging, paging after getting the query results, rather than physical paging) */ args[2] = RowBounds.DEFAULT; MappedStatement mappedStatement = (MappedStatement) args[0]; BoundSql boundSql = mappedStatement.getBoundSql(args[1]); // Get SQL statement and splice limit statement String sql = boundSql.getSql(); String limit = String.format("LIMIT %d,%d", rowBounds.getOffset(), rowBounds.getLimit()); sql = sql + " " + limit; // Create a StaticSqlSource object SqlSource sqlSource = new StaticSqlSource(mappedStatement.getConfiguration(), sql, boundSql.getParameterMappings()); // Get and set the sqlSource field of MappedStatement through reflection Field field = MappedStatement.class.getDeclaredField("sqlSource"); field.setAccessible(true); field.set(mappedStatement, sqlSource); // Execute intercepted method return invocation.proceed(); } @Override public Object plugin(Object target) { // default impl return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { // default nop } }
In the paging plug-in above, @ Intercepts and @ Signature annotations specify that the enhanced method is Executor Query (mappedstatement MS, object parameter, rowboundaries, rowboundaries, resulthandler, resulthandler), that is, the method used by the Executor to perform database query operations
In the implemented intercept method, the paging information is obtained through the rowboundaries parameter, and the corresponding SQL is generated (spliced limit), and the SQL is used as a parameter to re create a StaticSqlSource object. Finally, the sqlSource field in the MappedStatement object is replaced by reflection, so as to realize a simple paging plug-in
The above is just a simple example, which should be used with caution in actual scenarios
Interceptor
org.apache.ibatis.plugin.Interceptor: interceptor interface. The code is as follows:
public interface Interceptor { /** * interceptor method * * @param invocation Call information * @return Call result * @throws Throwable In case of abnormality */ Object intercept(Invocation invocation) throws Throwable; /** * Application plug-ins. If the application is successful, a proxy object for the target object will be created * * @param target Target object * @return The result object of the application can be a proxy object, a target object, or any object. Specifically, look at the code implementation */ default Object plugin(Object target) { return Plugin.wrap(target, this); } /** * Set interceptor properties * * @param properties attribute */ default void setProperties(Properties properties) { // NOP } }
- Intercept method: intercept method, plug-in enhanced logic
- Plugin method: apply the plug-in and implant the corresponding plug-in logic into the target object. If the application is successful, a proxy object (JDK dynamic proxy) will be returned. Otherwise, the original object will be returned and the wrap method of plugin will be called by default
- setProperties method: set interceptor properties
Invocation
org.apache.ibatis.plugin.Invocation: intercepted object information. The code is as follows:
public class Invocation { /** * Target object */ private final Object target; /** * method */ private final Method method; /** * parameter */ private final Object[] args; public Invocation(Object target, Method method, Object[] args) { this.target = target; this.method = method; this.args = args; } // Omit getter setter method }
Plugin
org.apache.ibatis.plugin.Plugin: implements the InvocationHandler interface, which is used to intercept the intercepted object. On the one hand, it provides the method of creating dynamic proxy object, on the other hand, it implements the interception processing of the specified method of the specified class. It is the core class of MyBatis plug-in mechanism
Construction method
public class Plugin implements InvocationHandler { /** * Target object */ private final Object target; /** * Interceptor */ private final Interceptor interceptor; /** * Intercepted method mapping * * KEY: class * VALUE: Method set */ private final 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; } }
wrap method
Wrap (object, target, interceptor) method to create the proxy object of the target class. The method is as follows:
public static Object wrap(Object target, Interceptor interceptor) { // <1> Get the method collection of the class to be intercepted in the interceptor Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); // <2> Gets the Class object of the target object Class<?> type = target.getClass(); // <3> Obtain all Class objects (parent Class or interface) of the target object that need to be intercepted Class<?>[] interfaces = getAllInterfaces(type, signatureMap); // <4> If there is something that needs to be intercepted, create a dynamic proxy object (JDK dynamic proxy) for the target object, and the proxy class is Plugin object if (interfaces.length > 0) { // Because Plugin implements the InvocationHandler interface, it can be used as the calling processor of JDK dynamic agent return Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)); } // <5> If not, the original target object is returned return target; }
- Call the getSignatureMap method to obtain the method collection of the classes to be intercepted in the interceptor, including the enhanced methods specified through the @ Intercepts and @ Signature annotations
- Get the Class object (parent Class or interface) of the target object
- Get all Class objects of the target object that need to be intercepted
- If it needs to be intercepted, create a dynamic proxy object (JDK dynamic proxy) for the target object, the proxy class is Plugin object, and return the dynamic proxy object
- Otherwise, the original target object is returned
getSignatureMap method
getSignatureMap(Interceptor interceptor) method to obtain the methods to be enhanced by the plug-in, as follows:
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) { // Get @ Intercepts annotation Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class); // issue #251 if (interceptsAnnotation == null) { throw new PluginException( "No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName()); } // Get @ Signature annotation in @ Intercepts annotation Signature[] sigs = interceptsAnnotation.value(); Map<Class<?>, Set<Method>> signatureMap = new HashMap<>(); for (Signature sig : sigs) { // Create a method array for the class name defined in the @ Signature annotation Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>()); try { // Gets the method object defined in the @ Signature annotation Method method = sig.type().getMethod(sig.method(), sig.args()); methods.add(method); } catch (NoSuchMethodException e) { throw new PluginException( "Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e); } } return signatureMap; }
- Through the @ Intercepts and @ Signature annotations on the plug-in, get the methods that need to be enhanced in all objects that need to be intercepted
getAllInterfaces method
Getallinterfaces (class <? > type, map < class <? >, set < method > > signaturemap) method to judge whether the target object needs to be applied by the plug-in. The method is as follows:
private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) { // Collection of interfaces Set<Class<?>> interfaces = new HashSet<>(); // Circular recursive type class, machine parent class while (type != null) { // Traverse the interface collection. If it is in the signatureMap, it will be added to the interfaces for (Class<?> c : type.getInterfaces()) { if (signatureMap.containsKey(c)) { interfaces.add(c); } } // Get parent class type = type.getSuperclass(); } // Create an array of interfaces return interfaces.toArray(new Class<?>[interfaces.size()]); }
- The input parameter signatureMap is the method returned by the getSignatureMap method, which needs to be enhanced by the plug-in
- Returns the parent class or interface of all target objects existing in the signatureMap collection
invoke method
Invoke (object proxy, method, method, object [] args) method is used to intercept dynamic proxy objects. The methods are as follows:
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { // Get the intercepted method of the class where the target method is located Set<Method> methods = signatureMap.get(method.getDeclaringClass()); if (methods != null && methods.contains(method)) { // If the intercepted method contains the current method // Use the plug-in to intercept this method for processing return interceptor.intercept(new Invocation(target, method, args)); } // If there is no method to be intercepted, the original method is called return method.invoke(target, args); } catch (Exception e) { throw ExceptionUtil.unwrapThrowable(e); } }
- Get the intercepted method of the class where the target method is located
- If the intercepted method contains the current method, encapsulate the current method into an Invocation object, call the intercept method of the Interceptor plug-in, and execute the plug-in logic
- Otherwise, execute the original method
In this way, when you call the corresponding method of the target object, you will enter the intercept method of the plug-in to execute the plug-in logic and extend the functions
InterceptorChain
org.apache.ibatis.plugin.InterceptorChain: interceptor chain, which is used to implant the plug-in logic of all interceptors into the target object in order. The code is as follows:
public class InterceptorChain { private final List<Interceptor> interceptors = new ArrayList<>(); public Object pluginAll(Object target) { // Traversal interceptor collection for (Interceptor interceptor : interceptors) { // Call the plugin method of the interceptor to implant the corresponding plug-in logic target = interceptor.plugin(target); } return target; } public void addInterceptor(Interceptor interceptor) { interceptors.add(interceptor); } public List<Interceptor> getInterceptors() { return Collections.unmodifiableList(interceptors); } }
The Configuration MyBatis plug-ins will be saved in the interceptors collection. You can recall the pluginElement method in the XMLConfigBuilder section of initialization (I) loading mybatis-config.xml. All parsed will be added to the InterceptorChain object of Configuration in turn. The code is as follows:
private void pluginElement(XNode parent) throws Exception { if (parent != null) { // Traverse < plugins / > tags for (XNode child : parent.getChildren()) { String interceptor = child.getStringAttribute("interceptor"); Properties properties = child.getChildrenAsProperties(); // <1> Create an Interceptor object and set properties Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance(); interceptorInstance.setProperties(properties); // <2> Add to configuration configuration.addInterceptor(interceptorInstance); } } }
summary
This paper analyzes the plug-in mechanism in MyBatis. Generally speaking, it is relatively simple. To implement a plug-in, you need to implement the Interceptor interface, and specify the interception point of the plug-in through the @ Intercepts and @ Signature annotations (it supports the enhancement of methods in the four objects of Executor, ParameterHandler, ResultSetHandler and StatementHandler), Perform logical processing in the implemented intercept method
When MyBatis is initialized, the plug-in will be scanned and added to InterceptorChain
Then, during SQL execution, when MyBatis creates the above four objects, it will submit the created objects to InterceptorChain for processing, traverse all plug-ins, create a dynamic proxy object for it through the plugin method of the plug-in and return it. The proxy class is the plugin object
In the invoke method in the Plugin object, the request is handled by the intercept method of the plug-in
Although the plug-in mechanism of MyBatis is relatively simple, it is complex to implement a perfect and efficient plug-in. You can refer to it PageHelper paging plug-in
Here, I believe you have a certain understanding of MyBatis plug-in mechanism. Thank you for reading!!!