Implementation of Mybatis Interceptor:
Examples of official documents: https://mybatis.org/mybatis-3/zh/configuration.html#plugins
The interceptor uses the responsibility chain mode + dynamic agent + reflection mechanism. The interceptor can monitor some data permissions, SQL execution time and performance without invading business code.
- Custom Interceptor class: the custom Interceptor needs to implement the Interceptor interface, and add @ Intercepts annotation on the custom Interceptor class;
- Register interceptor class: mybatis interceptor configuration information, and register the plug-in in the global configuration file.
Use example:
// ExamplePlugin.java // @Intercepts identity requires interceptor class // @Signature specifies the method signature to be intercepted @Intercepts( {@Signature( // Target object to intercept type= Executor.class, // Specifies the method to intercept the target object method = "update", // Method parameters args = {MappedStatement.class,Object.class}) } ) public class ExamplePlugin implements Interceptor { // The properties of the interceptor are set through setProperties during plug-in registration private Properties properties = new Properties(); // Intercept the target object executor Execute the intercept method when the update method is called public Object intercept(Invocation invocation) throws Throwable { // implement pre processing if need // Execution target method Object returnObject = invocation.proceed(); // implement post processing if need return returnObject; } // When the plug-in is registered, the property property property is set public void setProperties(Properties properties) { // Store the property configuration in properties this.properties = properties; } } <!-- mybatis-config.xml --> // mybatis interceptor configuration information, and register the plug-in in the global configuration file <plugins> // Configure the registration plug-in through the classpath <plugin interceptor="org.mybatis.example.ExamplePlugin"> // Property configuration <property name="someProperty" value="100"/> </plugin> </plugins>
Four objects that Mybatis can intercept:
- Executor: Mybatis executor, mainly responsible for generating and executing SQL statements;
- ParameterHandler: convert the parameters passed by the user into the parameters required by JDBC Statement;
- ResultHandler: converts the ResultSet result set object returned by JDBC into a List type set;
- StatementHandler: encapsulates the JDBC statement operation and is responsible for the operation of JDBC statement, such as setting parameters and converting the Statement result set into a List set.
Interceptable class | Interceptable method |
Executor | update, query, flushStatements, commit, rollback,getTransaction, close, isClosed |
ParameterHandler | getParameterObject, setParameters |
ResultSetHandler | handleResultSets, handleOutputParameters |
StatementHandler | prepare, parameterize, batch, update, query |
1. After the four objects are created, they do not return directly. The responsibility chain mode is used. They all pass through the interceptorchain of the interceptor chain Call the pluginall () method to determine whether it is necessary to intercept to return the wrapped target object (new dynamic proxy object);
2. Using interceptors will create a proxy object for the target object. If there are N interceptors, N proxy objects will be generated. Generating dynamic agents layer by layer is more performance consuming; Although there are specific methods to execute interception, reflection dynamic judgment is used when executing methods. So don't write unnecessary interceptors.
3. AOP aspect oriented method can create proxy objects for four objects and intercept each execution of each object.
Configuration source code four object generation and call interceptor chain source code:
// Configuration source code four object generation and call interceptor chain source code package org.apache.ibatis.session; 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); } executor = (Executor) interceptorChain.pluginAll(executor); return executor; } 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; }
XMLConfigBuilder resolves the plugins tag of MyBatis global configuration file:
// XMLConfigBuilder. pluginElement(root.evalNode("plugins")); // XMLConfigBuilder.pluginElement method private void pluginElement(XNode parent) throws Exception { // Parsing plugins Tags if (parent != null) { // Traverse the lower sub label for (XNode child : parent.getChildren()) { // Get the interceptor attribute on the sub tag, that is, the class path of the interceptor implementation class String interceptor = child.getStringAttribute("interceptor"); // Get the following property property configuration Properties properties = child.getChildrenAsProperties(); // Generate interceptor class through reflection Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance(); // Set properties for the interceptor's class interceptorInstance.setProperties(properties); // Add interceptor to interceptor chain configuration.addInterceptor(interceptorInstance); } } } // Configuration.addInterceptor public void addInterceptor(Interceptor interceptor) { interceptorChain.addInterceptor(interceptor); }
Source code of Mybatis Interceptor:
The @ Intercepts and @ Signature annotations on the Interceptor:
- The interceptor annotation is used to identify the class of an interceptor and configure the method signature to be intercepted by the interceptor;
- @Intercepts: identifies a class as an interceptor, and the content can contain annotations for multiple @ signatures
- @Signature: declare the class (type), method and method parameter (args) to be intercepted;
- Configuration example:
@Intercepts( { @Signature(type=Executor.class, method="query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}), @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}), } )
// Intercepts source code package org.apache.ibatis.plugin; // The annotation of interceptor is used to identify the class of an interceptor. The content can contain multiple @ Signature annotations // @The Signature annotation specifies the class, method and input parameter of the target object to be intercepted @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface Intercepts { /** * Returns method signatures to intercept. * @return method signatures */ Signature[] value(); }
// Signature source code package org.apache.ibatis.plugin; @Documented @Retention(RetentionPolicy.RUNTIME) @Target({}) // Intercept method Signature annotation, @ Signature annotation specifies the class, method and input parameter of the target object to be intercepted public @interface Signature { /** * Returns the java type. * @return the java type */ // The class of the target object that needs to be intercepted Class<?> type(); /** * Returns the method name. * @return the method name */ // Method of target object to be intercepted String method(); /** * Returns java types for method argument. * @return java types for method argument */ // The input parameter of the target object method to be intercepted Class<?>[] args(); }
Interceptor interceptor interface:
- Provide the implementation interface class of the development interceptor and implement the interceptor object;
- Plugin method: the interface has been implemented by default. It is implemented through the dynamic proxy method of JDK, which calls plugin Wrap (target, this). If there is a specified interception class and method, wrap the target object target and return a new dynamic proxy object class. When calling the method of the new dynamic proxy object class target, it will persistently implement the invoke method in the plugin class of InvocationHandler, and the intercept method interceptor of the interceptor will be called in the invoke method Intercept() implements interception; If the method is not specified, it will directly return to the target object and call the original method without interception;
- Intercept: the implementation of the interception method, which is called when it is necessary to intercept the method of the hair class.
- setProperties: set some configurable properties of the interceptor.
- The current interceptor proxy object is multi-layer proxy. The method to obtain the real interceptor object is as follows:
@SuppressWarnings("unchecked") public static <T> T realTarget(Object target) { if (Proxy.isProxyClass(target.getClass())) { MetaObject metaObject = SystemMetaObject.forObject(target); return realTarget(metaObject.getValue("h.target")); } return (T) target; }
// Interceptor source code package org.apache.ibatis.plugin; // Interceptor interface public interface Interceptor { // Intercept: intercepts the execution of the target method of the target object Object intercept(Invocation invocation) throws Throwable; // Wrap the target object and create a proxy object for the target object default Object plugin(Object target) { return Plugin.wrap(target, this); } // When the plug-in is registered, set the properties property of the plug-in default void setProperties(Properties properties) { // NOP } }
// InterceptorChain source code package org.apache.ibatis.plugin; // Interceptor chain public class InterceptorChain { // Interceptor set private final List<Interceptor> interceptors = new ArrayList<>(); // Wrap the target object with the interceptors in all interceptor sets and return the wrapped target object public Object pluginAll(Object target) { for (Interceptor interceptor : interceptors) { // Wrap the target object target = interceptor.plugin(target); } return target; } // Add interceptors to interceptor collection public void addInterceptor(Interceptor interceptor) { interceptors.add(interceptor); } // Gets the interceptor collection and returns interceptors that cannot be modified public List<Interceptor> getInterceptors() { return Collections.unmodifiableList(interceptors); } }
JDK dynamic proxy, Java lang.reflect. Proxy:
- Using the java reflection class Proxy+InvocationHandler callback interface implementation, create a new class (also known as "dynamic proxy class") and its instance (object) to implement some interfaces. The proxy is interfaces, not classes or abstract classes. In fact, it can only proxy the methods defined in the interface implemented by this class;
- Implementation method: proxy Newproxyinstance (classloader, loader, class [] interfaces, invocationhandler h) returns the proxy object of an object.
- ClassLoader loader: class loader. You can use this type of loader to load the generated proxy class into the JVM, i.e. Java virtual machine, when the program is running, so as to meet the needs of runtime;
- Class[] interfaces: the interfaces to be implemented by the dynamic proxy class,
- InvocationHandler h: call / trigger the processor to call the invoke callback method that implements the InvocationHandler class. When the dynamic proxy method is executed, it will call the invoke method in h to execute.
// Plugin source code package org.apache.ibatis.plugin; // The Plugin class implements the invoke method of the InvocationHandler class // A new dynamically proxied target object is returned through wrap wrapping // When calling the method of the target object target, the invoke method in Plugin will be called first public class Plugin implements InvocationHandler { // Target object to intercept private final Object target; // Interceptor private final Interceptor interceptor; // A HashMap that stores the mapping of the interception target object class and 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 the target object and return a new proxy object class public static Object wrap(Object target, Interceptor interceptor) { // Get the HashMap of the method set of the intercepting target object class configured by the interceptor @ Intercepts annotation Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); // Gets the class of the intercepted target object Class<?> type = target.getClass(); // Get the target object class to be intercepted and all interface classes contained Class<?>[] interfaces = getAllInterfaces(type, signatureMap); if (interfaces.length > 0) { // Using the dynamic proxy of JDK, the interfaces interface class is represented through the class loader of type // Returns the new proxy object class target // When calling the method of the new proxy object target, it will insist on implementing the invoke method in the Plugin class of InvocationHandler return Proxy.newProxyInstance( type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)); } // If there is no interface requiring proxy, the original object will be returned directly return target; } @Override // Proxy method // Proxy has been the target object of dynamic proxy // Method the method of the target object // args parameter object array public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { // Gets the method set of the method declaration class Set<Method> methods = signatureMap.get(method.getDeclaringClass()); if (methods != null && methods.contains(method)) { // If the method is included in the method set to be intercepted, execute the interceptor's interception method intercept return interceptor.intercept(new Invocation(target, method, args)); } // If the method is not in the method set to be intercepted, the target method is executed directly return method.invoke(target, args); } catch (Exception e) { throw ExceptionUtil.unwrapThrowable(e); } } // Get the configuration information corresponding to the @ Intercepts annotation // Returns the HashMap of the method set that needs to intercept the target object class - > intercept the target object class private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) { // Gets the @ Intercepts annotation on the Interceptor object of the Interceptor Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class); // issue #251 // If the @ Intercepts annotation is missing on the interceptor, an error is thrown if (interceptsAnnotation == null) { throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName()); } // Get the array @ Signature annotation in the @ Intercepts annotation Signature[] sigs = interceptsAnnotation.value(); // The returned result stores the class of the intercepted target object and the Map of the method set of the target object to be intercepted Map<Class<?>, Set<Method>> signatureMap = new HashMap<>(); // Traverse @ Signature annotation array for (Signature sig : sigs) { // Get the method set of the interception target object class corresponding to the Map. If there is no corresponding target object class, create an empty HashSet method set Set<Method> methods = MapUtil.computeIfAbsent(signatureMap, sig.type(), k -> new HashSet<>()); try { // Obtain the corresponding Method object according to the annotation configured Method and input parameters Method method = sig.type().getMethod(sig.method(), sig.args()); // Add method to intercepted method set methods.add(method); } catch (NoSuchMethodException e) { throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e); } } return signatureMap; } // Get the target object class to be intercepted and all interface classes contained private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) { Set<Class<?>> interfaces = new HashSet<>(); while (type != null) { // Gets all the implementation interfaces of the type class object for (Class<?> c : type.getInterfaces()) { // If the Map that stores the class of the intercepted target object and the method set of the target object to be intercepted contains this interface class if (signatureMap.containsKey(c)) { // Add to interface collection interfaces.add(c); } } // Get the parent class of type, and end while until the parent class is null type = type.getSuperclass(); } // Converts an interface set to an array and returns return interfaces.toArray(new Class<?>[0]); } }