引言
插件——用来解决特定功能需求的配置拦截器的一种方式,起到相当于插件的作用。
概述
插件定义:在sqlSession四大对象调度的过程中,插入自定义代码执行特殊的功能满足特殊的需求。
内容
1 接口:mybatis使用插件,必须实现接口Interceptor
public interface Interceptor{
Object intercept(Invocation invocation) throws Throwable;
Object plugin(Object target);
void setProperties(Properties properties);
}
说明,方法定义
1)intercept方法:直接覆盖拦截对象的原有方法,是插件的核心方法;参数Invocation对象,通过它反射调用原来对象的方法。
2)plugin方法:target是被拦截对象,它的作用是给被拦截对象生成一个代理对象,并返回它
3)setProperties方法:为plugin元素配置属性参数,插件初始化时调用,将插件对象存入到配置中,便于后面使用。
2 初始化:MyBatis初始化开始就会对插件进行初始化
private void pluginElement(XNode parent) throw Exception{
if(parent != null){
for(XNode child : parent.getChildren()){
String interceptor = child.getStringAttribute("interceptor");
Properties properties = child.getChildrenAsProperties();
//通过反射生成插件实例
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
//设置配置参数
interceptorInstance.setProperties(properties);
//将插件实例保存到配置对象中
configuration.addInterceptor(interceptorInstance);
}
}
}
3 保存:插件保存在Configuration对象中
public void addInterceptor(Interceptor interceptor){
interceptorChain.addInterceptor(interceptor);
}
interceptorChain是Configuration的属性对象,包含一个addInterceptor方法,初始化之后插件就保存在List对象interceptors里面,使用时取出来即可。
private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
......
public void addInterceptor(Interceptor interceptor){
interceptors.add(interceptor);
}
4 常用工具类(MetaObject)
1)MetaObject forObject(Object object、ObjectFactory objectFactory、ObjectWrapperFactory objectWrapperFactory)方法用于包装对象。
本方法不再使用,通过MyBatis为我们提供的SystemMetaObject.forObject(Object obj)。
2)Object getValue(String name)方法用于获取对象属性值,支持OGNL
3)void setValue(String name、Object value)方法用于修改对象属性值,支持OGNL
5 插件开发过程
(1)确定需要拦截的签名:运行插件需要注册签名
1)根据功能确定需要拦截的对象
Executor:执行SQL的全过程,包括组装参数,组装结果集返回和执行SQL过程,都可以拦截;
StatementHandler:执行SQL的过程,可以重写SQL执行过程,常用的拦截对象;
ParameterHandler:主要拦截执行SQL的参数组装,可以重写组装参数规则;
ResultSetHandler:用于拦截执行结果的组装,也可以重写组装结果的规则;
需要拦截的是StatementHandler对象,在预编译SQL之前,修改SQL使得结果返回数量被限制。
2)拦截方法和参数:查询的过程是通过Executor调度StatementHandler来完成的,StatementHandler的prepare方法预编译SQL,于是我们需要拦截的方法便是prepare方法,在这之前重写SQL语句。
StatementHandler接口的定义:
public interface StatementHandler{
Statement prepare(Connection connection) throws SQLException;
void parameterize(Statement statement) throws SQLException;
void batch(Statement statement) throws SQLException;
int update(Statement statement) throws SQLException;
<E> List<E> query(Statement statement,ResultHandler resultHandler) throws SQLException;
BoundSql getBoundSql();
ParameterHandler getParameterHandler();
}
以上的任何方法都可以拦截,从接口定义而言,prepare方法有一个参数Connection对象,那么如何设计一个拦截器?
@Intercepts({ @Signature(type = StatementHandler.class,method = "prepare" , args={Connection.class}) })
public class MyPlugin implements Intercept{ ........ }
其中,@Intercepts说明它是一个拦截器。@Signature用来注册拦截器签名,只要签名满足条件才能拦截,type可以是四大对象中的一个,这里是StatementHandler。method代表要拦截四大对象的某一种接口方法, 而args则表示该方法的参数,需要根据拦截对象的方法参数进行设置。
(2)实现拦截方法:实现了简单的打印顺序功能
@Intercepts({@Signature(
type = Executor.class, //确定要拦截的对象
method="update", //确定要拦截的方法
args = {MappedStatement.class,Object.class} //拦截方法的参数
)})
public class MyPlugin implements Intercept{
Properties props = null;
/**
* 代替拦截对象方法的内容
* @param invocation 责任链对象
*/
@Override
public Object intercept(Invocation invocation) throws Throwable{
System.err.println("before....");
//如果当前代理的是一个非代理对象,那么它就会调用真实拦截对象的方法,如果不是它会调用下个插件代理对象的invoke方法
Object obj = invocation.proceed();
System.err.println("after......");
return obj;
}
/**
* 生成对象的代理,这里常用MyBatis提供的Plugin类的wrap方法
* @param target 被代理的对象
*/
@Override
public Object plugin(Object target){
//使用MyBatis提供的Plugin类生成代理对象
System.err.println("使用生成代理对象....");
return Plugin.wrap(target,this);
}
/**
* 获取插件配置的属性,我们在MyBatis的配置文件里面去配置
* @param props 是MyBatis配置的参数
*/
public void setProperties(Properties props){
System.err.println(props.get("dbType"));
this.props = props;
}
}
(3)配置插件
<plugins>
<plugin interceptor = "xxx.MyPlugin">
<property name="dbType" value="mysql" />
</plugin>
</plugins>
(4)插件实例
1)实例场景:大型的互联网系统,假如我们数据库使用的MySQL数据库,想要对数据库查询返回数据量需要限制,以避免数据量过大造成网站瓶颈,配置限制50条数据。
2)实现步骤:首先我们先确定需要拦截四大对象中的哪一个,根据功能需要修改SQL的执行。由SqlSession运行原理表明拦截对象是StatementHandler,因为它的prepare方法用来编译SQL语句,我们可以在预编译前修改语句来满足我们的需求。通过StatementHandler的prepare()方法,在它预编译前,需要重写SQL,达到限制数据量的要求,它有一个参数(Connection connection),我们就轻易地得到了签名注解。
3)代码实现:
@Intercepts({@Signature(
type = Executor.class, //确定要拦截的对象
method="update", //确定要拦截的方法
args = {MappedStatement.class,Object.class} //拦截方法的参数
)})
public class MyPlugin implements Intercept{
Properties props = null;
/**
* 代替拦截对象方法的内容
* @param invocation 责任链对象
*/
@Override
public Object intercept(Invocation invocation) throws Throwable{
System.err.println("before....");
//如果当前代理的是一个非代理对象,那么它就会调用真实拦截对象的方法,如果不是它会调用下个插件代理对象的invoke方法
Object obj = invocation.proceed();
System.err.println("after......");
return obj;
}
/**
* 生成对象的代理,这里常用MyBatis提供的Plugin类的wrap方法
* @param target 被代理的对象
*/
@Override
public Object plugin(Object target){
//使用MyBatis提供的Plugin类生成代理对象
System.err.println("使用生成代理对象....");
return Plugin.wrap(target,this);
}
/**
* 获取插件配置的属性,我们在MyBatis的配置文件里面去配置
* @param props 是MyBatis配置的参数
*/
public void setProperties(Properties props){
System.err.println(props.get("dbType"));
this.props = props;
}
}
<plugins>
<plugin interceptor = "xxx.MyPlugin">
<property name="dbType" value="mysql" />
</plugin>
</plugins>
@Intercepts({ @Signature(type = StatementHandler.class, //确定要拦截的对象
method = "prepare", //确定要拦截的方法
args = { Connection.class}) //拦截方法的参数
})
public class QueryLimitPlugin implements Interceptor{
//默认限制查询返回行数
private int limit;
private String dbType;
//限制表中间别名,避免表重名起得比较特殊
private static final String LMT_TABLE_NAME = "limit_Table_Name_person";
@Override
public Object intercept(Invocation invocation) throws Throwable{
//取出被拦截对象
StatementHandler stmtHandler = (StatementHandler) invocation.getTarget();
MetaObject metaStmtHandler = SystemMetaObject.forObject(stmtHandler);
// 分离代理对象,从而形成多次代理,通过俩次循环最原始的被代理类,MyBatis使用的是JDK代理
while (metaStmtHandler.hasGetter("h")){
Object object = metaStmtHandler.getValue("h");
metaStmtHandler = SystemMetaObject.forObject(object);
}
// 分离最后一个代理对象的目标类
while (metaStmtHandler.hasGetter("target")){
Object object = metaStmtHandler.getValue("target");
metaStmtHandler = SystemMetaObject.forObject(object);
}
// 取出即将要执行的SQL
String sql = (String)metaStmtHandler.getValue("delegate.boundSql.sql");
String limitSql;
//判断参数是不是MySQL数据库且SQL有没有被插件重写过
if ("mysql".equals(this.dbType) && sql.indexOf(LMT_TABLE_NAME) == -1){
//去掉前后空格
sql = sql.trim();
//将参数写入SQL
limitSql = "select * from (" + sql +") " + LMT_TABLE_NAME + " limit " + limit;
//重写要执行的SQL
metaStmtHandler.setValue("delegate.boundSql.sql", limitSql);
}
//调用原来对象的方法,进入责任链的下一层级
return invocation.proceed();
}
@Override
public Object plugin(Object target){
//使用默认的MyBatis提供的类生成代理对象
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties props){
String strLimit = (String)props.getProperty("limit", "50");
this.limit = Integer.parseInt(strLimit);
//读取设置的数据库类型
this.dbType = (String) props.getProperty("dbType", "mysql");
}
}
说明:在setProperties方法中可以读入配置给插件的参数,“limit”是数据库的名称,“50”是限制记录数;
在MyBatis初始化的时候就已经被设置好,需要的时候直接使用即可;
在plugin方法里,使用MyBatis提供的类target来生成代理对象,插件进入plugin的invoke方法;
最后使用到拦截器的intercept方法;
插件QueryLimitPlugin的intercept方法就会覆盖掉StatementHandler的prepare方法,先从代理对象
分离出真实对象,然后根据需要修改SQL,来达到限制返回行数的目的;
再然后使用invocation.proceed()来调度真实StatementHandler的prepare方法完成SQL预编译;
最后需要在MyBatis配置文件里才能运行这个插件
<plugins>
<plugin interceptor="com.learn.chapter7.plugin.QueryLimitPlugin">
<property name="dbType" value="mysql"/>
<property name="limit" value="50"/>
</plugin>
</plugins>
总结
(1)插件修改MyBatis的底层设计,尽量少用插件;
(2)插件生成的原理是层层代理对象的责任链模式,通过反射方法运行,性能不高,减少插件就能减少代理,提高系统性能;
(3)编写插件需要了解MyBatis的运行原理,了解四大对象及其方法的作用,准备判断需要拦截什么对象,什么方法,参数是什么,才能确定如何编写签名;
(4)插件需要读取和修改MyBatis映射器中的对象属性
(5)多个插件层层代理,保证逻辑的正确性
(6)尽量少改动MyBatis底层,减少错误