這次經歷是個很簡單的事情,但是確實又影響了開發人員很久,今天抽時間一步步復現了當時的問題,記錄一下。
目錄
5、從SpringBootServletInitializer源碼看
7、另一個問題:爲什麼取消自動配置後又出現分頁失效的問題呢?
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();
}