Mybatis分页插件PageHelper原理

前段时间甲同学遇到了在mybatis中遇到了一个神奇的问题,PageHelper会自动加上了limit ??导致查询数据不准,先还原现场,
mapper.xml中sql语句:

<select id="selectByExample" resultMap="BaseResultMap" parameterType="com.xxxx.xxxx.xxxx.domain.PartnerExample" >
    select
    <if test="distinct" >
      distinct
    </if>
    'true' as QUERYID,
    <include refid="Base_Column_List" />
    from usr_partner
    <if test="_parameter != null" >
      <include refid="Example_Where_Clause" />
    </if>
    <if test="orderByClause != null" >
      order by ${orderByClause}
    </if>
  </select>

Mapper.java代码:

@Component
public interface PartnerMapper {
	List<Partner> selectByExample(PartnerExample example);
}

service层代码:

public List<Partner> getAllPartnerList() {
        PartnerExample example = new PartnerExample();
        return  partnerMapper.selectByExample(example);
    }

甲同学一顿操作,调用查询所有商户方法getAllPartnerList()每次返回的结果都不一样,和预期返回所有的商户不一致,甲同学苦恼了好久并没有发现问题,后面我给他指点指点问题就找到了,首先解决问题的关键就是把mybatis执行的sql语句打印出来就能发现其中的奥秘,在mybatis配置文件中加入

<settings>  
       <setting name="logImpl" value="STDOUT_LOGGING" />  
  </settings>

执行方法后打印sql语句

select * from usr_partner  limit 0,3000

看到结果发现原来在执行的sql语句上面自动加上了limit ,,查看我们xml配置并没有发现sql中有limit,那limit来自何处?其实是分页插件PageHelper自动加上了limit
解决办法(作用是手动清理 ThreadLocal 存储的分页参):

  • 5.0及以后版本,调用PageHelper.clearPage();
  • 低于5.0版本,调用 SqlUtil.clearLocalPage();

Mybatis PageHelper原理分析

分页插件依赖:

 		<dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.2.8</version>
        </dependency>
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper</artifactId>
            <version>4.1.6</version>
        </dependency>

mybatis-config配置

<configuration>
    <plugins>
        <!-- com.github.pagehelper为PageHelper类所在包名 -->
        <plugin interceptor="com.github.pagehelper.PageHelper">
            <property name="dialect" value="mysql" />
        </plugin>
    </plugins>
</configuration>

使用分页插件:

InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = factory.openSession();
PageHelper.startPage(0, 3);
session.selectList("com.xxx.xxxx.xxx.dao.PartnerMapper.selectByExample");

通过build()入口分析

加载文件配置
 public SqlSessionFactory build(InputStream inputStream) {
        return this.build((InputStream)inputStream, (String)null, (Properties)null);
    }

继续看build方法

   public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
        SqlSessionFactory var5;
        try {
            XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
            var5 = this.build(parser.parse());
        } catch (Exception var14) {
            throw ExceptionFactory.wrapException("Error building SqlSession.", var14);
        } finally {
          ...................
        }
        return var5;
    }

继续分析XMLConfigBuilder的parse方法,看看config.xml文件是如何加载进去

public Configuration parse() {
        if (this.parsed) {
            throw new BuilderException("Each XMLConfigBuilder can only be used once.");
        } else {
            this.parsed = true;
            //先解析config.xml的root节点configuration
            this.parseConfiguration(this.parser.evalNode("/configuration"));
            return this.configuration;
        }
    }
    //获取到configuration节点后再解析config.xml中的其他结点
    private void parseConfiguration(XNode root) {
        try {
            this.propertiesElement(root.evalNode("properties"));
            this.typeAliasesElement(root.evalNode("typeAliases"));
            //解析插件
            this.pluginElement(root.evalNode("plugins"));
            this.objectFactoryElement(root.evalNode("objectFactory"));
            this.objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
            this.settingsElement(root.evalNode("settings"));
            this.environmentsElement(root.evalNode("environments"));
            this.databaseIdProviderElement(root.evalNode("databaseIdProvider"));
            this.typeHandlerElement(root.evalNode("typeHandlers"));
            this.mapperElement(root.evalNode("mappers"));
        } catch (Exception var3) {
            throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + var3, var3);
        }
    }

分页插件使用需要配置mybatis-config.xml的plugins,这里继续分析如何加载我们配置的分页插件

  private void pluginElement(XNode parent) throws Exception {
        if (parent != null) {
            Iterator i$ = parent.getChildren().iterator();

            while(i$.hasNext()) {
                XNode child = (XNode)i$.next();
                //我们在配置文件中添加了
                //<plugin interceptor="com.github.pagehelper.PageHelper">
          		//<property name="dialect" value="mysql" />
        		//</plugin>配置,这里可以读取到
                String interceptor = child.getStringAttribute("interceptor");
                //获取<property name="dialect" value="mysql" />等属性
                Properties properties = child.getChildrenAsProperties();
                //通过反射创建Interceptor 实例对象
                Interceptor interceptorInstance = (Interceptor)this.resolveClass(interceptor).newInstance();
                //设置属性
                interceptorInstance.setProperties(properties);
                //添加拦截器
                this.configuration.addInterceptor(interceptorInstance);
            }
        }

    }

继续分析addInterceptor方法

public void addInterceptor(Interceptor interceptor) {
        this.interceptorChain.addInterceptor(interceptor);
    }

public class InterceptorChain {
    private final List<Interceptor> interceptors = new ArrayList();

    public InterceptorChain() {
    }

    public void addInterceptor(Interceptor interceptor) {
        this.interceptors.add(interceptor);
    }
    public List<Interceptor> getInterceptors() {
        return Collections.unmodifiableList(this.interceptors);
    }
}
到这里就是把配置的分页插件添加到拦截器链中

通过上面分析发现PageHelper是一个拦截器,查看一下源码

Intercepts({@Signature(
    type = Executor.class,
    method = "query",
    args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
)})
public class PageHelper implements Interceptor {
    private SqlUtil sqlUtil;
    private Properties properties;
    private Boolean autoDialect;

    public PageHelper() {
    }
    .......
}

可以发现拦截的主要是Executor类的query方法,query参数主要有MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class,Executor又来自何方呢,继续分析SqlSession session = factory.openSession();

public SqlSession openSession() {
        return this.openSessionFromDataSource(this.configuration.getDefaultExecutorType(), (TransactionIsolationLevel)null, false);
    }
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
        Transaction tx = null;
        DefaultSqlSession var8;
     		............只看核心,其余代码省略..............
            Environment environment = this.configuration.getEnvironment();
            TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment);
            tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
            //拦截的Executor 在这里生成的
            Executor executor = this.configuration.newExecutor(tx, execType);
            var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);
			............只看核心,其余代码省略..............

    }

进入newExecutor方法

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
        executorType = executorType == null ? this.defaultExecutorType : executorType;
        executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
        Object executor;
  	 	............只看核心,其余代码省略..............
        Executor executor = (Executor)this.interceptorChain.pluginAll(executor);
        return executor;
    }
//这个方法其实在加载plugin的InterceptorChain 的方法,addInterceptor方法作用是加载拦截器到拦截器调用链,
//pluginAll方法是遍历拦截器链中的拦截器,调用拦截器的plugin方法,这里只关注PageHelper中的plugin方法
public Object pluginAll(Object target) {
        Interceptor interceptor;
        for(Iterator i$ = this.interceptors.iterator(); i$.hasNext(); target = interceptor.plugin(target)) {
            interceptor = (Interceptor)i$.next();
        }
        return target;
    }

继续分析PageHelper中的plugin方法

 public Object plugin(Object target) {
 		//如果是需要拦截的Executor对象实例则调用wrap方法
        return target instanceof Executor ? Plugin.wrap(target, this) : target;
    }
    
public static Object wrap(Object target, Interceptor interceptor) {
        Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
        Class<?> type = target.getClass();
        Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
        return interfaces.length > 0 ? Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)) : target;
    }

通过JDK的动态代理生成增强的Executor对象

了解动态代理的应该清楚当执行拦截的方法时就会调用InvocationHandler的invoke方法,Plugin实现了InvocationHandler的invoke方法,继续查invoke方法会发现
 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            Set<Method> methods = (Set)this.signatureMap.get(method.getDeclaringClass());
            //拦截的方法执行前会调用interceptor.intercept方法也就是PageHelper的intercept()方法
            return methods != null && methods.contains(method) ? this.interceptor.intercept(new Invocation(this.target, method, args)) : method.invoke(this.target, args);
        } catch (Exception var5) {
            throw ExceptionUtil.unwrapThrowable(var5);
        }
    }

其实这里就是一个拦截器,所以当执行Executor的query的方法并且参数是MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class类型时就会执行拦截方法intercept也就是PageHelper的intercept方法,继续看PageHelper的intercept方法实现

 public Object intercept(Invocation invocation) throws Throwable {
       	............只看核心,其余代码省略..............
        return this.sqlUtil.processPage(invocation);
    }

public Object processPage(Invocation invocation) throws Throwable {
        Object var3;
        try {
            Object result = this._processPage(invocation);
            var3 = result;
        } finally {
            clearLocalPage();
            OrderByHelper.clear();
        }
        return var3;
    }

先看看参数invocation的值
在这里插入图片描述
在这里插入图片描述
可以看到Invocation类的3个属性值主要包括执行器为SimpleExecutor、查询方法query、Mapper.xml位置、Mapper.xml中定义的方法、sql语句等,继续查看_processPage方法实现

 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];
        //如果设置的page分页参数为空或者不支持分页时跳过
        if (this.supportMethodsArguments && page == null || !this.supportMethodsArguments && getLocalPage() == null && rowBounds == RowBounds.DEFAULT) {
            return invocation.proceed();
        } else {
            if (!this.supportMethodsArguments && page == null) {
                page = this.getPage(args);
            }
			
            return this.doProcessPage(invocation, page, args);
        }
    }

继续跟踪doProcessPage方法

private Page doProcessPage(Invocation invocation, Page page, Object[] args) throws Throwable {
      
        ............只看核心,其余代码省略..............
        //判断page的条件
       if (page.getPageSize() > 0 && (rowBounds == RowBounds.DEFAULT && page.getPageNum() > 0 || rowBounds != RowBounds.DEFAULT)) {
                page.setCountSignal((Boolean)null);
                //获取sql
                BoundSql boundSql = ms.getBoundSql(args[1]);
                
                args[1] = this.parser.setPageParameter(ms, args[1], boundSql, page);
                page.setCountSignal(Boolean.FALSE);
                Object result = invocation.proceed();
                page.addAll((List)result);
            }
        return page;
    }

继续跟踪getBoundSql方法

public BoundSql getBoundSql(Object parameterObject) {
        Boolean count = this.getCount();
        if (count == null) {
            return this.getDefaultBoundSql(parameterObject);
        } else {
        	//如果查询总数不为0掉用此方法
            return count ? this.getCountBoundSql(parameterObject) : this.getPageBoundSql(parameterObject);
        }
    }

跟踪getPageBoundSql方法

protected BoundSql getPageBoundSql(Object parameterObject) {
       ............只看核心,其余代码省略..............
		//这个就是去获取执行的sql语句 tempSql为mapper.xml中编写的
        tempSql = ((Parser)localParser.get()).getPageSql(tempSql);
        return new BoundSql(this.configuration, tempSql, ((Parser)localParser.get()).getPageParameterMapping(this.configuration, this.original.getBoundSql(parameterObject)), parameterObject);
    }

跟踪getPageSql方法发现了新大陆,在待执行的语句后面加上了limit ??拼接新的sql语句通过jdbc执行获取分页结果

public String getPageSql(String sql) {
        StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
        sqlBuilder.append(sql);
        sqlBuilder.append(" limit ?,?");
        return sqlBuilder.toString();
    }

到此,PageHelper实现原理就分析完成,最新版本的源码略有修改,可以自行阅读。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值