Introduction to MyBatis Interceptor
MyBatis provides a plugin function. Although it is called a plugin, it is actually an interceptor function. So what does the interceptor intercepts MyBatis?
Let's go to the official website to take a look:
MyBatis allows you to intercept calls at a certain point during the execution of a mapped statement. By default, method calls MyBatis allows plugins to intercept include:
We have seen some methods that can intercept the Executor interface, such as update, query, commit, rollback and other methods, as well as some methods of other interfaces
wait.
The overall summary is:
Use of interceptors
Interceptor introduction and configuration
First, let’s look at the interface definition of the MyBatis interceptor:
public interface Interceptor { Object intercept(Invocation invocation) throws Throwable; Object plugin(Object target); void setProperties(Properties properties);}It's relatively simple, there are only 3 methods. MyBatis does not have an interceptor interface implementation class by default, and developers can implement interceptors that meet their needs.
Here is an example of an interceptor from the MyBatis official website:
@Intercepts({@Signature( type= Executor.class, method = "update", args = {MappedStatement.class,Object.class})})public class ExamplePlugin implements Interceptor { public Object intercept(Invocation invocation) throws Throwable { return invocation.proceed(); } public Object plugin(Object target) { return Plugin.wrap(target, this); } public void setProperties(Properties properties) { }}Global xml configuration:
<plugins> <plugin interceptor="org.format.mybatis.cache.interceptor.ExamplePlugin"></plugin></plugins>
This interceptor intercepts the update method of the Executor interface (in fact, it is the addition, deletion, and modification operations of SqlSession). All update methods that execute executor will be intercepted by the interceptor.
Source code analysis
Let’s analyze the source code behind this code.
First, start the analysis from the source->config file:
XMLConfigBuilder parses pluginElement private method of MyBatis global configuration file:
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(); interceptorInstance.setProperties(properties); configuration.addInterceptor(interceptorInstance); } }}The specific parsing code is actually relatively simple, so I won't post it. It mainly instantiates the class represented by the interceptor attribute in the plugin node by reflection. Then call the addInterceptor method of the global configuration class Configuration.
public void addInterceptor(Interceptor interceptor) { interceptorChain.addInterceptor(interceptor); }This interceptorChain is an internal property of Configuration, and its type is InterceptorChain, which is an interceptor chain. Let's take a look at its definition:
public class InterceptorChain { private final List<Interceptor> interceptors = new ArrayList<Interceptor>(); 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); }}Now that we understand the parsing of the interceptor configuration and the ownership of the interceptor, we now look back at why the interceptor intercepts these methods (partial methods of Executor, ParameterHandler, ResultSetHandler):
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql); parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler); return parameterHandler;}public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) { ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds); resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler); return resultSetHandler;}public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql); statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler); return statementHandler;}public Executor newExecutor(Transaction transaction, ExecutorType executorType, boolean autoCommit) { 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, autoCommit); } executor = (Executor) interceptorChain.pluginAll(executor); return executor;}The above 4 methods are all Configuration methods. These methods will be executed in an operation of MyBatis (add, delete, modify, and query). The order of execution is Executor, ParameterHandler, ResultSetHandler, StatementHandler (where ParameterHandler and ResultSetHandler are created when creating StatementHandler [3 available implementation classes CallableStatementHandler, PreparedStatementHandler, SimpleStatementHandler], and the constructor calls the constructor [the constructors of these three implementation classes actually call the constructor of the parent class BaseStatementHandler]).
After these 4 methods instantiate the corresponding object, they will call the pluginAll method of interceptorChain. As mentioned earlier, the pluginAll of InterceptorChain has been introduced, which is to traverse all interceptors and then call the plugin method of each interceptor. Note: The return value of the plugin method of the interceptor will be assigned directly to the original object.
Since StatementHandler can be intercepted, this interface mainly deals with the construction of SQL syntax. Therefore, for example, the function of paging can be implemented with an interceptor. You only need to process the SQL in the StatementHandler interface implementation class in the plugin method of the interceptor, and you can use reflection to implement it.
MyBatis also provides annotations for @Intercepts and @Signature about interceptors. The example of the official website is to use these two annotations, including the use of the Plugin class:
@Overridepublic Object plugin(Object target) { return Plugin.wrap(target, this);}Let’s analyze the source codes of these 3 "new combinations". First, let’s look at the wrap method of the Plugin class:
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, interface, signatureMap)); } return target;}The Plugin class implements the InvocationHandler interface. Obviously, we see that a dynamic proxy class provided by the JDK itself is returned here. Let's dissect other methods called by this method:
getSignatureMap method:
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) { Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class); if (interceptsAnnotation == null) { // issue #251 throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName()); } Signature[] sigs = interceptsAnnotation.value(); Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>(); for (Signature sig : sigs) { Set<Method> methods = signatureMap.get(sig.type()); if (methods == null) { methods = new HashSet<Method>(); signatureMap.put(sig.type(), methods); } try { 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;}The getSignatureMap method explanation: First, you will get the @Interceptors annotation of the interceptor class, then get the @Signature annotation collection of attributes of this annotation, then traverse this collection, take out the type attribute (Class type) of the @Signature annotation, and then get the Method with the method attribute and args attribute based on this type. Since the @Signature attribute annotated by @Interceptors is a property, it will eventually return a map with type as key and value as Set<Method>.
@Intercepts({@Signature( type= Executor.class, method = "update", args = {MappedStatement.class,Object.class})})For example, the @Interceptors annotation will return a key as Executor and value as a collection (there is only one element in this collection, that is, the Method instance, this Method instance is the update method of the Executor interface, and this method has parameters of type MappedStatement and Object). This method instance is obtained based on the method and args attributes of @Signature. If the args parameter does not correspond to the method method of type type, an exception will be thrown.
getAllInterfaces method:
private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) { Set<Class<?>> interfaces = new HashSet<Class<?>>(); while (type != null) { for (Class<?> c : type.getInterfaces()) { if (signatureMap.containsKey(c)) { interfaces.add(c); } } type = type.getSuperclass(); } return interfaces.toArray(new Class<?>[interfaces.size()]);}The getAllInterfaces method explanation: According to the target instance target (this target is the class that the MyBatis interceptor can intercept as mentioned earlier, Executor, ParameterHandler, ResultSetHandler, StatementHandler) and its parent classes, return the interface array containing the target implementation in the signatureMap.
Therefore, the function of the Plugin class is to obtain the attribute @Signature array of the annotated attributes based on the @Interceptors annotation, and then use reflection to find the corresponding method according to the type, method, and args attributes of each @Signature annotated. Finally, based on the interface implemented by the called target object, decide whether to return a proxy object to replace the original target object.
For example, in the MyBatis official website, when Configuration calls the newExecutor method, the update (MappedStatement ms, Object parameter) method of the Executor interface is intercepted by the interceptor. So the end is returned with a proxy class Plugin, not an Executor. When calling the method in this way, if it is a proxy class, it will be executed:
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)) { return interceptor.intercept(new Invocation(target, method, args)); } return method.invoke(target, args); } catch (Exception e) { throw ExceptionUtil.unwrapThrowable(e); }}That's right, if the corresponding method is found and is proxyed, the interceptor method of the Interceptor interface will be executed.
This Invocation class is as follows:
public class Invocation { private Object target; private Method method; private Object[] args; public Invocation(Object target, Method method, Object[] args) { this.target = target; this.method = method; this.args = args; } public Object getTarget() { return target; } public Method getMethod() { return method; } public Object[] getArgs() { return args; } public Object proceed() throws InvocationTargetException, IllegalAccessException { return method.invoke(target, args); }}Its process method is to call the original method (no proxy).
Summarize
Among the 3 methods provided by the MyBatis interceptor interface, the plugin method is used in the construction process of certain processors (Handlers). The interceptor method is used to handle the execution of the proxy class. The setProperties method is used to set the interceptor properties.
In fact, the methods provided by MyBatis official website to use @Interceptors and @Signature annotations and Plugin classes to handle interceptors are not necessarily used directly in this way. We can also abandon these three classes and directly perform corresponding operations based on the type of the target instance within the plugin method.
Overall, the MyBatis interceptor is still very simple. The interceptor itself does not require too many knowledge points, but learning the interceptor requires familiarity with each interface in MyBatis, because the interceptor involves the knowledge points of each interface.
Summarize
The above is an exploration of the principles of MyBatis interceptor introduced by the editor. I hope it will be helpful to everyone. If you have any questions, please leave me a message and the editor will reply to everyone in time. Thank you very much for your support to Wulin.com website!