这次经历是个很简单的事情,但是确实又影响了开发人员很久,今天抽时间一步步复现了当时的问题,记录一下。
目录
1、基本问题
2、错误信息
3、初步分析与验证
4、根源探究
5、从SpringBootServletInitializer源码看
6、从PageHelper插件源码看
7、另一个问题:为什么取消自动配置后又出现分页失效的问题呢?
8、关于PageHelper,还有哪些注意事项?
先看项目结构:
MytestApplication:加了注解@SpringBootApplication,实现一个main方法,作为启动类用于内置tomcat的启动;
SpringBootStartApplication:继承了SpringBootServletInitializer,重写configure方法,war包部署独立tomcat用;
当时项目组部署遇到的一个问题,本地测试正常,部署后就会出现这个错误:
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: 在系统中发现了多个分页插件,请检查系统配置!
报错还原如下:
发现测试环境和生产环境基本一致,唯一不同是本地测试用的是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是什么,都会返回整个表的数据;
实际上报错原因是MytestApplication 同样继承了SpringBootServletInitializer,重写了configure方法,而且
builder.sources(MytestApplication.class);两个重写的方法加了同一个启动类,也就是说,重复了,这一点在启动tomcat的时候其实已经显现出来了,只是当时的人没有注意到。我们看下:明明是一个项目,怎么启动的时候变成两个了呢?就是这个原因。
从SpringBootServletInitializer源码看
我们来看下SpringBootServletInitializer中的注释,这段话大概是说,如果要打war包部署到外置独立tomcat中,需要继承这个类,
而configure方法就是要指定启动资源,
两个类都继承SpringBootServletInitializer
并重写了configure方法,而且都指定了同一个资源,也就MytestApplication.class,所以启动的时候实际上加载了两次这个资源,两次都自动配置了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 additionalParameters = (Map) 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 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源码的分析,下次专门研究下写出来)
是因为MytestApplication 去掉了PageHelperAutoConfiguration.class,即不让PageHelper自动配置。
所以MytestApplication 和 SpringBootStartApplication 都加入这个资源,是一样的,都排除了自动配置,所以失效了。
而此时开发组实际上又犯了另一个错误,即加上exclude=PageHelperAutoConfiguration.class之后,想当然地认为在application.properties中加入配置就可以。
事实上,既然已经不允许自动配置,又怎么会主动去配置文件读取配置信息呢? 这里完全是个误区!
为了说明这一点,我们看一下自动配置实现,实际上是读取了配置文件中的以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();
}