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實現原理就分析完成,最新版本的源碼略有修改,可以自行閱讀。

發佈了48 篇原創文章 · 獲贊 35 · 訪問量 16萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章