带你了解Mybatis拦截器及手写分页插件

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...

你可能感兴趣的:(带你了解Mybatis拦截器及手写分页插件)