说到拦截器,举个通俗的例子就能理解了:
如果一个人要买票,可以直接自己买票,也可以通过中间代理买票,此时,可以把中间代理比做成一个拦截器,中间代理接受了顾客的委托,然后到火车站买票,买完票要将火车票交给顾客,由顾客拿着火车票去乘车,这是一个常规的流程。
在上述例子中,顾客自己买票属于Mybatis的正常流程;顾客请中间代理买火车票则属于利用拦截器在Mabatis中添加了自己的行为,但是同样达到了目的。
通过上述的例子,我们可以知道,一个拦截器的基本特点,或要做的事情:
--------------------------------------------------------
1. 要实现拦截
2. 拦截下来做什么事情
3. 事情完成后要交回主权。
---------------------------------------------------------
为什么使用要用拦截器,使用拦截器有什么作用呢?
在项目中,比如,各种不同类型的信息展示都需要分页功能等,如果对每类信息的展示都加入分页机制的代码,这样代码的重复率就会很高,功能的模块化下降,耦合性变高,所以,我们把这部分共同使用的功能用拦截器包装起来实现,然后规定此类功能的拦截器拦截哪些需要拦截的对象,那么,在执行这些对象的操作的时候,就可以自动实现拦截,加载此部分拦截内容执行,然后继续执行拦截的行为。当然,不局限于信息列表的分页功能,还有其他的公用功能也可以将其抽离出来,放在拦截器里面实现。
然后我们要知道拦截器拦截什么样的对象,拦截对象的什么行为,什么时候拦截?
在Mybatis框架中,已经给我们提供了拦截器接口,防止我们改动源码来添加行为实现拦截。说到拦截器,不得不提一下,拦截器是通过动态代理对Mybatis加入一些自己的行为。
拦截对象
确立拦截对象范围:要拦对人,既要保证拦对人,又要保证对正确的人执行正确的拦截动作,不要乱掏别人东西,拿需要的正确的东西。
拦截地点
在Mybatis的源码中
拦截时机
如果mybatis为我们提供了拦截的功能,我们应该在Mybatis执行过程的哪一步拦截更加合适呢?
拦截某个对象干某件事的是偶,拦截的时机要对,不能过早的拦截,也不能拦截太晚,过早的拦截会耽误别人做自己的工作,拦截太晚会错失良机。
Mybatis实际也是一个JDBC执行的过程,只不过被包装起来了而已,我们要拦截Mybatis的执行,最迟也要在获取PreparedStatement时拦截:
PreparedStatementstatement=conn.prepareStatement(sql.toString());
在此处偷偷的将sql语句换掉,就可以改变mybatis的执行,加入自己想要的执行行为。
而获取Mybatis的Statement是在StatementHandler中进行的。
举个栗子来说明:
一个分页拦截器的例子:
**************拦截器定义代码******************
@Intercepts(
{@Signature(type=StatementHandler.class,method="prepare",args={Connection.class,Integer.class })}
)
public class PageInterceptor implements Interceptor{
private String test;
public Object intercept(Invocation invocation) throws Throwable {
StatementHandlerstatementHandler =(StatementHandler)invocation.getTarget();
MetaObjectmetaObject = MetaObject.forObject(statementHandler,
SystemMetaObject.DEFAULT_OBJECT_FACTORY,
SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,new DefaultReflectorFactory());
//拦截的是RoutingStatemHandler,(RoutingStatemHandler下有一个BaseStatementHandler接口)
//delegate是第一层属性的属性名,mappedStatement:第二层属性的属性名
MappedStatementmappedStatement =(MappedStatement)metaObject.getValue("delegate.mappedStatement");
Stringid=mappedStatement.getId();
//以ByPage结尾的做拦截。$是字符串结束符
if(id.matches(".+ByPage$"))
{
BoundSqlboundSql=statementHandler.getBoundSql();
//原始的Sql语句
Stringsql=boundSql.getSql();
//查询总条数的Sql语句
StringcountSql = "select count(*) from (" + sql + ")a";
Connectionconnection = (Connection)invocation.getArgs()[0];
PreparedStatementcountStatement = connection.prepareStatement(countSql);
ParameterHandlerparameterHandler =(ParameterHandler)metaObject.getValue("delegate.parameterHandler");
parameterHandler.setParameters(countStatement);
ResultSetrs = countStatement.executeQuery();
//将泛型修改成?,则不会出现警告了。
Map,?> parameter = (Map,?>)boundSql.getParameterObject();
Pagepage = (Page)parameter.get("page");
if(rs.next()) {
page.setTotalNumber(rs.getInt(1));
}
//改造后的Sql语句
//delete.boundSql.sql是三层关系:RoutingStatemHandler下的delegate
StringpageSql = sql + " limit " + page.getDbIndex() + "," + page.getDbNumber();
metaObject.setValue("delegate.boundSql.sql", pageSql);
}
//通过源码下的一个反射来调用被拦截的方法,让原本被拦截的方法继续执行
return invocation.proceed();
}
//target被代理人
/*
* 达成协议,把具有代理权的要给普通的业务员this传进wrap实现的源码去做代理。
*
*/
public Object plugin(Object target) {
System.out.println(this.test);
return Plugin.wrap(target, this); }
public void setProperties(Properties proterties) {
this.test=proterties.getProperty("test");
}
}
**************拦截器配置******************
configuration >
定义一个拦截器,首先要在Mybatis中声明这是一个拦截器,要拦截的地点,在什么时候开始拦截,然后要在拦截器中做一些什么工作(属性设置,拦截过滤,拦截执行),拦截完让被拦截的对象继续执行原来要执行的动作。
先看拦截器声明:
@Intercepts(
{
@Signature(type=StatementHandler.class,method="prepare",args={Connection.class,Integer.class })}
)
通过定义一个拦截器Interceptor的注解注解声明这是一个拦截器,用signature签名指向要拦截StatementHandler这个接口类下的prepare方法,新版本的Mybatis需要在该方法中带入两个参数:Connnection和Integer两个类型。
在源码中,Parpare方法是通过BaseStatementHandler(Implementation实现)下的instantiateStatement方法获取,instantiateStatement是一个虚拟方法,是通过PreparedStatementHandler(Implementation)类来实现,该类下的instantiateStatement方法实现了Statement的获取。
Statement获取的实现语句是:return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
上述一个注解声明即完成了注解的声明(@Intercepts)、拦截时机(获取Statement之前),拦截对象(对Statement进行拦截,或者说是对里面的sql语句进行拦截)、拦截的方法(prepare),需要带入的参数(Connection和Integer两个参数)。
再说拦截器:
public class PageInterceptor implements Interceptor{
private String test;
public Object intercept(Invocation invocation) throws Throwable {
StatementHandlerstatementHandler =(StatementHandler)invocation.getTarget();
MetaObjectmetaObject = MetaObject.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY, SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,new DefaultReflectorFactory());
//delegate是第一层属性的属性名,mappedStatement:第二层属性的属性名
MappedStatementmappedStatement =(MappedStatement)metaObject.getValue("delegate.mappedStatement");
Stringid=mappedStatement.getId();
//以ByPage结尾的做拦截。$是字符串结束符
if(id.matches(".+ByPage$"))
{
BoundSqlboundSql=statementHandler.getBoundSql();
//原始的Sql语句
Stringsql=boundSql.getSql();
//查询总条数的Sql语句
StringcountSql = "select count(*) from (" + sql + ")a";
Connectionconnection = (Connection)invocation.getArgs()[0];
PreparedStatementcountStatement = connection.prepareStatement(countSql);
ParameterHandlerparameterHandler =(ParameterHandler)metaObject.getValue("delegate.parameterHandler");
parameterHandler.setParameters(countStatement);
ResultSetrs = countStatement.executeQuery();
//将泛型修改成?,则不会出现警告了。
Map,?> parameter = (Map,?>)boundSql.getParameterObject();
Pagepage = (Page)parameter.get("page");
if(rs.next()) {
page.setTotalNumber(rs.getInt(1));
}
//改造后的Sql语句
//delete.boundSql.sql是三层关系:RoutingStatemHandler下的delegate
StringpageSql = sql + " limit " + page.getDbIndex() + "," + page.getDbNumber();
metaObject.setValue("delegate.boundSql.sql", pageSql);
}
//通过源码下的一个反射来调用被拦截的方法,让原本被拦截的方法继续执行
return invocation.proceed();
}
public Object plugin(Object target) {
System.out.println(this.test);
return Plugin.wrap(target, this); }
public void setProperties(Properties proterties) {
this.test=proterties.getProperty("test");
}
}
拦截器通过实现Mybatis提供的Interceptor拦截接口,重写了三个方法:setProperties/plugin/ intercept,三者执行顺序是setProperties—》plugin—》Interceptor。
setProperties方法:该方法通过设置属性,将核心配置文件configuration.xml文件中对拦截器的配置项下的属性获取过来,便于在拦截器中使用。
plugin方法:该方法用来协商,达成协议,把代理权给普通的业务员this,传进wrap方法实现的源码去做代理,没有获取代理权的代理人在这个地方就会停下,不会向下走了,获取代理权的代理人可以去做拦截代理。
intercept方法:则是获取拦截对象下的要拦截的东西,然后对其加以改编,添加自己的行为,按照条件进行改编拦截对象,然后通过源码下的反射invocation来调用被拦截的方法,让原本被拦截的方法继续执行(invocation.proceed())。
Dao.xml文件:
ID,COMMAND,DESCRIPTION,CONTENT
其中,parameterType="java.util.Map"是参数类型的权限命名(包名.类名),很容易通过该名称反射出类名(Class.forName());当Mybatis拿到这个类名,后面就很容易操作了。如果是数字类型就比较麻烦了,如何填写属性值表示数组类型呢?
即小问题:Mybatis是如何加载数组的Class的。