springboot整合PageHelper,本地内置tomcat测试正常,部署到服务器独立tomcat出现“多个分页插件”错误

这次经历是个很简单的事情,但是确实又影响了开发人员很久,今天抽时间一步步复现了当时的问题,记录一下。

 

目录

1、基本问题

2、错误信息

3、初步分析与验证

4、根源探究

5、从SpringBootServletInitializer源码看

6、从PageHelper插件源码看

7、另一个问题:为什么取消自动配置后又出现分页失效的问题呢?

8、关于PageHelper,还有哪些注意事项?



1、基本问题

先看项目结构:

springboot整合PageHelper,本地内置tomcat测试正常,部署到服务器独立tomcat出现“多个分页插件”错误_第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: 在系统中发现了多个分页插件,请检查系统配置!

报错还原如下:

springboot整合PageHelper,本地内置tomcat测试正常,部署到服务器独立tomcat出现“多个分页插件”错误_第2张图片

springboot整合PageHelper,本地内置tomcat测试正常,部署到服务器独立tomcat出现“多个分页插件”错误_第3张图片


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的时候其实已经显现出来了,只是当时的人没有注意到。我们看下:明明是一个项目,怎么启动的时候变成两个了呢?就是这个原因。

springboot整合PageHelper,本地内置tomcat测试正常,部署到服务器独立tomcat出现“多个分页插件”错误_第4张图片


5、从SpringBootServletInitializer源码看

我们来看下SpringBootServletInitializer中的注释,这段话大概是说,如果要打war包部署到外置独立tomcat中,需要继承这个类,

springboot整合PageHelper,本地内置tomcat测试正常,部署到服务器独立tomcat出现“多个分页插件”错误_第5张图片

 而configure方法就是要指定启动资源,

springboot整合PageHelper,本地内置tomcat测试正常,部署到服务器独立tomcat出现“多个分页插件”错误_第6张图片 

 两个类都继承SpringBootServletInitializer并重写了configure方法,而且都指定了同一个资源,也就MytestApplication.class,所以启动的时候实际上加载了两次这个资源,两次都自动配置了pagehelper插件,


6、从PageHelper插件源码看

我们再进一步看下原因:

看第二张报错图片中红框部分,我们发现,有两个跟PageHelper相关的方法,查看下报错位置,是在PageHelper类中的skip方法中:

springboot整合PageHelper,本地内置tomcat测试正常,部署到服务器独立tomcat出现“多个分页插件”错误_第7张图片

 原来是在这个拦截器中调用的这个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源码的分析,下次专门研究下写出来)


7、另一个问题:为什么取消自动配置后又出现分页失效的问题呢?

是因为MytestApplication 去掉了PageHelperAutoConfiguration.class,即不让PageHelper自动配置。

所以MytestApplication 和 SpringBootStartApplication 都加入这个资源,是一样的,都排除了自动配置,所以失效了。

而此时开发组实际上又犯了另一个错误,即加上exclude=PageHelperAutoConfiguration.class之后,想当然地认为在application.properties中加入配置就可以。

事实上,既然已经不允许自动配置,又怎么会主动去配置文件读取配置信息呢?  这里完全是个误区!

为了说明这一点,我们看一下自动配置实现,实际上是读取了配置文件中的以pagehelper开头的所有配置项:

springboot整合PageHelper,本地内置tomcat测试正常,部署到服务器独立tomcat出现“多个分页插件”错误_第8张图片


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();

    }

 

你可能感兴趣的:(java)