问答文章1 问答文章501 问答文章1001 问答文章1501 问答文章2001 问答文章2501 问答文章3001 问答文章3501 问答文章4001 问答文章4501 问答文章5001 问答文章5501 问答文章6001 问答文章6501 问答文章7001 问答文章7501 问答文章8001 问答文章8501 问答文章9001 问答文章9501

假装是小白之重学MyBatis(二)

发布网友 发布时间:2024-09-29 03:39

我来回答

1个回答

热心网友 时间:2024-09-29 13:48

前言

本篇我们来介绍MyBatis插件的开发,这个也是来源于我之前的一个面试经历,面试官为我如何统计Dao层的慢SQL,我当时的回答是借助于Spring的AOP机制,拦截Dao层所有的方法,但面试官又问,这事实上不完全是SQL的执行时间,这其中还有其他代码的时间,问我还有其他思路吗? 我想了想说没有,面试官接着问,有接触过MyBatis插件的开发吗? 我说没接触过。 但后面也给我过了,我认为这个问题是有价值的问题,所以也放在了我的学习计划中。

看本篇之前建议先看:

《代理模式-AOP绪论》

《假装是小白之重学MyBatis(一)》

如果有人问上面两篇文章在哪里可以找的到,可以去掘金或者思否翻翻,目前公众号还没有,预计年中会将三个平台的文章统一一下。

概述

翻阅官方文档的话,MyBatis并没有给处插件的具体定义,但基本上还是拦截器,MyBatis的插件就是一些能够拦截某些MyBats核心组件方法,增强功能的拦截器。官方文档中列出了四种可供增强的切入点:

Executor

执行SQL的核心组件。拦截Executor 意味着要干扰或增强底层执行的CRUD操作

ParameterHandler

拦截该ParameterHandler,意味着要干扰SQL参数注入、读取的动作。

ResultSetHandler

拦截该ParameterHandler, 要干扰/增强封装结果集的动作

StatementHandler

拦截StatementHandler ,则意味着要干扰/增强Statement的创建和执行的动作

当然还是从HelloWorld开始

要做MyBatis的插件,首先要实现MyBatis的Interceptor 接口 , 注意类不要导错了,Interceptor很抢手,该类位于org.apache.ibatis.plugin.Interceptor下。实现该接口,MyBatis会将该实现类当作MyBatis的拦截器,那拦截哪些方法,该怎么指定呢? 通过 @Intercepts注解来实现,下面是使用示例:

@Intercepts(@Signature(type?=?Executor.class,?method?=?"query",????????args?=?{MappedStatement.class,?Object.class,?RowBounds.class,?ResultHandler.class}))public?class?MyBatisPluginDemo?implements?Interceptor?{????????@Override????public?Object?intercept(Invocation?invocation)?throws?Throwable?{????????System.out.println("into?invocation?..........");????????System.out.println(invocation.getTarget());????????System.out.println(invocation.getMethod().getName());????????System.out.println(Arrays.toString(invocation.getArgs()));????????return?invocation.proceed();????}}@Intercepts可以填多个@Signature,@Signature是方法签名,type用于定位类,method定位方法名,args用于指定方法的参数类型。三者加在一起就可以定位到具体的方法。注意写完还需要将此插件注册到MyBatis的配置文件中,让MyBatis加载该插件。

注意这个标签一定要放在environments上面,MyBatis严格限制住了标签的顺序。

<plugins>????<plugin?interceptor="org.example.mybatis.MyBatisPluginDemo"></plugin></plugins>

我们来看下执行结果:

性能分析插件走起

那拦截谁呢? 目前也只有Executor 和StatementHandler 供我们选择,我们本身是要看SQL耗时,Executor 离SQL执行还有些远,一层套一层才走到SQL执行,MyBatis中标签的执行过程在《MyBatis源码学习笔记(一) 初遇篇》已经讲述过了,这里不再赘述,目前来看StatementHandler 是离SQL最近的, 它的实现类就直接走到JDBC了,所以我们拦截StatementHandler ,那有的插入插了很多值,我们要不要拦截,当然也要拦截, 我们的插件方法如下:

@Intercepts({@Signature(type?=?StatementHandler.class,?method?=?"query",????????args?=?{Statement.class,?ResultHandler.class}),?@Signature(type?=?StatementHandler.class,method?=??"update"?,args?=?Statement.class?)})public?class?MyBatisSlowSqlPlugin?implements?Interceptor?{????@Override????public?Object?intercept(Invocation?invocation)?throws?Throwable?{????????System.out.println("-----开始进入性能分析插件中----");????????long?startTime?=?System.currentTimeMillis();????????Object?result?=?invocation.proceed();????????long?endTime?=?System.currentTimeMillis();???????//?query方法入参是statement,所以我们可以将其转为Statement????????if?(endTime?-?startTime?>?1000){????????}????????return?result;????}}

那对应的SQL该怎么拿? 我们还是到StatementHandler去看下:

我们还是得通过Statement这个入参来拿, 我们试试看, 你会发现在日志级别为DEBUG之上,会输出SQL,像下面这样:

如果日志级别为DEBUG输出会是下面这样:

这是为什么呢? 如果看过《MyBatis源码学习笔记(一) 初遇篇》这篇的可能会想到,MyBatis架构中的日志模块,为了接入日志框架,就会用到代理,那么这个肯定就是代理类,我们打断点来验证一下我们的想法:

代理分析

我原本的想法是PreparedStatementLogger的代理类,仔细一想,感觉不对,感觉自己还是对代理模式了解不大透,于是我就又把之前的文章《代理模式-AOP绪论》看了一下,动态代理模式的目标:

我们有一批类,然后我们想在不改变它们的基础之上,增强它们, 我们还希望只着眼于编写增强目标对象代码的编写。

我们还希望由程序来编写这些类,而不是由程序员来编写,因为太多了。

在《代理模式-AOP绪论》中我们做的是很简单的代理:

public?interface?IRentHouse?{????void?rentHouse();????void?study();}public?class?RentHouse?implements?IRentHouse{????@Override????public?void?rentHouse()?{????????System.out.println("sayHello.....");????}????@Override????public?void?study()?{????????System.out.println("say?Study");????}}

我们现在的需求是增强IRentHouse中的方法,用静态代理就是为IRentHouse再做一个实现类,相当于在RentHouse上再包装一层。但如果我有很多想增强的类呢,这样去包装,事实上对代码的侵入性是很大的。对于这种状况,我们最终的选择是动态代理,在运行时产生接口实现类的代理类,我们最终产生代理对象的方法是:

/**???*?@param?target?为需要增强的类???*?@return?返回的对象在调用接口中的任意方法都会走到Lambda回调中。*/private?static??Object?getProxy(Object??target){????????Object?proxy?=?Proxy.newProxyInstance(target.getClass().getClassLoader(),?target.getClass().getInterfaces(),?(proxy1,?method,?args)?->?{????????????System.out.println("方法开始执行..........");????????????Object?obj?=?method.invoke(target,?args);????????????System.out.println("方法执行结束..........");????????????return?obj;????????});????????return?proxy;??}

接下来我们来看下MyBatis是怎么包装的,我们还是从PreparedStatementLogger开始看:

InvocationHandler是动态代理的接口,BaseJdbcLogger这个先不关注。值得关注的是:

public?static?PreparedStatement?newInstance(PreparedStatement?stmt,?Log?statementLog,?int?queryStack)?{??InvocationHandler?handler?=?new?PreparedStatementLogger(stmt,?statementLog,?queryStack);??ClassLoader?cl?=?PreparedStatement.class.getClassLoader();??return?(PreparedStatement)?Proxy.newProxyInstance(cl,?new?Class[]{PreparedStatement.class,?CallableStatement.class},?handler);}

可能有同学会问newProxyInstance为什么给了两个参数, 因为CallableStatement继承了PreparedStatement。 这里是一层,事实上还能点出来另外一层,在ConnectionLogger的回调中(ConnectionLogger也实现了InvocationHandler,所以这个也是个代理回调类),ConnectionLogger的实例化在BaseExecutor这个类里面完成,如果你还能回忆JDBC产生SQL的话,当时的流程事实上是这样的:

????public?static?boolean?execute(String?sql,?Object...?param)?throws?Exception?{????????boolean?result?=?true;????????Connection?connection?=?null;????????PreparedStatement?preparedStatement?=?null;????????try?{????????????//获取数据库连接????????????connection?=?getConnection();????????????connection.setAutoCommit(false);????????????preparedStatement?=?connection.prepareStatement(sql);????????????//?设置参数?????????????for?(int?i?=?0;?i?<?param.length;?i++)?{????????????????preparedStatement.setObject(i,?param[i]);????????????????preparedStatement.addBatch();????????????}????????????preparedStatement.executeBatch();????????????//提交事务????????????connection.commit();????????}?catch?(SQLException?e)?{????????????e.printStackTrace();????????????if?(connection?!=?null)?{????????????????try?{????????????????????connection.rollback();????????????????}?catch?(SQLException?ex)?{????????????????????ex.printStackTrace();????????????????????//?日志记录事务回滚失败????????????????????result?=?false;????????????????????return?result;????????????????}????????????}????????????result?=?false;????????}?finally?{????????????close(preparedStatement,?connection);????????}????????return?result;????}

我们来捋一下,ConnectionLogger是读Connection的代理,但是Connection接口中有许多方法, 所以ConnectionLogger在回调的时候做了判断:

@Overridepublic?Object?invoke(Object?proxy,?Method?method,?Object[]?params)????throws?Throwable?{??try?{????if?(Object.class.equals(method.getDeclaringClass()))?{??????return?method.invoke(this,?params);????}????if?("prepareStatement".equals(method.getName())?||?"prepareCall".equals(method.getName()))?{??????if?(isDebugEnabled())?{????????debug("?Preparing:?"?+?removeExtraWhitespace((String)?params[0]),?true);??????}??????//?Connection?的prepareStatement方法、prepareCall会产生PreparedStatement??????PreparedStatement?stmt?=?(PreparedStatement)?method.invoke(connection,?params);??????//?然后PreparedStatementLogger产生的还是stmt的代理类??????//?我们在plugin中拿到的就是????????stmt?=?PreparedStatementLogger.newInstance(stmt,?statementLog,?queryStack);??????return?stmt;????}?else?if?("createStatement".equals(method.getName()))?{??????Statement?stmt?=?(Statement)?method.invoke(connection,?params);??????stmt?=?StatementLogger.newInstance(stmt,?statementLog,?queryStack);??????return?stmt;????}?else?{??????return?method.invoke(connection,?params);????}??}?catch?(Throwable?t)?{????throw?ExceptionUtil.unwrapThrowable(t);??}}

PreparedStatementLogger是回调类,这个PreparedStatementLogger有对应的Statement,我们通过Statement就可以拿到对应的SQL。那回调类和代理类是什么关系呢, 我们来看下Proxy类的大致构造:

所以我最初的想法是JDK为我们产生的类里面有回调类实例这个对象会有InvocationHandler成员变量,但是如果你用getClass().getDeclaredField("h")去获取发现获取不到,那么代理类就没有这个回调类实例,那我们研究一下getProxyClass0这个方法:

private?static?Class<?>?getProxyClass0(ClassLoader?loader,???????????????????????????????????????Class<?>...?interfaces)?{????if?(interfaces.length?>?65535)?{????????throw?new?IllegalArgumentException("interface?limit?exceeded");????}????//?If?the?proxy?class?defined?by?the?given?loader?implementing????//?the?given?interfaces?exists,?this?will?simply?return?the?cached?copy;????//?otherwise,?it?will?create?the?proxy?class?via?the?ProxyClassFactory????//?proxyClassCache?是?new?WeakCache<>(new?KeyFactory(),?new?ProxyClassFactory())?的实例????//?最终会调用ProxyClassFactory的apply方法。????//?在ProxyClassFactory的apply方法中有?ProxyGenerator.generateProxyClass()?????//?答案就在其中,最后调用的是ProxyGenerator的generateClassFile方法????//?中产生代理类时,让代理类继承Proxy类。????return?proxyClassCache.get(loader,?interfaces);}

所以破案了,在Proxy里的InvocationHandler是protected,所以我们取变量应当这么取:

@Intercepts可以填多个@Signature,@Signature是方法签名,type用于定位类,method定位方法名,args用于指定方法的参数类型。三者加在一起就可以定位到具体的方法。注意写完还需要将此插件注册到MyBatis的配置文件中,让MyBatis加载该插件。0

最后输出如下:

但是这个插件还不是那么完美,就是这个慢SQL查询时间了,我们现在是写死的

这两个问题在MyBatis 里面都可以得到解决,我们可以看Interceptor这个接口:

@Intercepts可以填多个@Signature,@Signature是方法签名,type用于定位类,method定位方法名,args用于指定方法的参数类型。三者加在一起就可以定位到具体的方法。注意写完还需要将此插件注册到MyBatis的配置文件中,让MyBatis加载该插件。1

setProperties用于从配置文件中取值, plugin将当前插件加入,intercept是真正增强方法。那上面的两个问题已经被解决了:

硬编码

首先在配置文件里面配置

@Intercepts可以填多个@Signature,@Signature是方法签名,type用于定位类,method定位方法名,args用于指定方法的参数类型。三者加在一起就可以定位到具体的方法。注意写完还需要将此插件注册到MyBatis的配置文件中,让MyBatis加载该插件。2

然后重写:

@Intercepts可以填多个@Signature,@Signature是方法签名,type用于定位类,method定位方法名,args用于指定方法的参数类型。三者加在一起就可以定位到具体的方法。注意写完还需要将此插件注册到MyBatis的配置文件中,让MyBatis加载该插件。3

回忆一下JDBC我们执行SQl事实上有两种方式:

Connection中的prepareStatement方法

Connection中的createStatement

在MyBatis中这两种方法对应不同的StatementType, 上面的PreparedStatementLogger对应 Connection中的prepareStatement方法, 如果说你在MyBatis中将语句声明为Statement,则我们的SQL监控语句就会出错,所以这里我们还需要在单独适配一下Statement语句类型。

@Intercepts可以填多个@Signature,@Signature是方法签名,type用于定位类,method定位方法名,args用于指定方法的参数类型。三者加在一起就可以定位到具体的方法。注意写完还需要将此插件注册到MyBatis的配置文件中,让MyBatis加载该插件。4

事实上MyBatis里面写好了反射工具类,这个就是SystemMetaObject,用法示例如下:

@Intercepts可以填多个@Signature,@Signature是方法
声明声明:本网页内容为用户发布,旨在传播知识,不代表本网认同其观点,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。E-MAIL:11247931@qq.com
ef英语哪个好 EF英孚英语培训怎么样? 英孚英语好不好 EF英孚教育到底好不好 大佬们,麦芒7和荣耀10那个值得入手?2500以下的机子还有啥好推荐的么... 介绍几款2500元以前的手机 像素一定要高 其他的不做要求 近期想入手一部安卓手机,价格2200到2500左右…买HTC desire Z还是 三星... 笔记本忘记开机密码怎么办急死了 笔记本电脑屏幕开机锁忘记密码 怎么办?急死了 华硕笔记本电脑开机密码忘记了怎样找回?系统是Windows 7旗舰版... 薏仁红豆一起吃的功效与作用 在excel表格里,怎么提取某一单元格中的位置不固定想要的文字... 中考模拟有多重要? 中考一模二模有什么作用吗? 如何解决打印机提示另存为的问题? win7c盘users占用很大空间怎么办 C盘占用空间过高怎么办? 为什么我的电脑C盘空间会那么大? 人怎么活才不枉费世上走一回 “万诗枉费一生工”的出处是哪里 把时间用在值得的人和事上,才不枉费一生 !!! 深圳北站做了核酸没出结果可以进站吗? 永川哪里有边油卖,就是猪油的一种 街机三国4399 怎样关闭qq登录的手机验证 养父如何称呼养子的亲生方的爷爷? 日照日百有哪些 日照百货大楼属于哪个社区 lol红框锁 lol屏幕红框怎么去掉 推行的英语是什么 求翻译,handbook of teacher-education 微信扫码的记录在哪里查 华为手机右上角绿灯一直闪怎么办? 八个小时能补完寒假作业吗? 寒假作业写不完怎么办?我们老师肯定会检查!除了装病什么的请假补作业还... 微信怎么清除其他账号留下的数据 应届毕业生。中部省会城市电业局(应该是正式编制)和江苏镇江 镇政府公 ... 信阳市电业局怎么进?我是国电的一名员工,31岁,7年工龄,研究生能进吗... 写植物作文要打比方,列数字,分类别的句子 S24有红外线遥控吗? 雷凌1.2t发动机有个螺丝掉了,起什么作用 昆明厨师培训考证 QQ昵称改成非主流文字的形式 帮我把我的qq昵称 星星&amp;知我心 设计成非主流文字的形式!谢了!_百度... 2024年7月,夜视效果好的家用监控摄像头推荐 如何把网页上的内容变成word文件如何把网页上的内容变成word文件夹 设计专业薄弱我该如何是好 小户型局部装修设计该怎么做好? word文档打开就关闭?