首页 >> 大全

深入剖析 mybatis 原理(四)插件的原理和应用

2023-07-29 大全 21 作者:考证青年

红红落叶长埋尘土内

《 一生所爱》

从前现在过去了再不来

红红落叶长埋尘土内

开始终结总是没变改

天边的你飘泊白云外

苦海翻起爱恨

在世间难逃避命运

相亲竟不可接近

或我应该相信是缘份

情人别后永远再不来(消散的情缘)

无言独坐放眼尘世外(愿来日再续)

鲜花虽会凋谢(只愿)

但会再开(为你)

一生所爱 隐约(守候)

在白云外(期待)

苦海翻起爱恨

在世间难逃避命运

相亲竟不可接近

或我应该相信是缘份

苦海翻起爱恨

在世间难逃避命运

相亲竟不可接近

或我应该相信是缘份

前言

这是我们分析 的第四篇文章,看标题,我们是分析 的插件,其实,在前面的三篇文章中,我们已经在剖析源码的时候多多少少接触到 的插件设计和运行过程了,只是没有单独的开一篇文章来讲这个, 的日志系统就是基于插件的。这个在我们之前的源码剖析里也说过。插件在整个 中只占很小的一部分, 不像 ,留了很多的接口给使用者扩展,只留了一个接口给开发者扩展。究其原因还是两者的目标和工作不同。有了之前三篇文章的基础,我们今天研究 的插件,基本就是一个复习的过程,整体上还是比较轻松的。那么,接下来我们就看看吧!

我们将分为 2 个部分来讲述,一个是插件原理,一个是如何应用插件接口并且对比国内流行的插件。

1.插件原理

我们在剖析 的时候,就已经发现了 的插件在他自己框架身上的应用,我们回顾一下在哪里出现的:

newExecutor

newParameterHandler

newResultSetHandler

newStatementHandler

从上面的截图,可以看到,在 4大对象的创建过程中,都调用了 . 方法,可见该方法的重要性,那么该方法的作用是上面呢?我们首先猜测一下,从该方法的名字可以看出,该方法是拦截器链调用插件方法,并传入了一个对象,最后返回了一个该对象,那么,我们看看该方法是如何实现的:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img--75)(%/2/w/1240)]

mybatis原理解析_mybatis原理图_

该类可以说是非常的简单,所谓大道无形,该类是 插件核心,首先有一个插件集合,一个 方法,一个 方法, 一个 方法,可以看的出来该类就是一个过滤器链,类似 的过滤器和的AOP,我们主要看两个方法,一个是 ,一个是 方法,我们首先看看 方法,也即使添加过滤器,什么时候添加呢?我们看看该方法的调用栈:

调用栈

可以看到,从我们的main方法开始,调用了 lder.build 方法,再调用了 的 parse 方法,该方法又调用了自身的 方法,在 方法中调用了 解析 “” 属性,在该方法中调用了 . 方法,该方法又调用了 . 方法,将插件添加进该集合。也就是说,该方法是在解析XML配置文件的时候调用的,将配置好的插件添加进集合中,以便之后的调用。

那么 方法是什么时候运行的呢?我们同样看看他的方法调用栈:

pluginAll  调用栈

我们在方法调用栈图上看到的最后一层调用了 方法,也就是我们 .() 方法生成 的时候,该方法会调用 自身的

ource 方法,然后调用 . 方法插件 ,在 方法中,我们上面的图上也有,调用了 = () .() 方法,返回了一个 ,很显然,这个对象肯定被处理过了。这里我们只说了 对象,4大对象的其余三个对象也是这么生成的,我们就不一一讲了,有兴趣的同学可以翻看源码。

那么,我们就要看看该方法到底是如何实现的,让 的 4 大对象都要调用该方法。

_mybatis原理解析_mybatis原理图

该方法循环了所有的拦截器,并调用了拦截器的 方法,每次都讲返回的 对象作为参数作为下一次调用。那么 方法的内容是什么呢? 是个接口,在 源码中,只有2个实现类,我们找其中一个 实现类看看源码实现:

_mybatis原理解析_mybatis原理图

该类实现了 接口,并重写了3个方法,其中就有我们关注的 方法,该方法内部很简单的调用了 .wrap(, this) 方法,参数是 目标对象和自身,返回了和目标对象,我们该方法内部是如何实现的呢?

_mybatis原理解析_mybatis原理图

楼主只截取了一部分方法,该类实现类 JDK 动态代理中一个重要的接口 接口,而 wrap 方法是一个静态方法,通过传入的拦截器和目标对象,生成一个动态代理返回,注意,目标对象一定要实现某个接口,否则返回自身,我们看看代码实现。

调用自身的 方法,该方法获取了 注解上的 key 是 拦截的类型,value 是拦截的方法(多个)数据。并将数据包装成map返回。获取目标对象的接口,并讲接口放进一个Set中并转成Class 数组返回。根据上面生成的参数map,拦截器,目标对象,生成一个 对象。将生成 对象和接口和类加载器创建一个动态代理对象返回。

好了,我们知道了 方法的作用,也就是说,4 大对象都会调用该方法,都会将这些拦截器把自己包装起来,最后拦截自己。完成切面工作,比如日志。

那么,既然是实现类 JDK 的 接口,那么我们就要看看他的 方法是怎么实现的:

invoke 方法实现

该方法首先从刚刚从拦截器类 注解上取出的参数map中以目标方法的类作为key取出对应的方法集合,如果 方法和注解上定义的方法匹配,就执行拦截器的 方法,注意,此时,会创建一个 对象作为参数传递到 方法中,而这个对象的创建的参数包括 目标对象,代理拦截的方法,代理的参数。

我们回到 中的拦截器例子 类中看看 方法是如何实现的:

mybatis原理图_mybatis原理解析_

该方法只是调用了 的 方法,那么该方法是如何定义的呢?

_mybatis原理解析_mybatis原理图

该方法只是用反射调用刚刚构造函数中的方法。并没有执行任何的操作。也就是说,在 中的 方法中,调用了拦截器的 方法,并传入了 对象,该对象的作用就是将目标对象,目标方法,目标方法参数传入,让拦截器可以取出这些参数并做加强工作。注意,需要在执行完加强操作和执行 的 方法。也就是执行目标对象真正的方法。

到这里,我们已经弄懂了 的拦截器原理,首先拦截器拦截的是 的 4 大对象,我们需要在配置文件中配置拦截器,方便 添加到拦截器链中。 为我们提供了 接口,我们可以在该接口中实现自己的逻辑,主要需要实现 方法,在该方法中利用给定的 对象来对我们的业务做一些增强。而调用拦截器方法的类就是 JDK 动态代理的接口 的实现类 的 方法,该方法会根据目标方法是否匹配拦截器注解的值来决定是否调用拦截器的 方法。并传入封装了目标对象,目标方法,目标方法参数的 实例。

知道了拦截器的实现原理,那么我们就写一个例子来体验一下。

2. 拦截器的应用

首先编写 插件需要遵守几个约定:

实现 接口并实现接口中的方法。在配置文件中配置插件。在实现 接口的类上加上 @ 注解。该注解如下:

_mybatis原理解析_mybatis原理图

仅有一个 注解集合,我们看看 注解有哪些内容:

_mybatis原理解析_mybatis原理图

该注解有3个方法,分别代表着拦截的类型,拦截的哪个方法,拦截的方法的参数(因为可能是重载方法)。也就是说,这是一个方签名注解。

那么我们能拦截哪些类呢?我们前面说,执行 SQL 的是 4大对象,并且这4大对象也都会调用过滤器链,那么他们的调用过程是怎么样的呢?我们看看他们的方法调用栈:

mybatis原理图_mybatis原理解析_

_mybatis原理解析_mybatis原理图

最上面的是 抽象类的构造方法,实现类则是dler,在该构造器中,会创建2个包含了插件的 对象和 对象。那么这个方法是什么时候调用的呢?实际上, 方法,也就是创建 代理的方法是第一个创建的,然后再执行 方法的时候,会创建 对象,而再创建 对象的时候,会创建另外 2 个对象 和 。由此完成 4 大对象的代理创建。那么 4 大对象的创建调用是什么顺序呢?楼主写了一个例子:

_mybatis原理图_mybatis原理解析

楼主拦截了 4 大对象个一个方法,也就是说,只要执行这 4 个方法都会进入 方法,都会答应该对象的引用。我们看看运行结果:

mybatis原理图_mybatis原理解析_

可以看到顺序,首先执行了 的方法,然后执行了 的拦截方法, 再执行 的方法,再执行 的拦截器,最后执行 真正的查询方法。

知道了这个顺序,对我们开发插件是有帮助的。

看着这里,我们应该有个了解了,我们使用插件的目的大部分都是再运行SQL的时候修改SQL,比如分页,比如分表,再原有的SQL上做一些修改,那么怎么才能修改呢?重点就在 的 属性,该接口的实现类会存储SQL语句,比如其中一个实现类 :,我们看看该类的构造:

_mybatis原理解析_mybatis原理图

其中有一个重要的字段 : sql, 该字段就是存储 SQL 语句的字符串,那么我们的任务就是修改这个字段,从而达到我们自定义 SQL 的目的。

既然知道了怎么使用插件,那么我们就来写一个看看:

首先实现拦截器接口:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img--86)(%/2/w/1240)]

我们拦截了 类的 方法,理论上,我们如果想修改 sql,可以拦截 和 都可以。

我们看看 方法,该方法使用了 的 的 wrap 方法,基本就是官方默认的写法,没什么可修改的。而 方法呢?就是可以在配置文件中配置一些参数,可以在运行的时候获取配置文件的参数。最重要的而是 方法,该方法步骤如下:

获取 的目标对象,因为我们拦截的是 对象,那么就可以强转成这个对象,如果你拦截了2个对象,就需要进行判断。打印该对象的 sql 语句。使用反射修改sql。打印修改后的sql 语句。然后运行。

我们看看执行结果:

mybatis原理解析_mybatis原理图_

从结果中可以看到,我们拦截成功,并且成功执行了 sql 语句,返回了空值。如果不拦截,将返回正常的值。

mybatis原理解析__mybatis原理图

返回了正常结果。

_mybatis原理图_mybatis原理解析

到这里,我们已经知道如何使用 的插件,虽然这个例子非常的简单,但市面的分页插件基本都是这样设计的。都是通过修改 这个对象来修改Sql,有的可能只修改了这个对象的 Sql 字段,有的直接重新创建一个对象。比如 插件。我们看看该类的关键源码:

@SuppressWarnings({"rawtypes", "unchecked"})
@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}),}
)
public class PageInterceptor implements Interceptor {//缓存count查询的msprotected Cache<String, MappedStatement> msCountMap = null;private Dialect dialect;private String default_dialect_class = "com.github.pagehelper.PageHelper";private Field additionalParametersField;private String countSuffix = "_COUNT";@Overridepublic Object intercept(Invocation invocation) throws Throwable {try {Object[] args = invocation.getArgs();MappedStatement ms = (MappedStatement) args[0];Object parameter = args[1];RowBounds rowBounds = (RowBounds) args[2];ResultHandler resultHandler = (ResultHandler) args[3];Executor executor = (Executor) invocation.getTarget();CacheKey cacheKey;BoundSql boundSql;//由于逻辑关系,只会进入一次if(args.length == 4){//4 个参数时boundSql = ms.getBoundSql(parameter);cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);} else {//6 个参数时cacheKey = (CacheKey) args[4];boundSql = (BoundSql) args[5];}List resultList;//调用方法判断是否需要进行分页,如果不需要,直接返回结果if (!dialect.skip(ms, parameter, rowBounds)) {//反射获取动态参数String msId = ms.getId();Configuration configuration = ms.getConfiguration();Map<String, Object> additionalParameters = (Map<String, Object>) additionalParametersField.get(boundSql);//判断是否需要进行 count 查询if (dialect.beforeCount(ms, parameter, rowBounds)) {String countMsId = msId + countSuffix;Long count;//先判断是否存在手写的 count 查询MappedStatement countMs = getExistedMappedStatement(configuration, countMsId);if(countMs != null){count = executeManualCount(executor, countMs, parameter, boundSql, resultHandler);} else {countMs = msCountMap.get(countMsId);//自动创建if (countMs == null) {//根据当前的 ms 创建一个返回值为 Long 类型的 mscountMs = MSUtils.newCountMappedStatement(ms, countMsId);msCountMap.put(countMsId, countMs);}count = executeAutoCount(executor, countMs, parameter, boundSql, rowBounds, resultHandler);}//处理查询总数//返回 true 时继续分页查询,false 时直接返回if (!dialect.afterCount(count, parameter, rowBounds)) {//当查询总数为 0 时,直接返回空的结果return dialect.afterPage(new ArrayList(), parameter, rowBounds);}}//判断是否需要进行分页查询if (dialect.beforePage(ms, parameter, rowBounds)) {//生成分页的缓存 keyCacheKey pageKey = cacheKey;//处理参数对象parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey);//调用方言获取分页 sqlString pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey);BoundSql pageBoundSql = new BoundSql(configuration, pageSql, boundSql.getParameterMappings(), parameter);//设置动态参数for (String key : additionalParameters.keySet()) {pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));}//执行分页查询resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);} else {//不执行分页的情况下,也不执行内存分页resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql);}} else {//rowBounds用参数值,不使用分页插件处理时,仍然支持默认的内存分页resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);}return dialect.afterPage(resultList, parameter, rowBounds);} finally {dialect.afterAll();}}

该类是国内著名插件 的拦截器。该拦截器拦截了 的两个重载方法,在 方法内部,会从 对象中取出参数,目标对象,最终会创建一个 的 对象,执行 的 query 方法。那么分页参数放在哪里的呢?放在了 的 变量中。然后到这个方法中取出该变量。传入 sql 语句中。最后执行。

还有一个注意的地方,就是我们之前的简单 demo 里,只是使用了反射来修改 sql 语句, 中有一个反射的工具类:,他可以快捷的修改某个类的属性,底层是通过反射,而且支持 OGNL 表达式,非常的强大。我们将我们的例子修改一下:

mybatis原理解析__mybatis原理图

查看运行结果:

mybatis原理图__mybatis原理解析

使用 的工具类 ,使用 OGNL 表达式,修改SQL成功。返回了空值。

总结

我们分析了 中常用的插件,知道了他的原理,就是每次创建4大对象的时候,都会将场景封装到对象中,如果有多个,就层层包装。这个是通过动态代理的技术实现的。然后在运行的时候会调用实现了动态代理 接口的 类的 方法,而该方法会调用拦截器器的 方法,并传入封装了目标对象,目标方法,目标方法参数的 供使用者修改或加强。

修改 Sql 有多种方式,最终都是修改 的 中的 sql 字段,无论是直接修改属性,还是重新创建一个 对象。还有一个 的 类,该类是 提供的一个强大的通过反射修改对象属性的工具类, 中多次使用该类。

在我们的项目中,通过 的拦截器可以实现很多功能,比如分页插件,再比如 分表插件,因为如果一张表中数据过大,会拆分为多个表,这个时候可以通过一些特定的参数,将表的后缀加上去,起到自动分表的效果。而 XML 中的 SQL 是感知不到的。

总之, 插件可以实现很多功能。但使用他的时候请一定小心,毕竟这修改了 底层的逻辑。

good luck!!!!

关于我们

最火推荐

小编推荐

联系我们


版权声明:本站内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 88@qq.com 举报,一经查实,本站将立刻删除。备案号:桂ICP备2021009421号
Powered By Z-BlogPHP.
复制成功
微信号:
我知道了