pagehelper 原理解析

参考文章

https://blog.csdn.net/weixin_40757920/article/details/105400943
https://zhuanlan.zhihu.com/p/265641500

MyBatis插件的使用

首先我们先来通过一个例子来看看如何使用插件。
1、首先建立一个MyPlugin实现接口Interceptor,然后重写其中的三个方法(注意,这里必须要实现Interceptor接口,否则无法被拦截)。

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import java.util.Properties;

@Intercepts({@Signature(type = Executor.class,method = "query",args = {MappedStatement.class,Object.class, RowBounds.class, ResultHandler.class})})
public class MyPlugin implements Interceptor {

    /**
     * 这个方法会直接覆盖原有方法
     * @param invocation
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("成功拦截了Executor的query方法,在这里我可以做点什么");
        return invocation.proceed();//调用原方法
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target,this);//把被拦截对象生成一个代理对象
    }

    @Override
    public void setProperties(Properties properties) {//可以自定义一些属性
        System.out.println("自定义属性:userName->" + properties.getProperty("userName"));
    }
}

注解说明

@Intercepts是声明当前类是一个拦截器
@Signature是标识需要拦截的方法签名,通过以下三个参数来确定

  • type:被拦截的类名。指定拦截器类型(ParameterHandler ,StatementHandler,ResultSetHandler )
  • method:被拦截的方法名,是拦截器类型中的方法,不是自己写的方法
  • args:是method中方法的入参类型

2、我们还需要在mybatis-config中配置好插件。

<plugins>
        <plugin interceptor="com.lonelyWolf.mybatis.plugin.MyPlugin">
            <property name="userName" value="张三"/>
        </plugin>
</plugins>

这里如果配置了property属性,那么我们可以在setProperties获取到。

完成以上两步,我们就完成了一个插件的配置了,接下来我们运行一下:

在这里插入图片描述

可以看到,setProperties方法在加载配置文件阶段就会被执行了。

sql是怎么执行的

在MyBatis中插件是通过拦截器来实现的,那么既然是通过拦截器来实现的,就会有一个问题,哪些对象才允许被拦截呢?

真正执行Sql的是四大对象:

  • Executor : 执行器,由它调度StatementHandler、ParameterHandler和ResultSetHandler等来执行对应的SQL。

  • StatementHandler,使用数据库的Statement(PreparedStatement)执行操作,它是四大对象的核心,起到承上启下的作用,许多重要的插件都是通过拦截它来实现的。

  • ParameterHandler,用来处理SQL参数。

  • ResultSetHandler。进行数据集(ResultSet)的封装返回处理的。

需要注意的是,虽然我们可以拦截这四大对象,但是并不是这四大对象中的所有方法都能被拦截,下面就是官网提供的可拦截的对象和方法汇总:

在这里插入图片描述

插件的加载

我们进入XMLConfigBuilder类看看

    private void pluginElement(XNode parent) throws Exception {
        if (parent != null) {
            for (XNode child : parent.getChildren()) {
                String interceptor = child.getStringAttribute("interceptor"); // 获取拦截
                Properties properties = child.getChildrenAsProperties(); // 获取自定义properties属性
                // 根据配置文件中配置的插件类的全限定类名进行反射初始化
                Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
                interceptorInstance.setProperties(properties);
                //添加到配置类的InterceptorChain属性,InterceptorChain类维护了一个List<Interceptor>
                configuration.addInterceptor(interceptorInstance);
            }
        }
    }

解析出来之后会将插件存入InterceptorChain对象的list属性。

public class InterceptorChain {
	// 存取插件
    private final List<Interceptor> interceptors = new ArrayList(); 
    public Object pluginAll(Object target) {
        Interceptor interceptor;
        // 循环解析插件
        for(Iterator var2 = this.interceptors.iterator(); 
        	var2.hasNext(); 
        	// 获取代理对象
        	target = interceptor.plugin(target)) {
            	interceptor = (Interceptor)var2.next();
        }

        return target;
    }
    .....
}

看到InterceptorChain我们是不是可以联想到,MyBatis的插件就是通过责任链模式实现的

1、Executot——执行器

1.1 执行器介绍

Executor是一个执行器,SqlSession是一个门面,真正干活的是执行器,它是一个真正执行Java与数据库交互的对象。
mybatis中有3种执行器,可以在mybaits配置文件中的defaultExecutorType属性进行选择,对于spring boot项目则在application配置文件中配置mybatis.configuration.default-executor-type属性。

1.2 执行器分类

  • SIMPLE——简易执行器,没有什么特别的,默认执行器。
  • REUSE——一种能够执行重用预处理语句的执行器。
  • BATCH——执行器重用语句和批量更新,批量专用的执行器。

1.3、执行器的加载

在Configuration类中有如下方法,该方法根据配置的执行器类型去确定创建哪一种Executor。

  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) {
	//缓存用CachingExecutor进行包装Executor
      executor = new CachingExecutor(executor);
    }
	//在运用插件时,拦截Executor
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

上述代码中的 interceptorChain.pluginAll(executor) ;是运行插件的关键,它拦截Executor,并构建一层层的动态代理对象,可以修改在调度真实的Executor方法之前执行配置插件的代码,这个就是插件的原理

public Object pluginAll(Object target) {
        Interceptor interceptor;
        // 循环解析插件
        for(Iterator var2 = this.interceptors.iterator(); 
        	var2.hasNext(); 
        	// 获取代理对象
        	target = interceptor.plugin(target)) {
            	interceptor = (Interceptor)var2.next();
        }

        return target;
    }

点进去

    default Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

到这里我们是不是发现很熟悉,没错,这就是我们上面示例中重写的方法,而plugin方法是接口中的一个默认方法。
这个方法是关键,我们进去看看:

    public static Object wrap(Object target, Interceptor interceptor) {
        // 获取到我们加注解的类
        Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
        // 被代理对象,如 Executor,StatementHandler ……
        Class<?> type = target.getClass();
        // 被代理对象接口
        Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
        if (interfaces.length > 0) {
            // 使用JDK 动态代理,而这个对象必须要有接口,所以自定义插件一定要实现 Interceptor 接口
            return Proxy.newProxyInstance(
                    type.getClassLoader(),
                    interfaces,
                    new Plugin(target, interceptor, signatureMap));
        }
        // 返回被代理的对象
        return target;
    }


// 而最终执行的intercept方法,就是我们上面示例中重写的方法。
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
    	// 被拦截的方法签名
      Set<Method> methods = signatureMap.get(method.getDeclaringClass());
      //如何当前方法属于被拦截方法,则执行代理对象的方法 intercept
      if (methods != null && methods.contains(method)) {
        return interceptor.intercept(new Invocation(target, method, args));
      }
      // 没有被代理,调用员方法
      return method.invoke(target, args);
    } catch (Exception e) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
  }

可以看到这个方法的逻辑也很简单,但是需要注意的是MyBatis插件是通过JDK动态代理来实现的,而JDK动态代理的条件就是被代理对象必须要有接口,这一点和Spring中不太一样,Spring中是如果有接口就采用JDK动态代理,没有接口就是用CGLIB动态代理。

正因为MyBatis的插件只使用了JDK动态代理,所以我们上面才强调了一定要实现Interceptor接口。

而其他三个也是这样进行拦截处理的,同样是 Configuration 类中进行处理

public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
    ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
    parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
    return parameterHandler;
  }

public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
      ResultHandler resultHandler, BoundSql boundSql) {
    ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
    resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
    return resultSetHandler;
  }

public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
  }
  
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : 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) interceptorChain.pluginAll(executor);
    return executor;
  }

1.4、执行器是如何执行SQL的?

在映射器Mapper的动态代理中,我们知道最后就是通过SqlSession对象去运行对象的SQL。通过跟踪代码,在Mapper的代理对象(即MapperProxy的实例)中的invoke()方法中,最后是执行了mapperMethod.execute(sqlSession, args);,而在execute()方法中,是采用了命令模式跳转到需要的方法中,在上文中,我们看了executeForMany()方法,其中有下面这段代码:

	if (method.hasRowBounds()) {
      RowBounds rowBounds = method.extractRowBounds(args);
      result = sqlSession.selectList(command.getName(), param, rowBounds);
    } else {
      result = sqlSession.selectList(command.getName(), param);
    }

可以看到,最终就是通过SqlSession对象去运行对象的SQL,在单线程环境下,SqlSession的实现类是DefaultSqlSession,它的selectList()方法如下:

  @Override
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

这里用到执行器Executor的query()方法,定位到Executor的实现类BaseExecutor,代码如下:

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
      queryStack++;
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      queryStack--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }

上面代码中的doQuery(ms, parameter, rowBounds, resultHandler, boundSql);是由Executor的三种类型来实现的,默认是SIMPLE,即SimpleExecutor,如下:

  @Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
  Statement stmt = null;
  try {
    //从该映射器节点MappedStatement中获取配置信息Configuration
    Configuration configuration = ms.getConfiguration();
    //根据Configuration构建StatementHandler
    StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
    //对SQL编译和参数进行初始化
    stmt = prepareStatement(handler, ms.getStatementLog());
    //使用StatementHandler的query()方法,把ResultHandler传递进去
    return handler.query(stmt, resultHandler);
  } finally {
    closeStatement(stmt);
  }
}

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
  Statement stmt;
  Connection connection = getConnection(statementLog);
  //调用StatementHandler的prepare()进行了预编译和基础的设置
  stmt = handler.prepare(connection, transaction.getTimeout());
  //通过StatementHandler的parameterize()来设置参数
  handler.parameterize(stmt);
  return stmt;
}

根据上面代码,可以看到最终的操作落到StatementHandler上。

2、StatementHandler——数据库会话器

2.1、StatementHandler介绍

数据库会话器就是专门处理数据库会话的,在Configuration中,mybatis通过如下方式生成StatementHandler:

  public StatementHandler newStatementHandler(Executor executor, 
						  MappedStatement mappedStatement, 
						  Object parameterObject, 
						  RowBounds rowBounds, 
						  ResultHandler resultHandler, 
						  BoundSql boundSql) {
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
  }
  //对象适配器,它的作用是给3个接口对象的使用提供一个统一且简易的适配器
  private final StatementHandler delegate;
  
  public RoutingStatementHandler(Executor executor, 
						MappedStatement ms, 
						Object parameter, 
						RowBounds rowBounds, 
						ResultHandler resultHandler, 
						BoundSql boundSql) {
    switch (ms.getStatementType()) {
      case STATEMENT:
        delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case PREPARED:
        delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case CALLABLE:
        delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      default:
        throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
    }

  }

很显然,mybatis是通过RoutingStatementHandler的对象来创建真实对象的,RoutingStatementHandler不是真实的服务对象,它是通过适配器模式来找到对应的StatementHandler来执行的。
在mybaits中,RoutingStatementHandler分为3种:
SimpleStatementHandler、PreparedStatementHandler、CallableStatementHandler,他们分别对应的是JDBC的Statement、PreparedStatement(预编译处理)和CallableStatement(存储过程处理)。

2.2、StatementHandler的执行过程(以PreparedStatementHandler为例)

我们在Executor部分说到,Executor在执行查询时(如doQuery()方法)会执行StatementHandler的prepare、parameterize和query方法。其中PreparedStatementHandler的prepare方法如下:

public abstract class BaseStatementHandler implements StatementHandler {
	//......
	@Override
  public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
    ErrorContext.instance().sql(boundSql.getSql());
    Statement statement = null;
    try {
	  //对SQL进行预编译,做一些基础配置,由不同的StatementHandler的实现
      statement = instantiateStatement(connection);
      setStatementTimeout(statement, transactionTimeout);
      setFetchSize(statement);
      return statement;
    } catch (SQLException e) {
      closeStatement(statement);
      throw e;
    } catch (Exception e) {
      closeStatement(statement);
      throw new ExecutorException("Error preparing statement.  Cause: " + e, e);
    }
  }
	//...
}

接着,通过parameterize()方法设置参数,如下:

  @Override
  public void parameterize(Statement statement) throws SQLException {
   //显然使用了ParameterHandler
    parameterHandler.setParameters((PreparedStatement) statement);
  }

最后执行query()查询方法——执行SQL返回结果,如下:

  @Override
  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
   //java.sql.PreparedStatement 与数据库交互
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
	//包装结果集ResultSet
    return resultSetHandler.handleResultSets(ps);
  }

一条查询SQL的执行过程?

Executor先调用StatementHandler的prepare()方法预编译SQL,同时设置一些基本运行的参数。
然后用parameterize()方法启用ParameterHandler设置参数,完成预编译,执行查询,update()也是这样的。
如果是查询,mybatis会使用ResultSetHandler封装结果返回给调用者。

3、ParameterHandler——参数处理器

  • mybatis通过ParameterHandler对预编译语句进行参数设置,它的作用是完成对预编译参数的设置。
  • mybatis为ParameterHandler提供了一个实现类DefaultParameterHandler。
  • 设置参数过程中,是从parameterObject对象中取到参数,然后使用TypeHandler转换参数,如果有设置,那么它会根据签名注册的TypeHandler对参数进行处理。
  • TypeHandler是在mybatis初始化时,注册在Configutation里面的,需要时就可以直接拿来用了。

4、ResultSetHandler——结果处理器

  • ResultSetHandler是组装结果集返回的。
  • mybatis为ResultSetHandler提供了一个实现类DefaultResultSetHandler,在默认情况下都是通过这个类进行处理的。
  • 它涉及使用JAVASSIST(或者CGLIB)作为延迟加载。
  • 然后通过TypeHandler和ObjectFactory进行组装结果再返回。

插件执行流程

时序图

在这里插入图片描述

假如一个对象被代理很多次

一个对象是否可以被多个代理对象进行代理?也就是说同一个对象的同一个方法是否可以被多个拦截器进行拦截?

答案是肯定的,因为被代理对象是被加入到list,所以我们配置在最前面的拦截器最先被代理,但是执行的时候却是最外层的先执行。
具体点:
假如依次定义了三个插件:插件A,插件B和插件C。
那么List中就会按顺序存储:插件A,插件B和插件C,而解析的时候是遍历list,所以解析的时候也是按照:插件A,插件B和插件C的顺序,但是执行的时候就要反过来了,执行的时候是按照:插件C,插件B和插件A的顺序进行执行。

PageHelper插件原理

我们上面提到,要实现插件必须要实现MyBatis提供的Interceptor接口,所以我们去找一下,发现PageHeler实现了Interceptor:
在这里插入图片描述

经过上面的介绍这个类应该一眼就能看懂,我们关键要看看SqlUtil的intercept方法做了什么:

在这里插入图片描述

这个方法的逻辑比较多,因为要考虑到不同的数据库方言的问题,所以会有很多判断,我们主要是关注PageHelper在哪里改写了sql语句,上图中的红框就是改写了sql语句的地方:
在这里插入图片描述

这里面会获取到一个Page对象,然后在爱写sql的时候也会将一些分页参数设置到Page对象,我们看看Page对象是从哪里获取的:

java

我们看到对象是从LOCAL_PAGE对象中获取的,这个又是什么呢?

这是一个本地线程池变量,那么这里面的Page又是什么时候存进去的呢?
这就要回到我们的示例上了,分页的开始必须要调用:

PageHelper.startPage(0,10);

在这里插入图片描述

这里就会构建一个Page对象,并设置到ThreadLocal内。

为什么PageHelper只对startPage后的第一条select语句有效

这个其实也很简单哈,但是可能会有人有这个以为,我们还是要回到上面的intercept方法:
在这里插入图片描述

在finally内把ThreadLocal中的分页数据给清除掉了,所以只要执行一次查询语句就会清除分页信息,故而后面的select语句自然就无效了。

不通过插件能否改变MyBatis的核心行为

上面我们介绍了通过插件来改变MyBatis的核心行为,那么不通过插件是否也可以实现呢?

答案是肯定的,官网中提到,我们可以通过覆盖配置类来实现改变MyBatis核心行为,也就是我们自己写一个类继承Configuration类,然后实现其中的方法,最后构建SqlSessionFactory对象的时候传入自定义的Configuration方法:

SqlSessionFactory build(MyConfiguration)

当然,这种方法是非常不建议使用的,因为这种方式就相当于在建房子的时候把地基抽出来重新建了,稍有不慎,房子就要塌了。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值