前言
本篇咱们来介绍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在回调的时候做了判断:
@Override
public 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(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
Statement statement = (Statement)invocation.getArgs()[0];
if (Proxy.isProxyClass(statement.getClass())){
Class<?> statementClass = statement.getClass().getSuperclass();
Field targetField = statementClass.getDeclaredField("h");
targetField.setAccessible(true);
PreparedStatementLogger loggerStatement = (PreparedStatementLogger) targetField.get(statement);
PreparedStatement preparedStatement = loggerStatement.getPreparedStatement();
if (endTime - startTime > 1){
System.out.println(preparedStatement.toString());
}
}else {
if (endTime - startTime > 1){
System.out.println(statement.toString());
}
}
return result;
}
}
最初输入如下:
然而这个插件还不是那么完满,就是这个慢SQL查问工夫了,咱们当初是写死的
这两个问题在MyBatis 外面都能够失去解决,咱们能够看Interceptor这个接口:
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
default void setProperties(Properties properties) {
// NOP
}
}
setProperties用于从配置文件中取值, plugin将以后插件退出,intercept是真正加强办法。那下面的两个问题曾经被解决了:
- 硬编码
首先在配置文件外面配置
<plugins>
<plugin interceptor="org.example.mybatis.MyBatisSlowSqlPlugin">
<property name = "maxTolerate" value = "10"/>
</plugin>
</plugins>
而后重写:
@Override
public void setProperties(Properties properties) {
//maxTolerate 是MyBatisSlowSqlPlugin的成员变量
this.maxTolerate = Long.parseLong(properties.getProperty("maxTolerate"));
}
回顾一下JDBC咱们执行SQl事实上有两种形式:
- Connection中的prepareStatement办法
- Connection中的createStatement
在MyBatis中这两种办法对应不同的StatementType, 下面的PreparedStatementLogger对应 Connection中的prepareStatement办法, 如果说你在MyBatis中将语句申明为Statement,则咱们的SQL监控语句就会出错,所以这里咱们还须要在独自适配一下Statement语句类型。
@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 {
private long maxTolerate;
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("-----开始进入性能剖析插件中----");
long startTime = System.currentTimeMillis();
Object result = invocation.proceed();
SystemMetaObject
long endTime = System.currentTimeMillis();
// query办法入参是statement,所以咱们能够将其转为Statement
Statement statement = (Statement)invocation.getArgs()[0];
if (Proxy.isProxyClass(statement.getClass())){
Class<?> statementClass = statement.getClass().getSuperclass();
Field targetField = statementClass.getDeclaredField("h");
targetField.setAccessible(true);
Object object = targetField.get(statement);
if (object instanceof PreparedStatementLogger) {
PreparedStatementLogger loggerStatement = (PreparedStatementLogger) targetField.get(statement);
PreparedStatement preparedStatement = loggerStatement.getPreparedStatement();
if (endTime - startTime > maxTolerate){
System.out.println(preparedStatement.toString());
}
}else {
// target 是对应的语句处理器
// 为什么不反射拿? Statement 对应的实现类未重写toString办法
// 然而在RoutingStatementHandler 中提供了getBoundSql办法
RoutingStatementHandler handler = (RoutingStatementHandler) invocation.getTarget();
BoundSql boundSql = handler.getBoundSql();
if (endTime - startTime > maxTolerate){
System.out.println(boundSql);
}
}
}else {
if (endTime - startTime > maxTolerate){
System.out.println(statement.toString());
}
}
return result;
}
@Override
public void setProperties(Properties properties) {
this.maxTolerate = Long.parseLong(properties.getProperty("maxTolerate"));
}
}
事实上MyBatis外面写好了反射工具类,这个就是SystemMetaObject,用法示例如下:
@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 {
private long maxTolerate;
@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
Statement statement = (Statement)invocation.getArgs()[0];
MetaObject metaObject = SystemMetaObject.forObject(statement);
if (Proxy.isProxyClass(statement.getClass())){
Object object = metaObject.getValue("h");
if (object instanceof PreparedStatementLogger) {
PreparedStatementLogger loggerStatement = (PreparedStatementLogger) object;
PreparedStatement preparedStatement = loggerStatement.getPreparedStatement();
if (endTime - startTime > maxTolerate){
System.out.println(preparedStatement.toString());
}
}else {
// target 是对应的语句处理器
// 为什么不反射拿? Statement 对应的实现类未重写toString办法
// 然而在RoutingStatementHandler 中提供了getBoundSql办法
RoutingStatementHandler handler = (RoutingStatementHandler) invocation.getTarget();
BoundSql boundSql = handler.getBoundSql();
if (endTime - startTime > maxTolerate){
System.out.println(boundSql);
}
}
}else {
if (endTime - startTime > maxTolerate){
System.out.println(statement.toString());
}
}
return result;
}
@Override
public void setProperties(Properties properties) {
this.maxTolerate = Long.parseLong(properties.getProperty("maxTolerate"));
}
}
那我有多个插件,如何指定程序呢? 在配置文件中指定,从上往下顺次执行
<plugins>
<plugin interceptor="org.example.mybatis.MyBatisSlowSqlPlugin01">
<property name = "maxTolerate" value = "10"/>
</plugin>
<plugin interceptor="org.example.mybatis.MyBatisSlowSqlPlugin02">
<property name = "maxTolerate" value = "10"/>
</plugin>
</plugins>
如下面所配置执行程序就是MyBatisSlowSqlPlugin01、MyBatisSlowSqlPlugin02。 插件的几个办法执行程序呢
写在最初
感叹颇深,本来预计两个小时就能写完的,而后写了一下午,颇有种学海无涯的感觉。
参考资料
- mybatis拦截器插件实例-获取不带占位符的可间接执行的sql B站教学视频 https://www.bilibili.com/vide…
- MyBatis高级 https://www.bilibili.com/vide…
发表回复