MyabtisPlus(7):PageHelper插件

452 阅读3分钟

jar包

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.2.12</version>
</dependency>

代码实现

在beanname为com.github.pagehelper.autoconfigure.PageHelperAutoConfiguration的时候,最终会走到下面的方法中
PageHelperAutoConfiguration->addPageInterceptor():
    // 新建一个拦截器
    PageInterceptor interceptor = new PageInterceptor();
    Properties properties = new Properties();
    // 读取yml里pagehelper的配置
    properties.putAll(pageHelperProperties());
    properties.putAll(this.properties.getProperties());
    // 初始化PageHelper类
    interceptor.setProperties(properties);
    for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
        // 将该拦截器注入到configuration的interceptorChain中
        sqlSessionFactory.getConfiguration().addInterceptor(interceptor);
    }
DemoController->test():
    PageHelper.startPage(1, 10);   
    service.list();
PageMethod->startPage():
    return startPage(pageNum, pageSize, DEFAULT_COUNT);
PageMethod->startPage():
    // 关注前三个参数即可
    // pageNum是页码,pageSize是每页显示数量,count是否进行count查询
    return startPage(pageNum, pageSize, count, null, null);
PageMethod->startPage():
    ......
    setLocalPage(page);
PageMethod->setLocalPage():
    // LOCAL_PAGE是一个ThreadLocal变量,这就是为什么PageHepler只对当前线程起作用的原因了
    LOCAL_PAGE.set(page);
    
// 之前所有sqlSession都会被代理,所以会进入invoke方法
SqlSessionTemplate->SqlSessionInterceptor->invoke():
    // 每一条线程都会创建一个单独的sqlSession
    SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
        SqlSessionTemplate.this.executorType,SqlSessionTemplate.this.exceptionTranslator); // --1
    Object result = method.invoke(sqlSession, args); // --2
    ......
    // 最终会从连接池释放连接
    // 连接池有个connections[],启动的时候connections[0]=e,poolingCount=1
    // 查询时poolingCount=0,temp=e,connections[0]=null,return temp
    // 执行sqlSession.close之后,connections[0]=e,poolingCount=1
    // 这就是连接池的意义了
     finally {
        if (sqlSession != null) {
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
        }
    }
SqlSessionUtils->getSqlSession(): // --1
    session = sessionFactory.openSession(executorType);
DefaultSqlSessionFactory->openSession():
    return openSessionFromDataSource(execType, null, false);
DefaultSqlSessionFactory->openSessionFromDataSource():
    final Executor executor = configuration.newExecutor(tx, execType);
    return new DefaultSqlSession(configuration, executor, autoCommit);
MybatisConfiguration->newExecutor():
    // 通过适配器模式初始化执行器
    ......
    // pageInterceptor拦截器动态代理
    executor = (Executor) interceptorChain.pluginAll(executor); 
    return executor;
Plugin->wrap():  
    // PageInterceptor的@Intercepts信息,这里的key是Signature.type(Executor类),值是被拦截的方法及参数(query方法)
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    // target是前面的executor
    Class<?> type = target.getClass();
    // 获取目标类及父类所有实现的接口(即Executor接口),返回signatureMap包含该接口类的类
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    // 有的话,就创建动态代理,Plugin作为代理类
    if (interfaces.length > 0) {
        return Proxy.newProxyInstance(
            type.getClassLoader(),
            interfaces,
            new Plugin(target, interceptor, signatureMap));
    }
    return target;

image.png

DefaultSqlSession->selectList(): // --2
    return this.selectList(statement, parameter, RowBounds.DEFAULT);
DefaultSqlSession->selectList():
    MappedStatement ms = configuration.getMappedStatement(statement);
    // 因为executor被代理了,所以进入Plugin.invoke()方法
    return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
Plugin->invoke():
    // signatureMap.get(Executor)返回不为空
    Set<Method> methods = signatureMap.get(method.getDeclaringClass());
    // 现在是query方法,执行intercept方法
    if (methods != null && methods.contains(method)) {
        return interceptor.intercept(new Invocation(target, method, args));
    }
    return method.invoke(target, args);
PageInterceptor->intercept():
    try {
        // 调用方法判断是否需要进行分页,如果不需要,直接返回结果
        // LOCAL_PAGE.get()如果为空直接返回true
        if (!dialect.skip(ms, parameter, rowBounds)) {
            // 判断是否需要进行 count 查询
            if (dialect.beforeCount(ms, parameter, rowBounds)) {
                // 查询总数
                Long count = count(executor, ms, parameter, rowBounds, resultHandler, boundSql);
                // 处理查询总数,返回 true 时继续分页查询,false 时直接返回
                if (!dialect.afterCount(count, parameter, rowBounds)) {
                    // 当查询总数为 0 时,直接返回空的结果
                    return dialect.afterPage(new ArrayList(), parameter, rowBounds);
                }
            }
            // 这边会做很多的事情:主要有重写BoundSql,绑定参数,根据设置值优化sql等等
            resultList = ExecutorUtil.pageQuery(dialect, executor,
                        ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
        } else {
            // 不需要分页则直接查询
            resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
        }
        return dialect.afterPage(resultList, parameter, rowBounds);
    } finally {
        if(dialect != null){
            // 执行LOCAL_PAGE.remove()方法,移除当前线程中的Page信息
            dialect.afterAll();
        }
    }

记录: PageHelper包关键类与作用:

  • PageInterceptor
    • Dialect是数据库方言,不同的数据库拼接不同的分页语句,拼接参数
    • 当是query方法时,走拦截器
  • PageHelper 当前线程分页操作
  • PageMethod 其下有个ThreadLocal型变量LOCAL_PAGE,在执行startPage()方法时调用setLocalPage(page)设置page信息,如果当前线程没有page信息,则不分页;当拦截生效,其finally代码块执行LOCAL_PAGE.remove()移除page信息,这就是分页只针对当前线程且只对一条select sql生效的原因了
  • Page 记录总页数、总条数、当前页数、当前条数、起始条数、起始页数等信息

总结:
PageHelper在启动时给configuration的interceptorChain增加拦截器,查询时通过动态代理拦截query方法,进入PageInterceptor的intercept方法,对原来查询的sql语句,参数值修改,最终返回executor.query

sql打印类PerformanceInterceptor实现原理与PageHelper一样一样的