近日在項目中使用SpringBoot集成PageHelper后,跑單元測試時出現了“在系統中發現了多個分頁插件,請檢查系統配置!”這個問題。
如下圖所示:
-
org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException:
-
### Error querying database. Cause: java.lang.RuntimeException: 在系統中發現了多個分頁插件,請檢查系統配置!
-
### Cause: java.lang.RuntimeException: 在系統中發現了多個分頁插件,請檢查系統配置!
-
-
at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:77)
-
at org.mybatis.spring.SqlSessionTemplate
$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:446)
-
at com.sun.proxy.
$Proxy109.selectList(Unknown Source)
-
at org.mybatis.spring.SqlSessionTemplate.selectList(SqlSessionTemplate.java:230)
-
at org.apache.ibatis.binding.MapperMethod.executeForMany(MapperMethod.java:137)
-
at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:75)
-
at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:59)
-
at com.sun.proxy.
$Proxy110.selectAll(Unknown Source)
-
at com.lianjia.cto.ke.broadband.service.StationInfoService.selectPage(StationInfoService.java:27)
-
at com.lianjia.cto.ke.broadband.service.StationInfoService$
$FastClassBySpringCGLIB$
$ee2f34a4.invoke(<generated>)
-
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)
-
at org.springframework.aop.framework.CglibAopProxy
$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:738)
-
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157)
-
at org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor.invoke(MethodBeforeAdviceInterceptor.java:52)
-
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:168)
-
at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92)
-
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
-
at org.springframework.aop.framework.CglibAopProxy
$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:673)
-
at com.lianjia.cto.ke.broadband.service.StationInfoService$
$EnhancerBySpringCGLIB$
$1da3a0f3.selectPage(<generated>)
-
at com.lianjia.cto.ke.broadband.service.StationInfoServiceTest.selectPage(StationInfoServiceTest.java:27)
-
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
-
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
-
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
-
at java.lang.reflect.Method.invoke(Method.java:498)
-
at org.junit.runners.model.FrameworkMethod
$1.runReflectiveCall(FrameworkMethod.java:50)
-
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
-
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
-
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
-
at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
-
at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
-
at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
-
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
-
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:252)
-
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:94)
-
at org.junit.runners.ParentRunner
$3.run(ParentRunner.java:290)
-
at org.junit.runners.ParentRunner
$1.schedule(ParentRunner.java:71)
-
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
-
at org.junit.runners.ParentRunner.access
$000(ParentRunner.java:58)
-
at org.junit.runners.ParentRunner
$2.evaluate(ParentRunner.java:268)
-
at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
-
at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
-
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
-
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:191)
-
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
-
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
-
at com.intellij.rt.execution.junit.IdeaTestRunner
$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
-
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
-
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
-
Caused by: org.apache.ibatis.exceptions.PersistenceException:
-
### Error querying database. Cause: java.lang.RuntimeException: 在系統中發現了多個分頁插件,請檢查系統配置!
-
### Cause: java.lang.RuntimeException: 在系統中發現了多個分頁插件,請檢查系統配置!
-
at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30)
-
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:150)
-
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:141)
-
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
-
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
-
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)
-
... 46 more
-
Caused by: java.lang.RuntimeException: 在系統中發現了多個分頁插件,請檢查系統配置!
-
at com.github.pagehelper.PageHelper.skip(PageHelper.java:55)
-
at com.github.pagehelper.PageInterceptor.intercept(PageInterceptor.java:92)
-
at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:61)
-
at com.sun.proxy.
$Proxy123.query(Unknown Source)
-
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
-
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
-
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
-
at java.lang.reflect.Method.invoke(Method.java:498)
-
at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:63)
-
at com.sun.proxy.
$Proxy123.query(Unknown Source)
-
at com.github.pagehelper.PageInterceptor.executeAutoCount(PageInterceptor.java:201)
-
at com.github.pagehelper.PageInterceptor.intercept(PageInterceptor.java:113)
-
at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:61)
-
at com.sun.proxy.
$Proxy123.query(Unknown Source)
-
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:148)
-
... 52 more
先上結果:maven的pagehelper-spring-boot-starter這個依賴,提供了自動配置分頁插件的功能,所以有兩種方法解決這個問題
第一種,SpringBoot啟動類的注解上排除這個自動配置
@SpringBootApplication(exclude = PageHelperAutoConfiguration.class)
第二種,在構建SqlSessionFactory時,不要再去手動添加分頁的攔截器,在application.yml中進行配置pagehelper的屬性,在SpringBoot啟動時會將配置自動注入生成攔截器
-
# PageHelper配置
-
pagehelper:
-
offsetAsPageNum:
true
-
rowBoundsWithCount:
true
-
reasonable:
true
-
returnPageInfo:
true
-
params: count=countSql
以下是解決問題的思路:
看到這個報錯后,查看了一下配置,發現並沒有配置多個PageHelper。於是查看了一下報錯部分的源碼,問題出現在一個skip方法中,如下圖所示:
-
public
class PageHelper extends PageMethod implements Dialect {
-
//...
-
-
@Override
-
public boolean skip(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {
-
//此處的MSUtils.COUNT為一個常量值"_COUNT"
-
if(ms.getId().endsWith(MSUtils.COUNT)){
-
throw
new RuntimeException(
"在系統中發現了多個分頁插件,請檢查系統配置!");
-
}
-
//...
-
}
-
-
//...
-
}
繼續向上找,找到了分頁插件的攔截器
-
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();
-
}
-
}
-
}
可以發現以_COUNT結尾的MappedStatement.id是由分頁插件的攔截器自動生成的,而這個由分頁插件的生成之后,怎么會又一次的調用skip方法呢?所以猜測是有多個分頁插件的攔截器影響到了。於是查閱了一下相關的資料,發現PageHelper引入了SpringBoot的自動配置,以下是自動配置的源碼:
-
@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);
-
}
-
}
-
-
}
至此,問題算是比較明白了,是因為多個分頁攔截器的作用,導致了該異常的出現,所以全局只要保留一個就可以了。