springboot整合PageHelper,本地內置tomcat測試正常,部署到服務器獨立tomcat出現“多個分頁插件”錯誤

這次經歷是個很簡單的事情,但是確實又影響了開發人員很久,今天抽時間一步步復現了當時的問題,記錄一下。

 

目錄

1、基本問題

2、錯誤信息

3、初步分析與驗證

4、根源探究

5、從SpringBootServletInitializer源碼看

6、從PageHelper插件源碼看

7、另一個問題:爲什麼取消自動配置後又出現分頁失效的問題呢?

8、關於PageHelper,還有哪些注意事項?



1、基本問題

先看項目結構:

MytestApplication:加了註解@SpringBootApplication,實現一個main方法,作爲啓動類用於內置tomcat的啓動;

 

SpringBootStartApplication:繼承了SpringBootServletInitializer,重寫configure方法,war包部署獨立tomcat用;


2、錯誤信息

當時項目組部署遇到的一個問題,本地測試正常,部署後就會出現這個錯誤:

2018-12-05 12:39:55.005 ERROR org.springframework.boot.web.servlet.support.ErrorPageFilter Line:190 - Forwarding to error page from request [/test/users/1/1/2] due to exception [nested exception is org.apache.ibatis.exceptions.PersistenceException:
### Error querying database.  Cause: java.lang.RuntimeException: 在系統中發現了多個分頁插件,請檢查系統配置!
### Cause: java.lang.RuntimeException: 在系統中發現了多個分頁插件,請檢查系統配置!]
org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException:
### Error querying database.  Cause: java.lang.RuntimeException: 在系統中發現了多個分頁插件,請檢查系統配置!
### Cause: java.lang.RuntimeException: 在系統中發現了多個分頁插件,請檢查系統配置!

報錯還原如下:


3、初步分析與驗證

        發現測試環境和生產環境基本一致,唯一不同是本地測試用的是springboot的內置tomcat啓動的,而生產環境是打的WAR包,運行在獨立tomcat之下的。

        用內置tomcat啓動類啓動或者打jar包運行也沒有問題,但是打war包部署到獨立tomcat就會出現這個報錯。那就要看看有什麼區別,但是項目組當時由於經驗不足,又緊跟着犯了一個錯誤,導致新的問題出現。

       項目組找到一個解決辦法:在MytestApplication 類註解中加上

       @SpringBootApplication(exclude=PageHelperAutoConfiguration.class)

        然後在配置文件再配置:(這裏留一個疑問,後面再談

#配置分頁插件pagehelper
pagehelper.helper-dialect=mysql
pagehelper.reasonable=true
pagehelper.supportMethodsArguments=true
pagehelper.pageSizeZero=true
pagehelper.params=count=countSql

       取消了自動配置,但是新的問題出現了:分頁請求不再報錯了,但是分頁失效了,也就是沒有分頁了,每次請求不管page、rows是什麼,都會返回整個表的數據;

      


4、根源探究

     實際上報錯原因是MytestApplication 同樣繼承了SpringBootServletInitializer,重寫了configure方法,而且

  builder.sources(MytestApplication.class);兩個重寫的方法加了同一個啓動類,也就是說,重複了,這一點在啓動tomcat的時候其實已經顯現出來了,只是當時的人沒有注意到。我們看下:明明是一個項目,怎麼啓動的時候變成兩個了呢?就是這個原因。


5、從SpringBootServletInitializer源碼看

我們來看下SpringBootServletInitializer中的註釋,這段話大概是說,如果要打war包部署到外置獨立tomcat中,需要繼承這個類,

 而configure方法就是要指定啓動資源,

 

 兩個類都繼承SpringBootServletInitializer並重寫了configure方法,而且都指定了同一個資源,也就MytestApplication.class,所以啓動的時候實際上加載了兩次這個資源,兩次都自動配置了pagehelper插件,


6、從PageHelper插件源碼看

我們再進一步看下原因:

看第二張報錯圖片中紅框部分,我們發現,有兩個跟PageHelper相關的方法,查看下報錯位置,是在PageHelper類中的skip方法中:

 原來是在這個攔截器中調用的這個skip方法,那這個攔截器又是什麼時候註冊的呢?

public class PageInterceptor implements Interceptor {
    //...
    private String countSuffix = "_COUNT";
 
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        try {
            //...
 
            //調用方法判斷是否需要進行分頁,如果不需要,直接返回結果
            if (!dialect.skip(ms, parameter, rowBounds)) {
                //反射獲取動態參數
                String msId = ms.getId();
                Configuration configuration = ms.getConfiguration();
                Map<String, Object> additionalParameters = (Map<String, Object>) additionalParametersField.get(boundSql);
                //判斷是否需要進行 count 查詢
                if (dialect.beforeCount(ms, parameter, rowBounds)) {
                    String countMsId = msId + countSuffix;
                    Long count;
                    //先判斷是否存在手寫的 count 查詢
                    MappedStatement countMs = getExistedMappedStatement(configuration, countMsId);
                    if(countMs != null){
                        count = executeManualCount(executor, countMs, parameter, boundSql, resultHandler);
                    } else {
                        countMs = msCountMap.get(countMsId);
                        //自動創建
                        if (countMs == null) {
                            //根據當前的 ms 創建一個返回值爲 Long 類型的 ms
                            countMs = MSUtils.newCountMappedStatement(ms, countMsId);
                            msCountMap.put(countMsId, countMs);
                        }
                        count = executeAutoCount(executor, countMs, parameter, boundSql, rowBounds, resultHandler);
                    }
                    //處理查詢總數
                    //返回 true 時繼續分頁查詢,false 時直接返回
                    if (!dialect.afterCount(count, parameter, rowBounds)) {
                        //當查詢總數爲 0 時,直接返回空的結果
                        return dialect.afterPage(new ArrayList(), parameter, rowBounds);
                    }
                }
                //判斷是否需要進行分頁查詢
                if (dialect.beforePage(ms, parameter, rowBounds)) {
                    //生成分頁的緩存 key
                    CacheKey pageKey = cacheKey;
                    //處理參數對象
                    parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey);
                    //調用方言獲取分頁 sql
                    String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey);
                    BoundSql pageBoundSql = new BoundSql(configuration, pageSql, boundSql.getParameterMappings(), parameter);
                    //設置動態參數
                    for (String key : additionalParameters.keySet()) {
                        pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
                    }
                    //執行分頁查詢
                    resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);
                } else {
                    //不執行分頁的情況下,也不執行內存分頁
                    resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql);
                }
            } else {
                //rowBounds用參數值,不使用分頁插件處理時,仍然支持默認的內存分頁
                resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
            }
            return dialect.afterPage(resultList, parameter, rowBounds);
        } finally {
            dialect.afterAll();
        }
    }
}

我們看下自動配置PageHelperAutoConfiguration:

/**
 * 自定注入分頁插件
 *
 * @author liuzh
 */
@Configuration
@ConditionalOnBean(SqlSessionFactory.class)
@EnableConfigurationProperties(PageHelperProperties.class)
@AutoConfigureAfter(MybatisAutoConfiguration.class)
public class PageHelperAutoConfiguration {

    @Autowired
    private List<SqlSessionFactory> sqlSessionFactoryList;

    @Autowired
    private PageHelperProperties properties;

    /**
     * 接受分頁插件額外的屬性
     *
     * @return
     */
    @Bean
    @ConfigurationProperties(prefix = PageHelperProperties.PAGEHELPER_PREFIX)
    public Properties pageHelperProperties() {
        return new Properties();
    }

    @PostConstruct
    public void addPageInterceptor() {
        PageInterceptor interceptor = new PageInterceptor();
        Properties properties = new Properties();
        //先把一般方式配置的屬性放進去
        properties.putAll(pageHelperProperties());
        //在把特殊配置放進去,由於close-conn 利用上面方式時,屬性名就是 close-conn 而不是 closeConn,所以需要額外的一步
        properties.putAll(this.properties.getProperties());
        interceptor.setProperties(properties);
        for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
            sqlSessionFactory.getConfiguration().addInterceptor(interceptor);
        }
    }

應該是兩次自動配置加入了兩個攔截器PageInterceptor,所以纔會有開頭的時候的報錯“多個插件”。  換句話說,如果當時犯錯的人在SpringBootStartApplication中配置的資源不是MytestApplication.class,而是SpringBootStartApplication.class,同樣不會出現這個報錯。(這裏僅說明問題,而不是建議兩個都繼承並重寫,本身就是錯誤的。)

(關於PageHelper源碼的分析,下次專門研究下寫出來)


7、另一個問題:爲什麼取消自動配置後又出現分頁失效的問題呢?

是因爲MytestApplication 去掉了PageHelperAutoConfiguration.class,即不讓PageHelper自動配置。

所以MytestApplication 和 SpringBootStartApplication 都加入這個資源,是一樣的,都排除了自動配置,所以失效了。

而此時開發組實際上又犯了另一個錯誤,即加上exclude=PageHelperAutoConfiguration.class之後,想當然地認爲在application.properties中加入配置就可以。

事實上,既然已經不允許自動配置,又怎麼會主動去配置文件讀取配置信息呢?  這裏完全是個誤區!

爲了說明這一點,我們看一下自動配置實現,實際上是讀取了配置文件中的以pagehelper開頭的所有配置項:


8、關於PageHelper,還有哪些注意事項?

(下面的暫時沒有驗證過,是網上的常見說法,留着後面有時間再來看)

1、PageHelper.startPage(1,10);只對該語句以後的第一個查詢語句得到的數據進行分頁,

就算你在PageInfo pa = new PageInfo("",對象);語句裏面的對象是寫的最終得到的數據,該插件還是隻會對第一個查詢所查詢出來的數據進行分頁

第一個查詢語句是指什麼呢?舉個例子吧,比如你有一個查詢數據的方法,寫在了PageHelper.startPage(1, 10);下面.但是這個查詢方法裏面
包含兩個查詢語句的話,該插件就只會對第一查詢語句查詢的數據進行分頁,而不是對返回最終數據的查詢與基礎查詢出來的數據進行分頁

改變一下自己的代碼結構,讓最終需要的數據所需要的查詢語句放在PageHelper.startPage(1, 10)下面就行
 

2、mybatis-spring-boot-starter版本太低會導致失效,1.1.1及以後才支持的這種攔截器插件;

3、重新定義了SqlSessionFactory,但是並沒有配置對應的PageHelper插件,導致分頁失效;

(網上找的,下面代碼有錯,僅作爲參考記錄一下,)

    @Bean

    public SqlSessionFactorysqlSessionFactoryBean(DataSourcedataSource)throws Exception {

       SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();

       sqlSessionFactoryBean.setDataSource(dataSource);

       PathMatchingResourcePatternResolver resolver =newPathMatchingResourcePatternResolver();

       Interceptor[] plugins =  newInterceptor[]{pageHelper()};

       sqlSessionFactoryBean.setPlugins(plugins);

       // 指定mybatisxml文件路徑

      sqlSessionFactoryBean.setMapperLocations(resolver

              .getResources("classpath:/mybatis/*.xml"));

       returnsqlSessionFactoryBean.getObject();

    }

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章