您的位置:首页 > 其它

Mybatis 之 PageHelper分页插件源码浅析

2019-08-19 21:12 316 查看
版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。 本文链接:https://blog.csdn.net/vcj1009784814/article/details/99759738

PageHelper分页插件原理浅析

  1. PageHelper分页插件的使用

      引入依赖

      <dependency>
      <groupId>com.github.pagehelper</groupId>
      <artifactId>pagehelper</artifactId>
      <version>5.1.6</version>
      </dependency>
    • 在mybatis全局配置文件中添加plugin配置

      <configuration>
      <properties resource="properties/db.properties"></properties>
      <!-- 注意plugins 标签的位置,需要放在environments标签之前 -->
      <plugins>
      <plugin interceptor="com.github.pagehelper.PageHelper">
      <property name="dialect" value="mysql"/>
      </plugin>
      </plugins>
      <!-- 数据源 -->
      <environments default="development">
      <environment id="development">
      <transactionManager type="JDBC" />
      <dataSource type="POOLED">
      <property name="driver" value="${db.driver}" />
      <property name="url" value="${db.url}" />
      <property name="username" value="${db.username}" />
      <property name="password" value="${db.password}" />
      </dataSource>
      </environment>
      </environments>
      <mappers>
      <mapper resource="dao/UserMapper.xml" />
      </mappers>
      </configuration>
    • 在DAO层进行分页查询

      public class PageHelperTest{
      private SqlSessionFactory sqlSessionFactory;
      @Before
      public void init() {
      try {
      String resource = "mybatis-config.xml";
      InputStream inputStream = Resources.getResourceAsStream(resource);
      sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
      } catch (IOException e) {
      e.printStackTrace();
      }
      }
      
      @Test
      public PageInfo<User> testPage() {
      SqlSession session = sqlSessionFactory.openSession();//开启分页,只对最近一次的查询有分页的效果
      //剧透一下,是使用了ThreadLocal来存储Page分页信息的
      PageHelper.startPage(2,4);
      //下面的查询会被PageHelper拦截,返回的结果其实是一个Page对象,Page继承自ArrayList
      List<User> lists = session.selectList("findAll");
      //mapper.xml里配置了一个<select id="findAll"> select * from user;</select>
      PageInfo<User> pageInfo = new PageInfo<>(lists);
      return pageInfo;
      }
      }
  2. PageHelper插件工作原理

      首先,在全局配置文件里使用

      <plugin>
      标签,将PageHelper拦截器添加到配置中。实际,拦截器是对Executor起了作用,先来看拦截器是如何被配置的:

      进行全局配置文件的解析和装载时,就会将拦截器信息封装到了Configuration对象中,流程如下:

      String resource = "dao/SqlMapConfig.xml";
      InputStream inputStream = Resources.getResourceAsStream(resource);
      //下面的build方法,为全局配置文件的解析入口
      sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
      public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
      try {
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      // parser.parse():使用XPATH解析XML配置文件,将配置文件封装为Configuration对象
      // 返回DefaultSqlSessionFactory对象,该对象拥有Configuration对象(封装配置文件信息)
      return build(parser.parse());
      } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
      } finally {
      ErrorContext.instance().reset();
      try {
      inputStream.close();
      } catch (IOException e) {
      }
      }
      }

      继续看XMLConfigBuilder中的parse方法

      public Configuration parse() {
      if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
      }
      parsed = true;
      parseConfiguration(parser.evalNode("/configuration"));
      return configuration;
      }
      
      private void parseConfiguration(XNode root) {
      try {
      //下面的解析顺序,即解释了配置文件中的标签书写顺序
      propertiesElement(root.evalNode("properties"));
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      loadCustomLogImpl(settings);
      typeAliasesElement(root.evalNode("typeAliases"));
      // 关键在这,解析<plugins>标签
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      mapperElement(root.evalNode("mappers"));
      } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
      }
      }
      
      private void pluginElement(XNode parent) throws Exception {
      if (parent != null) {
      for (XNode child : parent.getChildren()) {
      //循环遍历plugins标签下的子标签
      String interceptor = child.getStringAttribute("interceptor");
      Properties properties = child.getChildrenAsProperties();
      Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
      interceptorInstance.setProperties(properties);
      //将当前拦截器添加到Configuration对象中
      configuration.addInterceptor(interceptorInstance);
      }
      }
      }
      
      //Configuration对象中
      public void addInterceptor(Interceptor interceptor) {
      interceptorChain.addInterceptor(interceptor);
      //Configuration持有一个InterceptorChain对象
      //InterceptorChain中持有一个List<Interceptor>对象
      }

      至此,装载拦截器流程完毕,Configuration对象被封装到了DefaultSqlSessionFactory里

    • 使用拦截器对Executor进行增强

      之后,我们调用openSession打开一个SqlSession,看看创建SqlSession时都做了些什么

      SqlSession session = sqlSessionFactory.openSession();
      //DefaultSqlSession中
      public SqlSession openSession() {
      return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
      }
      
      private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
      Transaction tx = null;
      try {
      // 获取数据源环境信息
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      // 创建Executor执行器,关键在这里*****
      final Executor executor = configuration.newExecutor(tx, execType);
      // 创建DefaultSqlSession
      return new DefaultSqlSession(configuration, executor, autoCommit);
      } catch (Exception e) {
      closeTransaction(tx);
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
      } finally {
      ErrorContext.instance().reset();
      }
      }

      进入Configuration

      public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
      executorType = executorType == null ? defaultExecutorType : executorType;
      executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
      Executor executor;
      if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
      } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
      } else {
      executor = new SimpleExecutor(this, transaction);
      }
      // 如果开启缓存(默认是开启的),则使用缓存执行器
      if (cacheEnabled) {
      executor = new CachingExecutor(executor);
      }
      // 插件增强,在这里使用插件对executor进行增强
      executor = (Executor) interceptorChain.pluginAll(executor);
      return executor;
      }

      进入InterceptorChain

      public Object pluginAll(Object target) {
      for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
      }
      return target;
      }

      看到这里,就知道其实是通过Interceptor接口的plugin方法为纽带,来对Executor进行增强的(AOP),这里,Interceptor即为PageHelper,所以接下来进入PageHelper

      //PageHelper
      public Object plugin(Object target) {
      return target instanceof Executor ? Plugin.wrap(target, this) : target;
      }
      //Plugin
      //这个Plugin是mybatis自带的接口,实现了InvocationHandler,也说明mybatis拦截器底层是通过JDK动态代理来实现的
      public static Object wrap(Object target, Interceptor interceptor) {
      //前面入参过来,这里interceptor是PageHelper
      Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
      Class<?> type = target.getClass();
      Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
      if (interfaces.length > 0) {
      //下面是经典的JDK动态代理生成代码
      //继续进入Plugin的构造函数看看
      return Proxy.newProxyInstance(
      type.getClassLoader(),
      interfaces,
      new Plugin(target, interceptor, signatureMap));
      }
      return target;
      }
      
      private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
      this.target = target;
      this.interceptor = interceptor;
      this.signatureMap = signatureMap;
      }
      //只是进行了一些简单的变量设置
      
      //JDK动态代理,执行时会调用InvocationHandler中的invoke方法,所以我们接着看Plugin里面的invoke方法
      
      @Override
      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      try {
      Set<Method> methods = signatureMap.get(method.getDeclaringClass());
      if (methods != null && methods.contains(method)) {
      //看到这里就比较明白了,实际调用了拦截器的intercept方法
      //那我们就去PageHelper的intercept方法里一探究竟
      return interceptor.intercept(new Invocation(target, method, args));
      }
      return method.invoke(target, args);
      } catch (Exception e) {
      throw ExceptionUtil.unwrapThrowable(e);
      }
      }

      //PageHelper
      public Object intercept(Invocation invocation) throws Throwable {
      if (this.autoRuntimeDialect) {
      SqlUtil sqlUtil = this.getSqlUtil(invocation);
      return sqlUtil.processPage(invocation);
      } else {
      if (this.autoDialect) {
      this.initSqlUtil(invocation);
      }
      
      return this.sqlUtil.processPage(invocation);
      }
      }
      //SqlUtil
      public Object processPage(Invocation invocation) throws Throwable {
      Object var3;
      try {
      Object result = this._processPage(invocation);
      var3 = result;
      } finally {
      clearLocalPage();
      }
      
      return var3;
      }
      
      private Object _processPage(Invocation invocation) throws Throwable {
      Object[] args = invocation.getArgs();
      Page page = null;
      if (this.supportMethodsArguments) {
      page = this.getPage(args);
      }
      RowBounds rowBounds = (RowBounds)args[2];
      if (this.supportMethodsArguments && page == null || !this.supportMethodsArguments && getLocalPage() == null && rowBounds == RowBounds.DEFAULT) {
      return invocation.proceed();
      } else {
      if (!this.supportMethodsArguments && page == null) {
      //SqlUtil里维护了一个ThreadLocal<Page>
      //这里会从ThreadLocal中去取Page对象
      //这个Page对象是在调用PageHelper.startPage()时,被添加到ThreadLocal里的
      page = this.getPage(args);
      }
      
      return this.doProcessPage(invocation, page, args);
      }
      }
      
      //下面是最最最终的执行
      //流程有点复杂
      //大概可以分为2步:
      //1. 修改入参中的MappedStatement,执行一次SQL查询,获取总数count,设置到Page对象中
      //(这一步可通过PageHelper.startPage(int pageNum,int pageSize,boolean count) 中的第三个参数来取消)
      //2. 通过Page对象里的分页信息,修改MappedStatement中的SqlSource,通过getBoundSql,在sql语句末尾添加 limit ?,? ,后通过StatementHandler,将分页信息绑定到入参,执行查询
      private Page doProcessPage(Invocation invocation, Page page, Object[] args) throws Throwable {
      //args是Executor执行query时的入参
      //args[0] 为 MappedStatement
      //args[1] 为 Object ,是SQL查询时的参数
      //args[2] 为 RowBounds 貌似和分页有关
      //agrs[3] 为ResultHandler 结果集处理器
      //可以参考CachingExecutor的query方法
      RowBounds rowBounds = (RowBounds)args[2];
      MappedStatement ms = (MappedStatement)args[0];
      if (!this.isPageSqlSource(ms)) {
      this.processMappedStatement(ms);
      }
      
      //这里设置Parser,可能的取值有MysqlParser,OracleParser,PostgreSQLParser等
      //本例中为MysqlParser
      ((PageSqlSource)ms.getSqlSource()).setParser(this.parser);
      
      try {
      args[2] = RowBounds.DEFAULT;
      if (this.isQueryOnly(page)) {
      Page var12 = this.doQueryOnly(page, invocation);
      return var12;
      }
      
      if (page.isCount()) {
      //如果开启了查询总数,先查询总数
      page.setCountSignal(Boolean.TRUE);
      //这里args[0]即是Executor执行查询时入参的MappedStatement
      //先将MappedStatement入参改为查询总数的Statement
      args[0] = msCountMap.get(ms.getId());
      //下面执行Executor查询,得到总数
      Object result = invocation.proceed();
      //将Executor查询入参的MappedStatement恢复
      args[0] = ms;
      //将记录的总条数,设置到Page对象中
      page.setTotal((long)(Integer)((List)result).get(0));
      if (page.getTotal() == 0L) {
      //若记录数量为0,则直接返回,不执行真正的查询
      Page var7 = page;
      return var7;
      }
      } else {
      page.setTotal(-1L);
      }
      
      if (page.getPageSize() > 0 && (rowBounds == RowBounds.DEFAULT && page.getPageNum() > 0 || rowBounds != RowBounds.DEFAULT)) {
      //存在分页信息时,进行分页查询
      page.setCountSignal((Boolean)null);
      //通过下面这一句,得到一个BoundSql,此时已经添加了limit ?,?
      //args[1]是sql查询的参数,将会进行sql参数绑定
      BoundSql boundSql = ms.getBoundSql(args[1]);
      //这一句进行参数封装,将分页信息封装到args[1]中,后续会替换limit ?,?中的?
      args[1] = this.parser.setPageParameter(ms, args[1], boundSql, page);
      page.setCountSignal(Boolean.FALSE);
      //执行查询
      Object result = invocation.proceed();
      //将结果封装为Page对象,并返回
      page.addAll((List)result);
      }
      } finally {
      ((PageSqlSource)ms.getSqlSource()).removeParser();
      }
      
      return page;
      }

      下面通过一次debug过程,来从头到尾跟一遍PageHelper拦截器的执行

      下面是测试代码:

      public class TestPageHelper{
      private SqlSessionFactory sqlSessionFactory;
      @Before
      public void before() {
      try {
      String resource = "dao/SqlMapConfig.xml";
      InputStream inputStream = Resources.getResourceAsStream(resource);
      sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
      } catch (IOException e) {
      e.printStackTrace();
      }
      }
      @Test
      public void testPage() {
      SqlSession session = sqlSessionFactory.openSession();PageHelper.startPage(2,4);
      List<User> lists = session.selectList("findAll");
      PageInfo<User> pageInfo = new PageInfo<User>(lists);
      System.out.println(lists);
      }
      }
      

      从SqlSessionFactory.openSession开始跟

      至此,openSession跟完了,使用JDK动态代理对Executor进行了增强

      下面开始跟PageHelper.startPage

      下面开始跟SQL查询流程

      执行Executor的query之前,会执行PageHelper中的intercept方法,按照先前分析的,最终会进入到SqlUtil的doProcessPage方法,下面替换掉MappedStatement后,会执行Executor的query

      执行query

      后委托给BaseExecutor执行查询

      最后会进入到BaseExecutor中的queryFromDatabase方法,随后进入SimpleExecutor的doQuery

      第一次查询总数完成后,继续返回到SqlUtil的doProcessPage方法中,继续往下执行

      先进入355,主要是将分页信息,封装到入参Object中

      下面进入到getBoundSql

      继续进入PageRawSqlSource的getBoundSql(PageRawSqlSource继承自PageSqlSource,这里是直接调用父类的方法)

      继续看getPageBoundSql方法


      继续看PageStaticSqlSource中的getPageBoundSql,关键在于调用了MysqlParser的getPageSql

      继续进入MysqlParser中去看看,最终找到了limit ? , ?

      再往后就是调用StatemenHandler对具体的SQL进行参数绑定,再执行查询,就不再赘述啦~

      下面列一下PageHelper涉及到的一些主要技术点作为总结:

      使用ThreadLocal存储Page分页信息(查询完后会清除)
    • 使用JDK动态代理来完成插件对Executor的拦截


第一遍跟,会觉得头晕,多跟几遍,再梳理一下类的结构关系,就会比较清晰一点。一些更加细节的部分没有列出来,如PageHelper拦截后,将MappedStatement中的SqlSource再进行了一次封装,封装成PageSqlSource,PageRawSqlSource,还有PageStaticSqlSource啥啥的(在SqlUtil的

doProcessPage
中的
this.processMappedStatement()
方法中,这个方法内还有将
select count(0) from user
添加到一个map里的操作),以及第一次查询总数时,是如何将
select * from user
替换成
select count(0) from user
(在
SqlParser
中的
sqlToCount
方法),有兴趣的胖友可以自行深入。本人技术有限,如有纰漏或错误的地方,欢迎各位大佬批评指正。

(完)

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: