通过本篇的介绍,你会了解到:
1、插件开发前的准备
2、插件的接口和初始化
3、插件的设计与实现
4、插件的开发实例
5、总结
插件开发前,我们需要知道签名,插件接口,插件如何初始化、插件代理和反射、分离拦截对象常用工具等。
插件开发前,需要确定我们拦截的签名,而签名的确需要以下的两个因素
Mybatis的Configuration对象,存储了mybatis的配置信息,在内部多个地方都可以看到Configuration的影子,这是一个非常重要的对象,在追踪源码的时候可以看到Mybatis插件生效的地方。
通过Configration对象,我们看出可以拦截的对象有
Executor:调度以下三个对象并且执行SQL全过程,组装参数,执行SQL,组装结果集并返回,通常不怎么拦截使用.
StatementHandler:是执行SQL的过程(预处理语句构成),这里我们可以获得SQL,重写SQL执行。所以这是最常被拦截的对象。
ParameterHandler:参数组装,可以拦截参数重组参数。
ResultSetHandler:结果集处理,可以重写组装结果集返回。
Mybatis的这四大对象的详解,可参考文档:
https://www.jianshu.com/p/39270777d3f6
,此处不做详细介绍。
确定了拦截对象之后,需要确定拦截对象中的方法和参数,比如我们拦截的是StatementHandler对象的关键预处理prepare(Connection connection, Integer transactionTimeOut)方法。
因此,我们可以定义这样的签名:
//拦截StatementHandler对象的prepare预处理方法,同时指定该该方法的Connection参数
@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
其中
@Inttercepts :该注解说明是一个拦截器
@Signature是注册拦截器签名的地方,只有满足签名条件才能拦截,type是四大对象中的一个。Method是指拦截的方法,args表示该方法参数。
MetaObject是MyBatis给我们提供的工具类,它可以有效的获取或修改一些重要对象的属性。
在Mybatis中,四大对象给我们提供的public设置参数的方法很少,我们难以通过其自身得到相关的属性信息,但是有了MetaObject这个工具类,我们就可以通过其他的技术手段来读取或者修改这些重要对象的属性。
Mybatis对象中大量使用这个类进行包装,包括四大对象,使得我们可以通过它来给四大对象的某些属性赋值从而满足需求。
例如,我们拦截StatementHandler对象,首先要获取它要执行的SQL,添加返回行数限制。
取出被拦截的对象
StatementHandler statementHandler = (StatementHandler) PluginUtils.realTarget(invocation.getTarget());
MetaObject metaStatementHandler = SystemMetaObject.forObject(statementHandler);
//取出即将执行的SQL
String sql = (String) metaStatementHandler.getValue("delegate.boundSql.sql");
我们拿到sql,就可以加上limit行数,然后重新赋值。
在MyBatis中使用插件,插件开发的第一步是必须实现Interceptor接口,定义如下:
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
Object plugin(Object target);
void setProperties(Properties properties);
}
详细说说这3个方法:
插件的初始化时在MyBatis初始化的时候完成的,读入插件节点和配置的参数,使用反射技术生成插件实例,然后调用插件方法中的setProperties方法设置参数,并将插件实例保存到配置对象中,具体过程看下面代码。
plugin配置如下:
入口在于xml中配置的mybatisplus的
MybatisSqlSessionFactoryBean,
sqlSessionFactory工厂,在构建sqlSessionFactory, buildSqlSessionFactory()方法中通过配置对象Configuration的添加插件方法,
其中的配置对象:
public class Configuration {
protected final InterceptorChain interceptorChain = new InterceptorChain();
public void addInterceptor(Interceptor interceptor) {
interceptorChain.addInterceptor(interceptor);
}}
InterceptorChain是一个类,主要包含一个List属性,保存Interceptor对象:
MyBatis 通过提供插件机制,让我们可以根据自己的需要去增强 MyBatis 的功能。
需要注意的是,如果没有完全理解 MyBatis 的运行原理和插件的工作方式,最好不要使用插件,因为它会改变系底层的工作逻辑,给系统带来很大的影响。
MyBatis 的插件可以在不修改原来的代码的情况下,通过拦截的方式,改变四大核心对象的行为,比如处理参数,处理 SQL,处理结果。
它内部运用到两个设计模式,代理模式和责任链模式
代理模式:
比如它可以在不修改对象的代码的情况下,对对象的行为进行修改,比如说在原来的方法前面做一点事情,在原来的方法后面做一点事情
责任链模式:
插件用的是责任链模式,责任链模式是一种对象行为模式。在责任链模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链,请求在这个链上传递,直到链上的某一个对象决定处理此请求。
插件的调用流程
Executor 是 openSession() 的 时 候 创 建 的 ;
StatementHandler 是SimpleExecutor.doQuery()创建的;
里面包含了处理参数的 ParameterHandler 和处理 结果集的 ResultSetHandler 的创建,
创建之后即调用 InterceptorChain.pluginAll(),返回层层代理后的对象。
大致流程图如下:
可以被代理,顺序如下图:
Plugin类,在重写的plugin() 方法里面可以直接调用return Plugin.wrap(target, this);它会根据插件的签名配置,使用JDK动态代理的方法,生成并返回一个代理对象。
因为代理类是 Plugin,所以最后调用的是 Plugin 的invoke(Object proxy, Method method, Object[] args)方法。
此方法中,它先调用了定义的拦截器的 intercept(Invocation)方法。另外,Invocation对象包含一个proceed方法,这个方法就是调用被代理对象的真实方法,如果有n个插件,第一个传递的参数是四大对象本身,然后调用一次wrap方法产生第一个代理对象,这里的反射就是四大对象的真实方法,如果有第二个插件,这里的反射就是第一个代理对象的invoke方法。
所以,在多个插件的情况下,调度proceed方法,MyBatis总是从最后一个代理对象运行到第一个代理对象,最后是真实被拦截的对象方法被执行。
实际开发过程中,我可能需要限制每次SQL返回的数据行数,限制的行数需要是一个可配置的参数,也去可以配置自己的需要配置,查询时,返回我配置限制返回的条数。
限制返回条数肯定是先要拦截StatementHandler对象,在预编译SQL之前,修改SQL返回数量。
Mapper中原始的SQL
SELECT * FROM table
我们最后需要的SQL,也就是插件最后执行的SQL
SELECT * FROM (SELECT * FROM table) limit_Table_Name_Alias limit 3
拦截预编译,自然是要拦截StatementHandler的prepare()方法,prepare()方法传入参数Connection对象和超时参数Integer类型。
@Intercepts({@Signature()})
指定拦截的对象和方法、方法参数 方法名称+参数类型,构成了方法的签名,决定了能够拦截到哪个方法,这样才能唯一确定需要拦截的是哪个对象及对象中的哪个方法。
@
intercept:它将直接覆盖你所拦截的对象,有个参数Invocation对象,通过该对象,可以反射调度原来对象的方法;
plugin:target是被拦截的对象,它的作用是给被拦截对象生成一个代理对象;
setProperties:允许在plugin元素中配置所需参数,该方法在插件初始化的时候会被调用一次。
/**
* 限制查询返回行数
*
* @author Mr.Xu
* @date 2019/8/19 15:02
*/
@Intercepts({//注意看这个大花括号,也就这说这里可以定义多个@Signature对多个地方拦截,都用这个拦截器
@Signature(type =StatementHandler.class,//这是指拦截哪个接口
method="prepare" ,//这个接口内的哪个方法名,不要拼错了
args={Connection.class, Integer.class})})//这是拦截的方法的入参,按顺序写到这,不要多也不要少,如果方法重载,可是要通过方法名和入参来确定唯一的
public class QueryLimitPlugin implements Interceptor{
@Setter
@Getter
@Accessors(chain = true)
private int limit;
@Setter
@Getter
@Accessors(chain = true)
private String dbType;
private static final String LIMIT_TABLE_ALIAS ="limit_Table_Name_Alias";
/**
* 用于覆盖被拦截对象的原有方法(在调用代理对象 Plugin 的 invoke()方法时被调用)
* @param invocation
* @return
* @throws Throwable
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 取出被拦截的对象
StatementHandler statementHandler = (StatementHandler) PluginUtils.realTarget(invocation.getTarget());
MetaObject metaStatementHandler = SystemMetaObject.forObject(statementHandler);
//取出即将执行的SQL
String sql = (String) metaStatementHandler.getValue("delegate.boundSql.sql");
// 判断参数是不是Mysql数据库且SQL有没有被插件重写过
if ("mysql".equals(this.dbType) && sql.indexOf(LIMIT_TABLE_ALIAS) == -1) {
sql = sql.trim();
//添加limit条件
sql = "select * from (" + sql + ") " + LIMIT_TABLE_ALIAS + " limit 3";
//重新设置sql
metaStatementHandler.setValue("delegate.boundSql.sql", sql);
}
// 调用原来对象的方法,进入责任链的下一层级
return invocation.proceed();
}
/**
* target 是被拦截对象,这个方法的作用是给被拦截对象生成一个代理对象,并返回它
* @param target 被拦截对象
* @return
*/
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
/**
* 设置参数
* @param props
*/
@Override
public void setProperties(Properties props) {
String strLiit = (String) props.getProperty("limit", "50");
this.limit = Integer.parseInt(strLiit);
this.dbType = (String) props.getProperty("dbtype", "mysql");
}
}
在 applicationContext-mybatis.xml 中配置自定义的插件
配置好后,启动项目,调用对应的查询接口
查看日志打印结果:
所用到的4个对象
参考文献:
https://www.jianshu.com/p/d55de0ee3f68
https://blog.51cto.com/13714880/2112522