Mybatis plugin, also known as interceptor, is also interesting.
It is easy to use, directly take the example in the documentation to briefly explain
A, use,
It’s easy to use
Copy// Use this annotation to indicate that this is an interceptor
@Intercepts(
// Method signature
{@Signature(
// The class of the intercepted method
type= Executor.class,
// The name of the intercepted method
method = "update".// Intercepted method parameter list type
args = {MappedStatement.class ,Object.class})}
)
public class ExamplePlugin implements Interceptor {
// Intercept specific behavior
@Override
public Object intercept(Invocation invocation) throws Throwable {
// implement pre-processing if needed
Object returnObject = invocation.proceed();
// implement post-processing if needed
returnreturnObject; }}Copy the code
And then if it’s XML configuration
Copy<plugins>
<plugin interceptor="org.mybatis.example.ExamplePlugin">
<property name="someProperty" value="100"/>
</plugin>
</plugins>
Copy the code
If SpringBoot is used in conjunction with automatic configuration, use the @Component annotation to manage the above classes in the Spring container, and then register them with the MyBatis InterceptorChain
Mybaits currently supports intercepting classes and methods, including the following
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- StatementHandler (prepare, parameterize, batch, update, query)
- ParameterHandler (getParameterObject, setParameters)
- ResultsetHandle (handleResultSets, handleOutputParameters)
Two, the implementation principle
It is possible to guess the implementation principle of its interceptor from the above use, that is the way of dynamic proxy, but Mybatis here is not very direct to use proxy, around a bend, so it gives people a feeling of special dizzy, also can not say whether there are some problems with this implementation
Let’s start with the Executor
When we get a SqlSession from the SqlSessionFactory, we create a new Executor instance, and the actual creation action is here
org.apache.ibatis.session.Configuration#newExecutorCopypublic Executor newExecutor(Transaction Transaction, ExecutorType ExecutorType) {// Depending on the Executor type, 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 caching is enabled, use caching, where the cache executor is a bit like a static proxyif(cacheEnabled) { executor = new CachingExecutor(executor); } / / the original actuators, packaging, generate a new actuator, the proxy object after the executor = (executor) interceptorChain. PluginAll (executor);return executor;
}
Copy the code
InterceptorChain is a butler, similar to FilterChain, but very different
Copypublic class InterceptorChain {
// List of all interceptors
private final List<Interceptor> interceptors = new ArrayList<>();
// There may be layers of proxy objects, one after another, depending on the number of interceptors and what is being intercepted
// The class of the method
// The number of interceptors configured is not the same as the number of proxy object generation times
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
// Returns the last proxy object created
return target;
}
// Register the interceptor
public void addInterceptor(Interceptor interceptor) { interceptors.add(interceptor); }}Copy the code
Interceptor is an interface that mybatis directly exposes to users and requires user implementation
Copypublic interface Interceptor {
// The Invocation class fills its own logic with the Invocation,
Object intercept(Invocation invocation) throws Throwable;
// Create a proxy object by default
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
// Implement the class to do something
default void setProperties(Properties properties) {
// NOP}}Copy the code
Let’s focus on this one
Copy// This is a Jdk dynamic proxy implementation class called InvocationHandler
public class Plugin implements InvocationHandler {
private final Object target;
private final Interceptor interceptor;
private finalMap<Class<? >, Set<Method>> signatureMap;private Plugin(Object target, Interceptor interceptor, Map
, Set
> signatureMap)
> {
this.target = target;
this.interceptor = interceptor;
this.signatureMap = signatureMap;
}
// Static method used to create proxy objects directly
public static Object wrap(Object target, Interceptor interceptor) {
// Get all methods that the current interceptor needs to interceptMap<Class<? >, Set<Method>> signatureMap = getSignatureMap(interceptor);// Get the Class of the proxied objectClass<? > type = target.getClass();// Find all directly implemented interfaces and ancestor interfaces of the proxied object that can be found in the signature MapClass<? >[] interfaces = getAllInterfaces(type, signatureMap);// Create a proxy object when it is found
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
// Create an InvocationHandler instance
new Plugin(target, interceptor, signatureMap));
}
// Return if not found
// This object is not necessarily an unpropped object, but may be propped
return target;
}
This method is called when any method of this proxy object is called
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// Find the class in which the method is currently being called, and all methods that are being intercepted
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
if(methods ! =null && methods.contains(method)) {
// If the currently called method needs to be intercepted, then we execute our custom intercepting logic
// interceptor is our custom interceptor. In our custom interceptor, we need to get the interceptor
// The original delegate object, the method to be called, and the parameters are nicely encapsulated here
// Concrete implementation, a complete separation is made, the user is not aware of any concrete implementation
// invoke method. Invoke (target,args);
return interceptor.intercept(new Invocation(target, method, args));
}
// If the currently called method is not intercepted, then the original method is called directly
return method.invoke(target, args);
} catch (Exception e) {
throwExceptionUtil.unwrapThrowable(e); }}// Gets the method signature of the intercepted method specified by the interceptor
// Key is the return value type of the intercepted method
private staticMap<Class<? >, Set<Method>> getSignatureMap(Interceptor interceptor) {// Get the @intercepts information
Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
// Issue #251 Avoid situations where there is no specific blocking information
if (interceptsAnnotation == null) {
throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
}
// Get the signature information for the configured intercepted methodSignature[] sigs = interceptsAnnotation.value(); Map<Class<? >, Set<Method>> signatureMap =new HashMap<>();
// Put them all in Map
for (Signature sig : sigs) {
Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>());
try {
// Build the definition into the actual Method object
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;
}
// Get information about all interfaces of the proxied object
private staticClass<? >[] getAllInterfaces(Class<? > type, Map<Class<? >, Set<Method>> signatureMap) { Set<Class<? >> interfaces =new HashSet<>();
while(type ! =null) {
for(Class<? > c : type.getInterfaces()) {if (signatureMap.containsKey(c)) {
// If the type of the proxy interface is the same as that of the return value, the interface is addedinterfaces.add(c); }}// Find the proxied class's parent, and continue to look for interface information
type = type.getSuperclass();
}
return interfaces.toArray(new Class<?>[interfaces.size()]);
}
}
Copy the code
Here is the implementation principle of mybatis plug-in
The overall implementation is also very simple, we want to intercept the class which method, the implementation of the interceptor, and then configure the information to intercept, mybatis to generate a dynamic proxy object for us, it is ok.
Below we take concrete case to understand its realization principle more deeply
Third, strengthen understanding
Let’s go through it using specific examples
1. Case 1
①. Single interceptor single interception method
Copy@Intercepts({@Signature(
type= Executor.class,
method = "update",
args = {MappedStatement.class ,Object.class})}
)
public class ExamplePlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("before execute update");
Object returnObject = invocation.proceed();
System.out.println("after execute update");
returnreturnObject; }}Copy the code
Let’s say we have this interceptor set up so far
A CachingExecutor proxy object is generated, assuming that the fully qualified name of the proxy object is com.sun.proxy.$Proxy0, and that the object object is implements Executor’s
The update method stack that calls the proxy object is as follows
2. Case 2
①. Single interceptor and multiple interceptor methods
Suppose there is a requirement to add point-like interception logic to StatementHandler’s UPDATE
Copy@Intercepts({@Signature(
type= Executor.class,
method = "update",
args = {MappedStatement.class ,Object.class})},
{@Signature(
type= StatementHandler.class,
method = "update",
args = {Statement.class})}
)
public class ExamplePlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("before execute update");
Object returnObject = invocation.proceed();
System.out.println("after execute update");
returnreturnObject; }}Copy the code
It’s also the logic of getting the Executor, so we’re not going to get a proxy object that implements both the Executor and StatementHandler interfaces, only the Executor interface, because of this
Copy// Type is CachingExecutor
private staticClass<? >[] getAllInterfaces(Class<? > type, Map<Class<? >, Set<Method>> signatureMap) { Set<Class<? >> interfaces =new HashSet<>();
while(type ! =null) {
// The interface Executor of type is clearly in Map
for(Class<? > c : type.getInterfaces()) {// Only instances whose interface is Executor are allowed to be proxied because the proxied class must be an Executor implementation class
if (signatureMap.containsKey(c)) {
interfaces.add(c);
}
}
type = type.getSuperclass();
}
// The only interface that can be found is Executor
return interfaces.toArray(newClass<? >[interfaces.size()]); }Copy the code
In this case it’s the same thing as up here
The difference is that if we need to create an instance of StatementHandler, the proxy object becomes an instance of StatementHandler’s implementation class
Copypublic StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
// The interceptor enhanced proxy object StatementHandler
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
Copy the code
3. Case 3
Multiple interceptors single interceptor method
What if there are multiple interceptors?
Copy@Intercepts({@Signature(
type= Executor.class,
method = "update",
args = {MappedStatement.class ,Object.class})}
)
public class ExamplePluginA implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("before execute update");
Object returnObject = invocation.proceed();
System.out.println("after execute update");
returnreturnObject; }}@Intercepts({@Signature(
type= Executor.class,
method = "flushStatements",
args = {})}
)
public class ExamplePluginB implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("before execute flushStatements");
Object returnObject = invocation.proceed();
System.out.println("after execute flushStatements");
returnreturnObject; }}Copy the code
With the two interceptors above, a proxy object can become a delegate object, that is, generate a proxy object for an object that is already a proxy object.
Assuming that a proxy class named $Proxy0 is generated initially, the InterceptorChain#pluginAll method uses this proxy class as a delegate class to generate a new proxy class. The first proxy class performs the ExamplePluginA interception logic, The second proxy class performs the interception logic of ExamplePluginB.
As you can see from the pluginAll method, the last proxy class created at the end of the interceptor chain is called first because it is actually returned to the client for use.
Assuming the second proxy class is named $Proxy1, take a look at what the method call stack looks like in this case
Four,
Why is mybatis implemented like this?
-
Why not use a chain of filters?
The filter chain approach, it doesn’t really work here, although maybe it does, but in my opinion, the filter chain is more about filtering, it’s about making a conditional judgment on the call request, whether it meets a certain condition and therefore whether it has a certain permission to continue. What it determines is whether the call can continue.
And interceptors, more is the intercept call request, to intercept the request after doing some additional logic, the agent is this way, it is an enhancement of the original logic, will not change the original call destination, no matter how many layers of the intercept treatment, will eventually reach its destination (except an exception occurs)
If you use filters, you need multiple filter chains, and each class needs to hold a reference to its filter chain and so on. With my shallow knowledge, it is not necessary, but it is too troublesome. Dynamic proxy is still the best choice.
-
Why encapsulate?
Recall that we implemented our own InvocationHandler directly using the Jdk’s dynamic proxy, and then just wrote it out of the way, i.e., we made an enhancement.
But the situation is more complicated here, and there may be multiple methods of multiple classes that need to be enhanced. For Executor, for example, if we were to enhance each method, and the enhancement logic for each method was different, and in fact, only one proxy object was generated, In the invoke method of an InvocationHandler, there is a lot of if-else determination (we can also optimize if-else here), which method is being called, and what logic should be executed. This is implementationally fine, but ease of use is also a powerful attraction and competitiveness for a framework. If these things are not elegant or difficult for users to implement, the framework is encapsulated and easy to use, then it will be more popular, and it will be a good framework