环境

JDK:17
SpringBoot:2.7.5
mybatis-plus-boot-starter:3.5.3.1
pagehelper-spring-boot-starter:1.4.1

1. 问题描述

PageHelper的分页是通过拦截器实现的,通过源码可以看到,PageHelper先是查询了总数,然后查询了数据 1676334968255.png

之后封装为Page对象,Page继承了ArrayList,包含了总数和数据结果 1676335157088.png

但因为代码规范的问题,我们的Service方法必须返回Result对象,不能直接返回查询之后的List对象。我们使用 MapStruct 来做对象间的转换,这样在对象转换后就丢失了Page中的总数。

2. 解决方法

思路:增加一个Mybatis拦截器,在PageHelper执行完分页查询之后,将Page对象保存在ThreadLocal中。

1.LocalPage,继承 Closeable,用于保存Page并能够自动清理ThreadLocal

public record LocalPage<T>(Page<T> page) implements Closeable {

    public Page<T> getPage() {
        return page;
    }

    public static <T> LocalPage<T> of(Page<T> page) {
        return new LocalPage<>(page);
    }

    @Override
    public void close() {
        LocalPageHelper.clearLocalPage();
    }
}

2.LocalPageHelper,用于封装ThreadLocal操作

public class LocalPageHelper {

    private static final ThreadLocal<LocalPage<?>> LOCAL_PAGES = new ThreadLocal<>();

    public static void setLocalPage(LocalPage<?> page) {
        LOCAL_PAGES.set(page);
    }

    public static LocalPage<?> getLocalPage() {
        return LOCAL_PAGES.get();
    }

    public static void clearLocalPage() {
        LOCAL_PAGES.remove();
    }
}

3.LocalPageInterceptor,添加一个拦截器,将LocalPage存入ThreadLocal

@Intercepts({
        @Signature(
                type = Executor.class,
                method = "query",
                args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
        ),
        @Signature(
                type = Executor.class,
                method = "query",
                args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}
        )
})
public class LocalPageInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws InvocationTargetException, IllegalAccessException {
        Object result = invocation.proceed();
        if (result instanceof Page page) {
            LocalPageHelper.setLocalPage(LocalPage.of(page));
        }
        return result;
    }

}

4.在启动类中,禁用 PageHelper 的自动配置

@SpringBootApplication(exclude = PageHelperAutoConfiguration.class)

5.MybatisConfiguration,手动配置插件,可以更好的控制插件顺序

@Configuration
@EnableConfigurationProperties(PageHelperProperties.class)
public class MybatisConfiguration {

    private final List<SqlSessionFactory> sqlSessionFactoryList;

    private final PageHelperProperties properties;

    public MybatisConfiguration(List<SqlSessionFactory> sqlSessionFactoryList, PageHelperProperties properties) {
        this.sqlSessionFactoryList = sqlSessionFactoryList;
        this.properties = properties;
    }

    @PostConstruct
    public void addMyInterceptor() {
        PageInterceptor pageInterceptor = new PageInterceptor();
        pageInterceptor.setProperties(this.properties);
        LocalPageInterceptor localPageInterceptor = new LocalPageInterceptor();
        for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
            // 后加入的插件先执行,这里需要先执行我们自定义的LocalPage插件,之后执行分页插件才能拿到Page对象
            sqlSessionFactory.getConfiguration().addInterceptor(pageInterceptor);
            sqlSessionFactory.getConfiguration().addInterceptor(localPageInterceptor);
        }
    }
}

6.使用 LocalPage

    protected TableDataInfo getDataTable(List<?> list) {
        TableDataInfo rspData = new TableDataInfo();
        rspData.setCode(HttpStatus.SUCCESS);
        rspData.setRows(list);
        rspData.setMsg("查询成功");
        LocalPage<?> localPage = LocalPageHelper.getLocalPage();
        rspData.setTotal(localPage == null ? list.size() : localPage.getPage().getTotal());
        return rspData;
    }