Mybaties插件原理
本文主要解析Mybaties插件基本原理,所谓的插件就是Mybaties提供的Interceptor拦截器,用于SQL执行时动态对“执行方法”、“参数”、“返回值”或“SQL语句”的拦截处理。在业务很多场景都会用到,如分页和sql操作同步等场景。
1. MyBatis拦截原理
MyBatis插件可以拦截四个接口的方法:Executor,ParameterHandler,StatementHandler,ResultSetHandler。顾名思义,这四个接口都有名称相近的处理操作,前述四个接口分别表示:执行器拦截、参数拦截、结果集处理和SQL构建。以Executor为例,拦截Executor中int update(MappedStatement ms, Object parameter) throws SQLException方法,也就是业务在执行insert、update或者delete操作前后都会被该方法拦截。将操作拦截下来,判断结果是否正确或者满足,在自定义操作。
拦截器接口代码
package org.apache.ibatis.plugin;
import java.util.Properties;
/**
* @author Clinton Begin
*/
public interface Interceptor {
// 拦截处理放在该方法中
Object intercept(Invocation invocation) throws Throwable;
// 代理生成实现类对象,说白了为了执行实现类对象的intercept方法
Object plugin(Object target);
void setProperties(Properties properties);
}
Interceptor.plugin(Object target)方法被InterceptorChain.pluginAll(Object target)方法调用,源码
package org.apache.ibatis.plugin;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* @author Clinton Begin
*/
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
// 会被Configuration配置类调用
// 在进行增删改查时,会遍历所有拦截器,并执行其plugin方法
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
// 会被Configuration.addInterceptor(Interceptor interceptor)调用
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}
}
MyBatis提供了@Intercepts和 @Signature拦截器相关的注解。增删改查等数据库操作会进入拦截器Interceptor,拦截时会根据拦截器上的这两个注解值来确定拦截与否。比如@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})})表示该拦截器将处理Executor组件且参数是(MappedStatement.class, Object.class)的update方法。具体的判断逻辑是在Plugin.wrap(target, this)中。
Mybaties的Plugin类源码
// 该类实现了InvocationHandler接口,用于JDK动态代理
// 这样做的好处是在动态代理调用invoke方法时,会直接使用生成代理对象时缓存的方法如target、interceptor和signatureMap。
public class Plugin implements InvocationHandler {
private final Object target;
private final Interceptor interceptor;
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;
}
// 生成代理对象
public static Object wrap(Object target, Interceptor interceptor) {
// @Signature注解的type属性为key,method属性集合为value的Map对象
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
// 获取被代理对象实现的接口,但接口类型必须在signatureMap的key中
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
// JDK动态代理是接口的代理,实现接口不能为空
if (interfaces.length > 0) {
// 生成代理对象
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
// 代理Interceptor.intercept(Invocation invocation)方法
@Override
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);
}
}
// 该方法主要用于返回参数是@Signature注解的type属性为key,method属性集合为value的Map对象
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
// 拿到拦截器上Intercepts注解,不存在时会抛异常
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());
}
Signature[] sigs = interceptsAnnotation.value();
// 返回的对象
Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
for (Signature sig : sigs) {
// sig.type()即是@Signature注解的type属性
Set<Method> methods = signatureMap.get(sig.type());
// signatureMap没有值时,新建一个Set存入
if (methods == null) {
methods = new HashSet<Method>();
signatureMap.put(sig.type(), methods);
}
try {
// 通过函数名和参数类型确定具体的Method
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;
}
// 获取被代理对象实现的接口,但接口类型必须在signatureMap的key中
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()) {
// 要包含在signatureMap的key中
if (signatureMap.containsKey(c)) {
interfaces.add(c);
}
}
// 父类
type = type.getSuperclass();
}
return interfaces.toArray(new Class<?>[interfaces.size()]);
}
}
在调用Mybaties配置类Configuration的newExecutor、newParameterHandler、newResultSetHandler和newStatementHandler方法时都会调用所有拦截器。
2. 一个拦截器示例
package com.mingo.es.sync.mybaties.plugin;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.springframework.stereotype.Component;
import java.sql.Statement;
import java.util.Arrays;
import java.util.Properties;
/**
* MyBaties拦截测试。这里只拦截处理:
* Executor.update(MappedStatement ms, Object parameter) 和
* StatementHandler.update(Statement statement)
*
* update表示insert、update和delete
*
* @author Doflamingo
*/
@Component
@Slf4j
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
@Signature(type = StatementHandler.class, method = "update", args = {Statement.class}),
})
public class TestInterceptor implements Interceptor {
/**
* 拦截处理
*
* @param invocation
* @return
* @throws Throwable
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object res = invocation.proceed();
System.out.println("拦截处理的接口和方法分别是:" +
Arrays.toString(invocation.getTarget().getClass().getInterfaces()) + ", " +
invocation.getMethod().getName()
);
return res;
}
/**
* 生成代理对象
*
* @param target
* @return
*/
@Override
public Object plugin(Object target) {
// 代理对象,用于触发intercept()
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
StatementHandler.update(Statement statement)方法表示将 insert、update、delete 操作推送到数据库。写一个测试用例,将一条数据插入数据库表中。执行结果如下
拦截处理的接口和方法分别是:[interface org.apache.ibatis.executor.statement.StatementHandler], update
拦截处理的接口和方法分别是:[interface org.apache.ibatis.executor.Executor], update