Mybatis 分页插件的实现

126 阅读5分钟

手撸Mybatis 分页插件的实现

1、分页插件

插件

  • Mybatis 通过提供插件机制,让我们可以根据自己的需要去增强MyBtais的功能。
  • Mybatis 的插件可以在不修复原来的代码情况下,通过拦截的方式,改变四大核心对象的行为,比如 处理参数、处理SQL、处理结果

MyBatis 插件典型适用场景

分页功能

  • Mybatis的分页默认是基于内存分页的(查出所有,再截取),数据量大的情况下效率较低
  • 使用mybatis插件可以改变该行为,只需要拦截StatementHandler类的prepare方法,改变要执行的SQL语句为分页语句即可;

公共字段统一赋值

一般业务系统都会有创建者,创建时间,修改者,修改时间四个字段,对于这四个字段的赋值,实际上可以在DAO层统一拦截处理,可以用mybatis插件拦截Executor类的update方法,对相关参数进行统一赋值即可;

性能监控

对于SQL语句执行的性能监控,可以通过拦截Executor类的update, query等方法,用日志记录每个方法执行的时间;

其它

其实mybatis扩展性还是很强的,

  • 基于插件机制,基本上可以控制SQL执行的各个阶段,如初始化阶段、执行阶段,参数处理阶段,语法构建阶段,结果集处理阶段,具体可以根据项目业务来实现对应业务逻辑。

插件实现的思考

  1. 不修改对象的代码,怎么对对象的行为进行修改,比如说在原来的方法前面做一点事情,在原来的方法后面做一点事情?
    • 答案:大家很容易能想到用代理模式,这个也确实是MyBatis 插件的原理。动态代理
  2. 我们可以定义很多的插件,那么这种所有的插件会形成一个链路,比如我们提交一个休假申请,先是项目经理审批,然后是部门经理审批,再是HR 审批,再到总经理审批,怎么实现层层的拦截?
  • 答案:插件是层层拦截的,我们又需要用到另一种设计模式——责任链模式。

  • 在之前的源码中我们也发现了,mybatis内部对于插件的处理确实使用的代理模式,既然是代理模式

  • 我们应该了解MyBatis 允许哪些对象的哪些方法允许被拦截,并不是每一个运行的节点都是可以被修改的。只有清楚了这些对象的方法的作用,当我们自己编写插件的时候才知道从哪里去拦截。

image.png

上图可以看出

  • 我们的分页插件实现:就是拦截了Executor对象 的 query方法

  • Executor 会拦截到CachingExcecutor 或者BaseExecutor。因为创建Executor 时是先创建CachingExcecutor,再包装拦截。从代码顺序上能看到。我们可以通过mybatis的分页插件来看看整个插件从包装拦截器链到执行拦截器链的过程;如下图

image.png

  • 我们需要来看看官网对于自定义插件是怎么来做的,官网上有介绍:

    通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。

2、自定义Mybatis 分页插件

自定义分页插件

/**   
 * @version V1.0.0
 * @description    自定义分页插件:需要实现 Interceptor接口,重写 intercept方法
 * @date   2022/4/23 11:32 
 * @copyright 2021
 *  
 */

// 通过注解:指定要拦截什么对象(Executor) 哪些方法(query)
@Intercepts({
        @Signature(type = Executor.class,method = "query",args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
        @Signature(type = Executor.class,method = "query",args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
})

public class MyInterceptor implements Interceptor {


    /**
     * 实现代理对象
     *
     * @param invocation
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {

        System.out.println("简易版的分页插件:逻辑分页改为物理分页");


        //修改sql 拼接Limit 0,10
		
        //获取上面注解拦截的query方法中的参数
        Object[] args = invocation.getArgs();
        // MappedStatement 对mapper映射文件里面元素的封装
        MappedStatement ms = (MappedStatement) args[0];
        // boundSql 对sql和参数的封装
        Object parameterObj = args[1];
        BoundSql boundSql = ms.getBoundSql(parameterObj);
        // rowBounds 封装了逻辑分页的参数:当前页 offset、每页数量 limit
        RowBounds rowBounds = (RowBounds)args[2];

        // 拿到原来的sql语句
        String sql = boundSql.getSql();
        String limitSql =  sql+ " limit " + rowBounds.getOffset() + "," + rowBounds.getLimit();

        // 将分页sql重新封装一个 BoundSql 进行后续执行
        BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), limitSql, boundSql.getParameterMappings(), parameterObj);

        // 被代理的对象
        Executor executor = (Executor)invocation.getTarget();
        CacheKey cacheKey = executor.createCacheKey(ms, parameterObj, rowBounds, boundSql);

        //调用修改过后的sql  继续执行查询
        return executor.query(ms,parameterObj,rowBounds,(ResultHandler) args[3],cacheKey,pageBoundSql);
    }
}

分页插件使用

  1. 自定义分页插件:需要实现 Interceptor接口,实现 intercept方法(上步已完成)

  2. 插件注册,在mybatis-config.xml 中注册插件:

    <configuration>
    
    -- 注意:插件标签的顺序有要求,否则会报错:properties?,settings?,typeAliases?,typeHandlers?,objectFactory?,objectWrapperFactory?,reflectorFactory?,plugins?,environments?,databaseIdProvider?,mappers?
        
    	<plugins>
         <!--自定义分页插件-->
            <plugin interceptor="com.kaidph.interceptor.MyInterceptor"></plugin>
    </plugins>
    
    </configuration>
    
  3. 调用

    /**
         * 自定义分页插件 测试
         */
        @Test
        public void test2(){
            // 从 xml 中构建SqlSessionFactory
            String resource = "mybatis-config.xml";
            InputStream inputStream = null;
            try {
                inputStream = Resources.getResourceAsStream(resource);
            } catch (IOException e) {
                e.printStackTrace();
            }
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    
            // 获取sqlSession 获得mapper动态代理,执行方法
            try (SqlSession session = sqlSessionFactory.openSession()) {
    
                UserMapper mapper = session.getMapper(UserMapper.class);
                List<User> userList = mapper.selectAllUser(new RowBounds(0,10));
    
                for (User user : userList) {
                    System.out.println(user);
                }
            }
    
        }
    

image.png

代理和拦截是怎么实现的?

  1. 上面提到的可以被代理的四大对象都是什么时候被代理的呢?

    Executor 是openSession() 的时候创建的;

    StatementHandler 是SimpleExecutor.doQuery()创建的;里面包含了处理参数的ParameterHandler 和处理结果集的ResultSetHandler 的创建,创建之后即调用InterceptorChain.pluginAll(),返回层层代理后的对象。

    代理是由Plugin 类创建。

    在我们重写的 plugin() 方法里面可以直接调用returnPlugin.wrap(target, this);返回代理对象。

  2. 当多个插件的情况下,代理能不能被代理?

    可以被代理

  3. 代理顺序和调用顺序的关系?

image.png

  1. 因为代理类是Plugin插件,所以最后调用的是Plugin 的invoke()方法。它先调用了定义的拦截器的intercept()方法。可以通过invocation.proceed()调用到被代理对象被拦截的方法。

image.png

   调用流程时序图

image.png