聊聊MyBatis插件(拦截器)
Mybatis为我们提供了一个插件扩展功能,这个插件又叫拦截器。通过Mybatis拦截器可以拦截某些方法的调用,我们可以选择在这些被拦截的方法执行前后加上某些逻辑。
Mybatis拦截器可以拦截Executor、ParameterHandler、StatementHandler、ResultSetHandler四个对象里面的方法。
添加一个自定义拦截器的步骤分两步:
1.实现Interceptor接口,自定义一个拦截器。
2.将前面自定义的拦截器注册到spring容器中。
Interceptor接口的源码如下:
public interface Interceptor {
Object intercept(Invocation var1) throws Throwable;
Object plugin(Object var1);
void setProperties(Properties var1);
}
该接口中有三个方法,intercept、plugin、setProperties,其中intercept和plugin方法尤为重要。
1.plugin方法是拦截器用于封装目标对象的,在plugin方法中我们可以决定是否要进行拦截进而决定要返回一个什么样的目标对象,通过该方法我们可以返回目标对象本身,也可以返回一个他的代理。当返回的是代理的时候,我们可以对其中的方法进行拦截来调用intercept方法。
2.intercept方法就是要进行拦截的时候要执行的方法,通过invocation.proceed()执行拦截的原方法,可以在原方法前后添加自己的逻辑。
对于plugin方法而言,其实Mybatis已经为我们提供了一个实现。Mybatis中有一个叫做Plugin的类,里面有一个静态方法wrap(Object target,Interceptor interceptor),通过该方法可以决定要返回的对象是目标对象还是代理对象。这里我们先来看一下Plugin的源码:
public class Plugin implements InvocationHandler {
private Object target;
private Interceptor interceptor;
private 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) {
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
return interfaces.length > 0 ? Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)) : target;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set<Method> methods = (Set)this.signatureMap.get(method.getDeclaringClass());
return methods != null && methods.contains(method) ? this.interceptor.intercept(new Invocation(this.target, method, args)) : method.invoke(this.target, args);
} catch (Exception var5) {
throw ExceptionUtil.unwrapThrowable(var5);
}
}
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
Intercepts interceptsAnnotation = (Intercepts)interceptor.getClass().getAnnotation(Intercepts.class);
if (interceptsAnnotation == null) {
throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
} else {
Signature[] sigs = interceptsAnnotation.value();
Map<Class<?>, Set<Method>> signatureMap = new HashMap();
Signature[] var4 = sigs;
int var5 = sigs.length;
for(int var6 = 0; var6 < var5; ++var6) {
Signature sig = var4[var6];
Set<Method> methods = (Set)signatureMap.get(sig.type());
if (methods == null) {
methods = new HashSet();
signatureMap.put(sig.type(), methods);
}
try {
Method method = sig.type().getMethod(sig.method(), sig.args());
((Set)methods).add(method);
} catch (NoSuchMethodException var10) {
throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + var10, var10);
}
}
return signatureMap;
}
}
private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
HashSet interfaces;
for(interfaces = new HashSet(); type != null; type = type.getSuperclass()) {
Class[] var3 = type.getInterfaces();
int var4 = var3.length;
for(int var5 = 0; var5 < var4; ++var5) {
Class<?> c = var3[var5];
if (signatureMap.containsKey(c)) {
interfaces.add(c);
}
}
}
return (Class[])interfaces.toArray(new Class[interfaces.size()]);
}
}
我们先看一下Plugin的wrap方法:
public static Object wrap(Object target, Interceptor interceptor) {
// 获取拦截器钟@Intercepts注解的内容,key:四大对象中要拦截的对象,value:要拦截的对象方法
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
// interfaces.length > 0:目标对象type需要被拦截
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
// 需要被拦截则返回代理对象,否则直接返回当前对象
return interfaces.length > 0 ? Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)) : target;
}
private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
HashSet interfaces;
for(interfaces = new HashSet(); type != null; type = type.getSuperclass()) {
Class[] var3 = type.getInterfaces();
int var4 = var3.length;
for(int var5 = 0; var5 < var4; ++var5) {
Class<?> c = var3[var5];
if (signatureMap.containsKey(c)) {
interfaces.add(c);
}
}
}
return (Class[])interfaces.toArray(new Class[interfaces.size()]);
}
它根据当前的Interceptor上面的注解定义哪些接口需要拦截,然后判断当前目标对象是否有实现对应需要拦截的接口,如果没有则返回目标对象本身,如果有则返回一个代理对象。而这个代理对象的InvocationHandler正是一个Plugin。所以当目标对象在执行接口方法时,如果是通过代理对象执行的,则会调用对应InvocationHandler的invoke方法,也就是Plugin的invoke方法。所以接着我们来看一下该invoke方法的内容:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 获取该拦截对象中需要拦截的方法
Set<Method> methods = (Set)this.signatureMap.get(method.getDeclaringClass());
// 判断当前执行方法是否是需要拦截的方法,若是则调用拦截器的intercept方法,否则字节调用该方法本身
return methods != null && methods.contains(method) ? this.interceptor.intercept(new Invocation(this.target, method, args)) : method.invoke(this.target, args);
} catch (Exception var5) {
throw ExceptionUtil.unwrapThrowable(var5);
}
}
这里invoke方法的逻辑是:如果当前执行的方法是定义好的需要拦截的方法,则把目标对象、要执行的方法以及方法参数封装成一个Invocation对象,再把封装好的Invocation作为参数传递给当前拦截器的intercept方法。如果不需要拦截,则直接调用当前的方法。Invocation中定义了一个proceed方法,其逻辑就是调用当前方法,所以如果在intercept中需要继续调用当前方法的话可以调用invocation的procced方法。
自定义拦截器
@Intercepts({
@Signature(type = ParameterHandler.class, method = "setParameters", args = PreparedStatement.class)
})
public class EncryptInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();
return invocation.proceed();
}
@Override
public Object plugin(Object o) {
Object wrap = Plugin.wrap(o, this);
log.info("****************plugin方法{},入参:{},返回值:{}********************", o==wrap, o, wrap);
return wrap;
}
@Override
public void setProperties(Properties properties) {
log.info("****************setProperties方法********************");
}
}
对于这个拦截器而言,当Mybatis是要ParameterHandler对象的时候就会返回一个代理对象,其他都是原目标对象本身。然后当ParameterHandler代理对象执行参数为PreparedStatement的setParameters方法时,就会触发拦截器的intercept方法。