优质文章,及时送达
学习背景
最近公司在做一些数据库安全方面的事情,如数据库中不能存手机号明文,不能存身份证号明文, 但是项目已经进行了好几个月了, 这时候在应用层面去改显然不太现实, 所以就有了Mybatis的自定义插件就出场了!
插件知识点总述
一. mybatis的插件,使用拦截器链的方式调用
其代码抽象如下所示org.apache.ibatis.plugin.InterceptorChain
public Object pluginAll(Object target) { for (Interceptor interceptor : interceptors) { target = interceptor.plugin(target); } return target; }
不停的把target传入到插件的plugin方法中, 让插件对参数/返回值/语句/执行器做修改,如下图所示
二. 使用动态代理的方式调用插件功能
这是一个通用的思路,在想对一个原有的方法/功能进行加强的时候,首要的思路就是使用代理, 然后采用如下的代码格式来加强
beforeInvoke();//在调用之前加强proxy.invoke();//原来的逻辑afterInvoke();// 在调用之后加强
mybatis的动态代理也是如此,关键代码如下org.apache.ibatis.plugin.Plugin
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);
三. 可以单独拿出来脱离mybatis框架使用
小刀觉得, 这一点才是Mybatis插件的精华所在
这一点很关键,不要被Mybatis框架给局限了,如下面的测试类,还可以使用Mybatis的插件去拦截HashMap的get方法,这样就可以提炼一个小型的AOP了.
插件的使用以及原理
在这里我们用Mybatis的官方测试代码为例:
/**
*单元测试类
*/
@Test
void mapPluginShouldInterceptGet() {
Map map = new HashMap();
map = (Map) new AlwaysMapPlugin().plugin(map);
assertEquals("Always", map.get("Anything"));
}
/**
*插件类,我们在自己的项目中也应该建这样的类
*/
@Intercepts({@Signature(type = Map.class, method = "get", args = {Object.class})})
public static class AlwaysMapPlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) {
return "Always";
}
}
这份代码中要注意以下几个地方
@Intercepts({@Signature(type = Map.class, method = "get", args = {Object.class})})
和工具封装的思路类似,把描述和实现分开,这个注解就是描述性信息,描述我们要去拦截哪个类的哪个方法, 这个方法有什么参数,这样去唯一定位到那个方法
implements Interceptor
为什么实现接口要单独提出来说呢, 我们点进去这个接口看一下, 发现有两个default方法, 其中plugin
是Mybatis插件中的核心
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
我们在测试类中可以看到map = (Map) new AlwaysMapPlugin().plugin(map);
调用了plugin
方法之后,就可以强转成原来的类使用,这时候就会调用加强的方法, 所以这里的plugin方法应该就是生成动态代理的方法,我们跟踪进Plugin.wrap这个方法里面看一下,如下所示:
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, interceptor, signatureMap));
}
return target;
}
getAllInterfaces
上面源码中的这个方法要注意
我们都知道jdk的动态代理的前提是对接口做加强. 现有接口A, 类B实现A , 我们要加强B, 动态代理会为A建一个代理类C,然后把B的实例b 传入C中做target . 然后调用C中的同名方法, 来实现看起来调的都是同一个方法,而且功能也得到了加强. 所以getAllInterfaces
中有一个很重要的逻辑,就是要判断是否是接口
for (Class<?> c : type.getInterfaces()) {
if (signatureMap.containsKey(c)) {
interfaces.add(c);
}
}
如上所示: 只有signatureMap
中包含这个接口的时候, 动态代理的参数interfaces中才会加入这个接口. 同样也就是说我们的@Intercepts
这个注解中的type,只能是接口类型, 这个一定要注意, 在mybatis中使用还没什么,单独提出来的时候就容易出错.
public Object intercept
这个是开发人员自己实现的逻辑
在这里要注意传入的参数new Invocation(target, method, args)
,我们点击去Invocation的代码中可以看到, proceed方法可以执行原有的逻辑
public Object proceed() throws InvocationTargetException, IllegalAccessException {
return method.invoke(target, args);
}
因为在重写的intercept
方法中,可以调用:invocation.proceed()
来获取原来的逻辑的返回值,然后对返回值做修改,
mybatis插件在源码中的调用流程
这一段是为了大家方便在源码中查看插件是从哪里开始,然后到哪里执行.对整体有个印象
一. 从xml中读取插件配置,并加入到插件链中
org.apache.ibatis.builder.xml.XMLConfigBuilder.parseConfiguration
pluginElement(root.evalNode("plugins"));
二. 在获取ParameterHandler/ResultSetHandler/StatementHandler/Executor的时候调用插件链
org.apache.ibatis.session.Configuration
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
// 在这里调用
parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
}
三. 在pluginAll()
方法中如上所述生成代理对象,把我们自定义的加强逻辑给加上去. 然后在执行Mybatis的mapper的时候,就可以走我们插件里面逻辑了
总结
MyBatis源码并不难,整理好逻辑好一步步的跟踪下去就可以了,建议阅读本文时把mybatis源码也打开,跟着一起看,紧紧抓住动态代理的实现就肯定会把这个弄清的