Mybatis的拦截器原理还有点绕,也还算简单,原理就是通过JDK的动态代理技术来为我们自定义的拦截器类实现代理,并且这个代理可以有多个,所以Mybatis拦截器会成一个链条形式存在,一个处理完在一个。分页原理就是在拦截器中先拿到旧的SQL,然后拼接limit语句让Mybatis继续处理。
举个列子,你准备去一家工厂,但是门口有个大爷把你拦下,让你必须套个脚套才可以进入,于是你套上了脚套包裹了自己,没走几步,一大爷又让你带头套,于是你又套了个头套,这里的你就是个就sql语句,大爷就是拦截器,不断通过拦截器装饰自己(比如加limit语句),最终华丽的干你的事。
先分析源码后干事。
源码分析
一、 加载拦截器类
首先Mybatis需要知道我们配置了那些拦截器,在XMLConfigBuilder类下的pluginElement方法下读取配置文件,说叫拦截器,其实又叫插件。
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
Iterator var2 = parent.getChildren().iterator();
while(var2.hasNext()) {
XNode child = (XNode)var2.next();
String interceptor = child.getStringAttribute("interceptor");
Properties properties = child.getChildrenAsProperties();
Interceptor interceptorInstance = (Interceptor)this.resolveClass(interceptor).newInstance();
interceptorInstance.setProperties(properties);
this.configuration.addInterceptor(interceptorInstance);
}
}
}
Mybatis有自己的dtd,需要按照顺序配置plugins节点
加载后并且实例化完成,通过一系列调用最终放入到InterceptorChain类的List中。也就是说我们要实现一个拦截器,需要继承Interceptor接口。
二、设置代理
这是最重要的一步,在Configuration的newExecutor方法中,InterceptorChain下的pluginAll下完成链式的动态代理设置,
这里的pluginAll方法被调用时,target对象就是其他Executor的实现类,如果没有关闭缓存开关,他应该是CachingExecutor类的实例。之后每调用一次interceptor.plugin就为target生成一个代理对象,也就是从用自定义拦截器下的plugin方法中返回一个动态代理对象,一般使用
Plugin.wrap()
方法返回即可。如果拦截器有多个,他最终可能是这样的形式:代理对象(代理对象(代理对象(被代理的Executor))),就像爷爷让爸爸买零食,爸爸又让儿子代理去买零食,儿子又让其他跑腿的人去买零食,一层套一层,这个地方可能有点绕。
就像配置两个拦截器之后,他的结构是这样的,最里层是真正的儿子。
三、 处理自定义逻辑
继承Interceptor后,首先要指定要拦截那个对象(这里的对象可以是Executor、StatementHandler、PameterHandler和ResultSetHandler),通过注解方式告诉Mybatis。
如下面地例子,告诉Mybatis我要拦截Executor类下的query方法并且重载参数为{MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class的方法
@Intercepts({
@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
)
})
之后Mybatis调用其query时先转入到Interceptor的intercept方法下进行处理。
处理自定义逻辑在intercept方法下,而intercept的参数很重要,所以要先了解他,参数类型是Invocation,他在动态代理invoke时候被转入,但是还是得先看wrap这个方法,warp自己new了自己一下,target是被代理的对象,也就是Executor的实现类,但是代理有多个时,他可能是代理Executor实现类的代理对象,或者更多层套入,interceptor就是我们的拦截器。
在invoke下调用拦截器的的intercept方法时,new出一个Invocation对象传入,其中args比较重要,他就是调用被代理对象的方法调用时的参数,如Executor的query的方法,args[]就是这个方法的参数列表集合,后续会有大用。
四、分页
首先定义一个PageInfo,其中toString通过(页大小×页数)-页数的公式拼接limit语句。
如果从2页取10条最终得出(10*2)-10="limit 10,10"
public class PageInfo {
private int page;
private int pageCount;
public PageInfo(int page, int pageCount) {
this.page = page;
this.pageCount = pageCount;
}
@Override
public String toString() {
return" limit "+( (pageCount*page)-pageCount)+","+pageCount;
}
}
分页思路大致是这样的,首先拿到DAO层的查询参数,这里就包括PageInfo,接着拿到旧的sql,拼接limit,并把新的sql语句设置给Mybatis让他继续执行,实现拦截。
public interface IUserDao {
List select(@Param("page")PageInfo pageInfo);
}
1.拿到请求参数
要首先拿到上面的pageInfo参数,直接调用invocation.getArgs()[1]即可,这里的getArgs()数组就是Executor#query方法的第二个参数,即代表参数数组,但是有可能没有被封装成MapperMethod.ParamMap,所以要判断以下。
Mybatis在ParamNameResolver下的getNamedParams中做了判断,当参数上没有注解,并且参数个数为1时直接返回String类型。否则包装一下,这里无伤大雅,不管他。
ParamMap也是继承HashMap,首先判断有没有page分页这个条件,有的话先拿到分页的语句
String limitPage = "";
Object argObject = invocation.getArgs()[1];
if (argObject instanceof MapperMethod.ParamMap) {
MapperMethod.ParamMap arg = (MapperMethod.ParamMap) invocation.getArgs()[1];
if (arg.get("page") != null) {
PageInfo pageInfo = (PageInfo) arg.get("page");
limitPage = pageInfo.toString();
}
}
2.拿到旧的sql
MappedStatement对象即封装的insert、select、delete、update的信息,同样在Executor#query参数1即表示它。接着通过getBoundSql获取sql语句,完成旧sql和分页数据的拼接
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
Class> type = mappedStatement.getParameterMap().getType();
String oldSql = mappedStatement.getBoundSql(argObject).getSql();
String newSql = oldSql + limitPage;
3.给Mybatis设置新的sql
下面就是设置新的sql语句到MappedStatement,这里知识点就有了,MappedStatement中保存sql语句的是SqlSource对象,但是MappedStatement并没有提供修改他的方法。
所以有三个办法
1:通过反射
2:自己创建一个新的MappedStatement,并替换原来的。
3:SystemMetaObject
其中SystemMetaObject是Mybatis提供的非常强大的一个类,两行代码即可修改内部属性。
知道了办法,那就简单了
//构建新的sql对象
SqlSource sqlSource = new RawSqlSource(mappedStatement.getConfiguration(), newSql, type);
MetaObject metaObject = SystemMetaObject.forObject(mappedStatement);
//替换原来的
metaObject.setValue("sqlSource", sqlSource);
最后返回invocation.proceed();
继续让Mybatis处理,如果有其他的拦截器,则继续调用,拦截器结束后,这时候他查询是sql语句就变了。
分页所有代码如下
@Intercepts({
@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
)
})
public class MybatisIntercepts implements Interceptor {
private Properties properties = new Properties();
@Override
public Object intercept(Invocation invocation) throws Throwable {
String limitPage = "";
Object argObject = invocation.getArgs()[1];
if (argObject instanceof MapperMethod.ParamMap) {
MapperMethod.ParamMap arg = (MapperMethod.ParamMap) invocation.getArgs()[1];
if (arg.get("page") != null) {
PageInfo pageInfo = (PageInfo) arg.get("page");
limitPage = pageInfo.toString();
}
}
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
Class> type = mappedStatement.getParameterMap().getType();
String oldSql = mappedStatement.getBoundSql(argObject).getSql();
System.out.println(oldSql);
String newSql = oldSql + limitPage;
System.out.println("新Sql语句" + newSql);
SqlSource sqlSource = new RawSqlSource(mappedStatement.getConfiguration(), newSql, type);
MetaObject metaObject = SystemMetaObject.forObject(mappedStatement);
metaObject.setValue("sqlSource", sqlSource);
// Field field = mappedStatement.getClass().getDeclaredField("sqlSource");
// field.setAccessible(true);
// field.set(mappedStatement,sqlSource);
Object o = invocation.proceed();
return o;
}
@Override
public void setProperties(Properties properties) {
}
@Override
public Object plugin(Object o) {
return Plugin.wrap(o, this);
}
}
这里其实setProperties方法是设置属性的,参数是在配置文件中写的property,本例子中没有用到
测试
public static void main( String[] args )
{
String resource = "mybatis-config.xml";
try {
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory build = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession2 = build.openSession();
List select = sqlSession2.getMapper(IUserDao.class).select(new PageInfo(2, 10));
System.out.println(select);
} catch (IOException e) {
e.printStackTrace();
}
}
end...