利用 LTW 处理 MyBatis Plus 实现自动分页
SpringBoot 启动注解
@EnableLoadTimeWeaving
JVM 参数
-javaagent:D:\repository\org\springframework\spring-instrument\6.1.14/spring-instrument-6.1.14.jar
-javaagent:D:\repository\org\aspectj\aspectjweaver\1.9.22.1\aspectjweaver-1.9.22.1.jar
aop.xml
<!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "https://www.eclipse.org/aspectj/dtd/aspectj.dtd">
<aspectj>
<weaver options="-showWeaveInfo -XnoInline -Xset:weaveJavaxPackages=true -Xlint:ignore -verbose -XmessageHandlerClass:org.springframework.aop.aspectj.AspectJWeaverMessageHandler">
<!-- 只对指定包下的类进行编织 -->
<!-- <include execution="* com.baomidou.mybatisplus.core.override.MybatisMapperMethod.executeForIPage(org.apache.ibatis.session.SqlSession, java.lang.Object[])"/>-->
<include within="com.baomidou.mybatisplus.core.override..*" />
<include within="com.baomidou.mybatisplus.core.toolkit..*" />
</weaver>
<aspects>
<!-- 使用指定的切面类进行编织 -->
<aspect name="org.cxy.MybatisMapperMethodAspect"/>
<aspect name="org.cxy.MyBatisPlusParameterUtilsAspect"/>
</aspects>
</aspectj>
分页参数解析
package org.cxy;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.cxy.PageParam;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
/**
* todo
* <p>部分参考 {@code com.baomidou.mybatisplus.core.toolkit.ParameterUtils#findPage(java.lang.Object)}
* <p>主要特殊处理了分页参数对象 PageParam
*
* @author chenxingyang
* @see com.baomidou.mybatisplus.core.toolkit.ParameterUtils#findPage(java.lang.Object)
* @since 2025-04-09
*/
public class PageHelper {
/**
* 保存分页数据
*/
private static final ThreadLocal<Optional<IPage<?>>> PAGE_THREAD_LOCAL = new ThreadLocal<>();
public static void clear() {
PAGE_THREAD_LOCAL.remove();
}
/**
* 查找分页参数
*
* @param parameterObject 参数对象
* @return 分页参数
*/
public static Optional<IPage<?>> findPage(Object parameterObject) {
if (Objects.nonNull(PAGE_THREAD_LOCAL.get())) {
return PAGE_THREAD_LOCAL.get();
}
// 下面实际上是不应该走到的,因为 init 已经初始化非空数据到 PAGE_THREAD_LOCAL 里面了
if (parameterObject == null) {
PAGE_THREAD_LOCAL.set(Optional.empty());
return Optional.empty();
}
// 遍历 Mapper 方法参数
if (parameterObject instanceof Map<?, ?> parameterMap) {
for (Map.Entry<?, ?> entry : parameterMap.entrySet()) {
// Mapper 传了分页数据
if (entry.getValue() != null && entry.getValue() instanceof IPage<?> pageData) {
Optional<IPage<?>> page = Optional.of(pageData);
PAGE_THREAD_LOCAL.set(page);
return page;
}
}
}
if (parameterObject instanceof Map<?, ?> parameterMap) {
for (Map.Entry<?, ?> entry : parameterMap.entrySet()) {
// Mapper 传的实体是一个分页实体
if (entry.getValue() != null && entry.getValue() instanceof PageParam<?> pageParam) {
// 得到分页对象
Optional<IPage<?>> page = Optional.of(pageParam.getPage());
PAGE_THREAD_LOCAL.set(page);
return page;
}
}
}
PAGE_THREAD_LOCAL.set(Optional.empty());
return Optional.empty();
// return ParameterUtils.findPage(parameterObject);
}
/**
* 查找分页参数
*
* @param args 参数对象
* @return 分页参数
*/
public static Object[] init(Object[] args) {
if (args == null || args.length == 0) {
PAGE_THREAD_LOCAL.set(Optional.empty());
return args;
}
for (Object arg : args) {
// Mapper 传了分页数据
if (arg instanceof IPage<?> pageData) {
Optional<IPage<?>> page = Optional.of(pageData);
PAGE_THREAD_LOCAL.set(page);
return args;
}
}
for (Object arg : args) {
// Mapper 传的实体是一个分页实体
if (arg instanceof PageParam<?> pageParam) {
Optional<IPage<?>> page = Optional.of(pageParam.getPage());
PAGE_THREAD_LOCAL.set(page);
return append(args, page.get());
}
}
PAGE_THREAD_LOCAL.set(Optional.empty());
return args;
// return ParameterUtils.findPage(parameterObject);
}
private static Object[] append(Object[] args, Object element) {
Object[] newArgs = new Object[args.length + 1];
System.arraycopy(args, 0, newArgs, 0, args.length);
newArgs[args.length] = element;
return newArgs;
}
}
拦截分页特殊处理
package org.cxy;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
/**
* todo
*
* @author chenxingyang
* @since 2025-04-09
*/
@Aspect
public class MybatisMapperMethodAspect {
public static MybatisMapperMethodAspect aspectOf() {
return new MybatisMapperMethodAspect();
}
@Pointcut("execution(* com.baomidou.mybatisplus.core.override.MybatisMapperMethod.executeForIPage(org.apache.ibatis.session.SqlSession, java.lang.Object[]))")
public void pointCut() {
}
@Around("pointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 本来是为了在参数后追加分页对象参数,但是发现 executeForIPage 里面的 convertArgsToSqlCommandParam 并不会处理到追加的分页对象(只会处理本身 Mapper 方法声明的参数数量)
// 导致到 MP 的分页插件里面得不到这个分页对象,因此加上了 MyBatisPlusParameterUtilsAspect 拦截分页查询获取分页对象的方法从 ThreadLocal 获取
// 即便分页插件通过 LTW 的 AOP 从 ThreadLocal 获取了,这里仍需要追究参数,因为 executeForIPage 里面获取了 args 里面的分页对象,然后得到分页查询结果后设置分页对象的结果列表
// 因此要保证 executeForIPage 里面获取到的分页对象和分页查询获取到的分页对象是同一个分页对象实例
Object[] args = PageHelper.init((Object[]) joinPoint.getArgs()[1]);
Object[] finalArgs = new Object[joinPoint.getArgs().length];
// 原参数的第一个参数: SqlSession 对象
finalArgs[0] = joinPoint.getArgs()[0];
// 原参数的 Object[] 变成了新的(追加了分页对象)
finalArgs[1] = args;
try {
return joinPoint.proceed(finalArgs);
} finally {
PageHelper.clear();
}
}
}
拦截分页查询获取分页对象逻辑
package org.cxy;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.ParameterUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.*;
import java.util.stream.Collectors;
/**
* todo
*
* @author chenxingyang
* @since 2025-04-09
*/
@Aspect
@Slf4j
public class MyBatisPlusParameterUtilsAspect {
public static MyBatisPlusParameterUtilsAspect aspectOf() {
return new MyBatisPlusParameterUtilsAspect();
}
@Pointcut("execution(* com.baomidou.mybatisplus.core.toolkit.ParameterUtils.findPage(java.lang.Object))")
public void pointCut() {
}
@Around("pointCut()")
@SuppressWarnings("all")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 拦截 findPage 返回分页对象
Optional<IPage<?>> page = PageHelper.findPage(joinPoint.getArgs()[0]);
if (page != null) {
return page;
}
return joinPoint.proceed();
}
}
MP 分页
分页特殊处理
在 com.baomidou.mybatisplus.core.override.MybatisMapperMethod#execute 对返回类型为 IPage 的 SELECT 进行特殊处理 -> executeForIPage
@SuppressWarnings("all")
private <E> Object executeForIPage(SqlSession sqlSession, Object[] args) {
IPage<E> result = null;
// 获取参数中的分页对象
for (Object arg : args) {
if (arg instanceof IPage) {
result = (IPage<E>) arg;
break;
}
}
Assert.notNull(result, "can't found IPage for args!");
// 转换为 Map, key 为 @Param 名称, 还追加了 parm1、parm2 这种
Object param = method.convertArgsToSqlCommandParam(args);
// List 查询
List<E> list = sqlSession.selectList(command.getName(), param);
// 分页对象设置查询结果,因此分页插件获取到的分页对象也必须是同一个对象(分页查询获取总数)
result.setRecords(list);
return result;
}
分页插件 PaginationInnerInterceptor
/**
* 这里进行count,如果count为0这返回false(就是不再执行sql了)
*/
@Override
public boolean willDoQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
// 获取分页对象
IPage<?> page = ParameterUtils.findPage(parameter).orElse(null);
if (page == null || page.getSize() < 0 || !page.searchCount() || resultHandler != Executor.NO_RESULT_HANDLER) {
return true;
}
// ... 略
// 设置总条数
page.setTotal(total);
return continuePage(page);
}
@Override
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
// 获取分页对象
IPage<?> page = ParameterUtils.findPage(parameter).orElse(null);
if (null == page) {
return;
}
// ... 略
// size 小于 0 且不限制返回值则不构造分页sql
Long _limit = page.maxLimit() != null ? page.maxLimit() : maxLimit;
if (page.getSize() < 0 && null == _limit) {
if (addOrdered) {
PluginUtils.mpBoundSql(boundSql).sql(buildSql);
}
return;
}
// ... 略
}
分析
- 要在 MybatisMapperMethod#executeForIPage 的 Object[] args 参数中加上分页对象
- 要拦截 ParameterUtils#findPage 得到我们自己追加的分页对象(因为 追加到 args 后由于executeForIPage 进行了 convertArgsToSqlCommandParam没有继续向下传递)
executeForIPage 是 private 的,findPage 是 static 的,且都没有被 Spring 管理,不能使用 Spring 的运行时 AOP,那么要么编译时 AOP,要么类加载时 AOP,这里选择了 LTW 类加载时 AOP
最开始想的是直接拦截 Mapper 方法添加分页参数,发现 aop 会校验参数数量是否改变