SpringBoot源码实用场景:SpringBoot 3.1.0 环境下 PageHelper 1.4.0不生效问题排查

发布时间 2023-08-09 16:30:17作者: 三板斧-Howie

1、技术栈:

  JDK 17 + SpringBoot 3.1.0 + PageHelper 1.4.0

 1 <?xml version="1.0" encoding="UTF-8"?>
 2 <project ...>
 3     <parent>
 4         <groupId>org.springframework.boot</groupId>
 5         <artifactId>spring-boot-starter-parent</artifactId>
 6         <version>3.1.0</version>
 7         <relativePath/>
 8     </parent>
 9     ...
10     <properties>
11         <java.version>17</java.version>
12         <pagehelper.boot.version>1.4.0</pagehelper.boot.version>
13         ...
14     </properties>
15 
16     <dependencyManagement>
17         <dependencies>
18             <!-- pagehelper 分页插件 -->
19             <dependency>
20                 <groupId>com.github.pagehelper</groupId>
21                 <artifactId>pagehelper-spring-boot-starter</artifactId>
22                 <version>${pagehelper.boot.version}</version>
23             </dependency>
24         </dependencies>
25         ...
26     </dependencyManagement>
27 </project>
View Code

 

2、异常表现:

  PageHelper 分页拦截器不生效

3、解决方案:

  将 pagehelper-spring-boot-starter 版本由 1.4.0 升级到 1.4.7 (写这篇博客时的最新版)

4、源码分析:

  分析、解决这个bug需要对PageHelper源码、SpringBoot源码有一定的了解。

  Step1: 首先分页插件PageHelper的本质是mybatis拦截器实现,可以大胆推测其核心类是一个Intercepter, 即 PageIntercepter.class 

1 public class PageInterceptor implements org.apache.ibatis.plugin.Interceptor {
2 ...
3 }
View Code

  Step2: 在 PageIntercepter 核心方法 intercept() 上打断点,debug发现代码运行时没有经过此处断点,推测可能是该 PageIntercepter 没有被创建,或者创建出的对象没有被绑定到 SqlSessionFactory

1     @Override
2     public Object intercept(Invocation invocation) throws Throwable {}
View Code

  Step3: 使用Idea分析 PageIntercepter 的构造方法,发现 PageIntercepter 的构造方法在 PageHelperAutoConfiguration 类的 afterPropertiesSet()  方法中被调用,在 PageHelperAutoConfiguration.afterPropertiesSet() 方法上打断点发现运行时逻辑也没有执行到当前断点

 1 @Configuration
 2 @ConditionalOnBean(SqlSessionFactory.class)
 3 @EnableConfigurationProperties({PageHelperProperties.class, PageHelperStandardProperties.class})
 4 @AutoConfigureAfter(MybatisAutoConfiguration.class)
 5 //@Import(PageHelperProperties.class)
 6 @Lazy(false)
 7 public class PageHelperAutoConfiguration implements InitializingBean {
 8     // ...
 9     @Override
10     public void afterPropertiesSet() throws Exception {
11         PageInterceptor interceptor = new PageInterceptor();
12         interceptor.setProperties(this.properties);
13         for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
14             org.apache.ibatis.session.Configuration configuration = sqlSessionFactory.getConfiguration();
15             if (!containsInterceptor(configuration, interceptor)) {
16                 configuration.addInterceptor(interceptor);
17             }
18         }
19     }
20 }
View Code

  Step4: 继续分析 PageHelperAutoConfiguration 源码,发现其被 @Configuration 注解标识,且在 com.github.pagehelper.autoconfigure 包下,由此判断 PageHelperAutoConfiguration 是一个被SpringBoot自动装配的类,且 PageHelperAutoConfiguration 被 @ConditionalOnBean(SqlSessionFactory.class)  注解标识

  Step5: 在 SpringBootApplication 启动类的main方法结尾处打断点, 发现SpringContext中实际是存在SqlSessionFactory Bean的,由此推断问题出现在SpringBoot自动装配阶段,在AutoConfigurationImportSelector.getCandidateConfigurations() 方法内打断点,确认PageHelperAutoConfiguration不在待加载的configurations

  Step6: 回到SpringBoot源码,@SpringBootApplication -> @EnableAutoConfiguration -> @Import(AutoConfigurationImportSelector.class)

-> AutoConfigurationImportSelector.getCandidateConfigurations() ->ImportCandidates.load() 方法, 发现 ImportCandidates.LOCATION = "META-INF/spring/%s.imports" (SpringBootStarter 最新版)

  Step7: 查看 com.github.pagehelper.autoconfigure-1.4.0.jar下 /META-INF 文件夹 发现 EnableAutoConfiguration配置信息放在 spring.factories 文件内(SpringBootStarter 旧版本支持),与Step6中的ImportCandidates.LOCATION不匹配,导致 PageHelperAutoConfiguration类未被装载

 1 public final class ImportCandidates implements Iterable<String> {
 2 
 3     private static final String LOCATION = "META-INF/spring/%s.imports";
 4 
 5     public static ImportCandidates load(Class<?> annotation, ClassLoader classLoader) {
 6         Assert.notNull(annotation, "'annotation' must not be null");
 7         ClassLoader classLoaderToUse = decideClassloader(classLoader);
 8         String location = String.format(LOCATION, annotation.getName());
 9         
10         Enumeration<URL> urls = findUrlsInClasspath(classLoaderToUse, location);
11         List<String> importCandidates = new ArrayList<>();
12         while (urls.hasMoreElements()) {
13             URL url = urls.nextElement();
14             importCandidates.addAll(readCandidateConfigurations(url));
15         }
16         return new ImportCandidates(importCandidates);
17     }
18 }
View Code

  Step8: 在 maven 中央仓库中查找pagehelper最新版本(1.4.7),升级pagehelper依赖版本到最新版,检查com.github.pagehelper.autoconfigure-1.4.0.jar下/META-INF 文件夹/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件,与ImportCandidates.LOCATION匹配! 重新测试代码,发现PageHelperAutoConfiguration装载成功,分页功能正常。

5、心得感悟:

  1、“内事不决问百度,外事不决问谷歌”, 出于赶项目工期等原因,一般在开发过程中遇到bug首先会去网上看有没有人遇到过同样的问题,有的话可以直接参考其原因及解决方案,没有的话再去打断点、分析源码

  2、To Be Honest, 对于各个框架的源码,我的学习方式也是直接在B站上先跟着专门做相关培训的老师后面先看一遍,因为他们投入了大量的时间整理出了阅读源码的核心思路,然后自己再参照视频实际看一下相关的源码,留一个大致的映像,在需要用到源码知识解决问题的时候再去源码中debug,有一种“且放白鹿青崖间,须行即骑访名山”的意思~