1. 程式人生 > 其它 >PageHelper不安全的分頁問題,導致ParserException: syntax error, error in :'it 1 LIMIT ? ', expect LIMIT, actual LIMIT pos , line , column , token LIMIT

PageHelper不安全的分頁問題,導致ParserException: syntax error, error in :'it 1 LIMIT ? ', expect LIMIT, actual LIMIT pos , line , column , token LIMIT

背景

專案中使用PageHlper外掛進行分頁,今日發現有多處SQL查詢語句都出現瞭如下的報錯。

com.alibaba.druid.sql.parser.ParserException: syntax error, error in :'it 1 LIMIT ? ', expect LIMIT, actual LIMIT pos 249, line 12, column 16, token LIMIT
at com.alibaba.druid.sql.parser.SQLParser.printError(SQLParser.java:284)
at com.alibaba.druid.sql.parser.SQLStatementParser.parseStatementList(
at com.alibaba.druid.sql.parser.SQLStatementParser.parseStatementList(
at com.alibaba.druid.sql.SQLUtils.format(SQLUtils.java:255)
at com.alibaba.druid.filter.logging.LogFilter.statement_executeErrorAfter(LogFilter.java:767)
at com.alibaba.druid.filter.FilterEventAdapter.preparedStatement_execute(
at com.alibaba.druid.filter.FilterChainImpl.preparedStatement_execute(FilterChainImpl.java:3407)
at com.alibaba.druid.filter.FilterEventAdapter.preparedStatement_execute(
at com.alibaba.druid.filter.FilterChainImpl.preparedStatement_execute(FilterChainImpl.java:3407)
at com.alibaba.druid.proxy.jdbc.PreparedStatementProxyImpl.execute(PreparedStatementProxyImpl.java:167)
at com.alibaba.druid.pool.DruidPooledPreparedStatement.execute(DruidPooledPreparedStatement.java:498)
at org.apache.ibatis.executor.statement.PreparedStatementHandler.query(PreparedStatementHandler.java:63)
at org.apache.ibatis.executor.statement.RoutingStatementHandler.query(RoutingStatementHandler.java:79)
at org.apache.ibatis.executor.SimpleExecutor.doQuery(SimpleExecutor.java:63)
at org.apache.ibatis.executor.BaseExecutor.queryFromDatabase(BaseExecutor.java:324)
at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:156)
at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:109)
at com.github.pagehelper.PageInterceptor.intercept(PageInterceptor.java:136)
at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:61)
at com.sun.proxy.$Proxy467.query(Unknown Source)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:148)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:141)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectOne(DefaultSqlSession.java:77)
at sun.reflect.GeneratedMethodAccessor239.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:433)
at com.sun.proxy.$Proxy137.selectOne(Unknown Source)
at org.mybatis.spring.SqlSessionTemplate.selectOne(SqlSessionTemplate.java:166)
at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:82)
at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:59)
at com.sun.proxy.$Proxy243.getOneSomthing(Unknown Source)
at com.lingyejun.project.impl.GetOneThingServiceImpl.getOneThingFromDb(GetOneThingServiceImpl.java:23)

調查

我們檢視堆疊資訊發現有一行關鍵資訊,在進行查詢的的時候使用的PageHelper進行了攔截。

at com.github.pagehelper.PageInterceptor.intercept(PageInterceptor.java:136)

但是看SQL語句並未發現有分頁的程式碼,而且報錯的不止這一處,還有其他幾個地方,檢視提交記錄發現最近都沒有改動。

我們想到那肯定是因為其他地方有改動導致的。調查原因後發現有一處程式碼在呼叫了PageHelper.startPage後直接返回了,導致的報錯,大致程式碼如下。

package com.lingyejun.authenticator;
import com.github.pagehelper.PageHelper;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;

@Service
public class PageHelperTest {

    @Resource
    private TeacherMapper teacherMapper;

    @Resource
    private StudentMapper studentMapper;

    public void doQueryByPage(byte type) {
        PageHelper.startPage(1,10);

        if (type == 1){
            studentMapper.query();
        }else if (type ==2){
            teacherMapper.query();
        }
        // 如果type不是1或者2那麼此方法執行完是沒有釋放和清理page變數
        // 會導致其他地方的查詢語句報錯,或者結果與預期不符
        return;
    }

}

原理

PageHelper 方法使用了靜態的 ThreadLocal 引數,分頁引數和執行緒是繫結的。只要我們保證在 PageHelper 方法呼叫後緊跟 MyBatis 查詢方法,這就是安全的。因為 PageHelper 在 finally 程式碼段中自動清除了 ThreadLocal 儲存的物件。

一次PageHelper的分頁過程如下

  1. 設定 page 引數
  2. 執行 query 方法
  3. Interceptor 介面 中校驗 ThreadLocal 中是否存在有設定的 page 引數
  4. 存在 page 引數,重新生成 count sql 和 page sql,並執行查詢。不存在 page 引數,直接返回 查詢結果
  5. 執行 LOCAL_PAGE.remove() 清除 page 引數

但是如果使用執行緒池的話,當前執行緒執行完畢,並不會被銷燬,而是會將當前執行緒再次存放到池中,標記為空閒狀態,以便後續使用。在後續使用這個執行緒的時候,由於 執行緒 的 threadLocals 依舊存在有值,儘管我們在第 1 步時未設定 page 引數,第 3 步 的也能獲取到page引數,從而生成 count sql 和 page sql,從而影響我們的正常查詢。

解決

以上問題屬於人為bug,沒有考慮到type為其他值的情況,即出現else時缺少後續邏輯處理,會導致 PageHelper 生產了一個分頁引數,但是沒有被消費,這個引數就會一直保留在這個執行緒上。當這個執行緒再次被使用時,就可能導致不該分頁的方法去消費這個分頁引數,這就產生了莫名其妙的分頁。所以我們把對應的邏輯進行調整修改即可, 將else if改成else即可解決這個問題。

本篇文章如有幫助到您,請給「翎野君」點個贊,感謝您的支援。

原文連結:https://www.cnblogs.com/lingyejun/p/15675208.html

作者:翎野君
出處:http://www.cnblogs.com/lingyejun/
若本文如對您有幫助,不妨點選一下右下角的【推薦】。
如果您喜歡或希望看到更多我的文章,可掃描二維碼關注我的微信公眾號《翎驛》。
轉載文章請務必保留出處和署名,否則保留追究法律責任的權利。