- 1. MyBatis-Plus分页功能实现
- 1.1. 主要的pom
- 1.2. mapper
- 1.3. controller
- 1.4. mybatis plus配置文件
- 1.5. MyBatis-Plus JavaConfig配置
- 2. MyBatis-Plus分页源码走读
- 2.1. 发起请求
- 2.2. mapper是个代理对象MybatisMapperProxy
- 2.3. MybatisMapperProxy.MapperMethodInvoker
- 2.4. MybatisMapperMethod.execute
- 2.5. SqlSessionTemplate.selectList
- 2.6. DefaultSqlSession.selectList
- 2.7. Plugin.query
- 2.8. 对实现了mybatis inceptor的拦截器的调用(也就是MybatisPlusInterceptor)
- 2.9. MybatisPlusIntercepotr中持有PaginationInnerInterceptor内部拦截器
- 2.10. PaginationInnerInterceptor.willDoQuery和beforeQuery
- 2.11. 如何取出page分页信息?
- 2.12. 如何将page分页信息绑定到sql中?
1. MyBatis-Plus分页功能实现
此项目gitee源代码:
- git clone https://gitee.com/kelvin11/springboot-blank, 关于mybatis plus的源码在2框架demo/2.1mybatisplus这里
目标:分析MyBatis-Plus源码是如何实现分页操作的
步骤
- 先进行基础代码的搭建
- 分析一个查询分页源码走读
1.1. 主要的pom环境:
springboot:2.3.7.RELEASE
mybatis-plus-boot-starter:3.4.3
druid、mysql驱动
1.2. mappercom.alibaba druid-spring-boot-starter 1.2.8 mysql mysql-connector-java com.baomidou mybatis-plus-boot-starter 3.4.3
package com.example.mybatisplus.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.mybatisplus.domain.MpGen; public interface MpGenMapper extends BaseMapper1.3. controller{ }
package com.example.mybatisplus.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.mybatisplus.domain.MpGen;
import com.example.mybatisplus.mapper.MpGenMapper;
import com.example.mybatisplus.service.IMpGenService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;
@RestController
@RequestMapping("/mp-gen")
public class MpGenController {
@Autowired
IMpGenService mpGenService;
@RequestMapping("/listall")
public List listAll() {
return mpGenService.list();
}
@Resource
MpGenMapper mpGenMapper;
@RequestMapping("/page")
public List testPagination() {
//参数一是当前页,参数二是每页个数
IPage userPage = new Page<>(2, 2);
return mpGenMapper.selectPage(userPage, null).getRecords();
}
}
1.4. mybatis plus配置文件
这个是可有可无的,没有特别的要求,可以不配
mybatis-plus:
global-config:
db-config:
logic-delete-field: del_flag # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
# configuration:
# 是否将sql打印到控制面板(该配置会将sql语句和查询的结果都打印到控制台)
# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
type-enums-package: com.example.mybatisplus.mybatisenums
logging:
level:
# mybatis plus使用debug级别,会输出sql语句
com.example.mybatisplus.mapper: debug
1.5. MyBatis-Plus JavaConfig配置
package com.example.mybatisplus.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
// 乐观锁
// interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
// interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}
2. MyBatis-Plus分页源码走读
2.1. 发起请求
这里顺便用到了一个好用的插件:RestfulToolkit-fix(jinhong 1.0.0),可以快速的复制完整的请求url:
拿到的就是:http://localhost:8021/mp-gen/page,放到浏览器头部请求进到断点。
2.2. mapper是个代理对象MybatisMapperProxympGenMapper是个代理对象,是 MybatisMapperProxy
所以要进入到MybatisMapperProxy的selectPage方法。debug进入selectPage方法调用,也就是MybatisMapperProxy.invoke方法,目的是为了找到实际调用的哪个类的哪个方法。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
return Object.class.equals(method.getDeclaringClass()) ? method.invoke(this, args) : this.cachedInvoker(method).invoke(proxy, method, args, this.sqlSession);
} catch (Throwable var5) {
throw ExceptionUtil.unwrapThrowable(var5);
}
}
2.3. MybatisMapperProxy.MapperMethodInvoker
可以看到,应该是要调用interface com.baomidou.mybatisplus.core.mapper.BaseMapper.selectPage方法。但是这是一个接口,并不是实现,分析的目标应该看到this.cachedInvoker(method),看起来是找到此mapper接口的实现来进行selectPage方法的调用。
private MybatisMapperProxy.MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
try {
return (MybatisMapperProxy.MapperMethodInvoker)CollectionUtils.computeIfAbsent(this.methodCache, method, (m) -> {
if (m.isDefault()) {
try {
return privateLookupInMethod == null ? new MybatisMapperProxy.DefaultMethodInvoker(this.getMethodHandleJava8(method)) : new MybatisMapperProxy.DefaultMethodInvoker(this.getMethodHandleJava9(method));
} catch (InstantiationException | InvocationTargetException | NoSuchMethodException | IllegalAccessException var4) {
throw new RuntimeException(var4);
}
} else {
// PlainMethodInvoker是cachedInvoker方法的返回类型,所以,this.cachedInvoker(method).invoke其实就是调用的PlainMethodInvoker类的invoke方法
return new MybatisMapperProxy.PlainMethodInvoker(new MybatisMapperMethod(this.mapperInterface, method, this.sqlSession.getConfiguration()));
}
});
} catch (RuntimeException var4) {
Throwable cause = var4.getCause();
throw (Throwable)(cause == null ? var4 : cause);
}
}
根据上面代码的注释,继续找PlainMethodInvoker类的invoke方法:
private static class PlainMethodInvoker implements MybatisMapperProxy.MapperMethodInvoker {
private final MybatisMapperMethod mapperMethod;
public PlainMethodInvoker(MybatisMapperMethod mapperMethod) {
this.mapperMethod = mapperMethod;
}
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
// 核心是这里,断点到这里看看mapperMethod是哪个类
return this.mapperMethod.execute(sqlSession, args);
}
}
2.4. MybatisMapperMethod.execute
所以,下面就是去找MybatisMapperMethod.execute()方法
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
Object param;
switch(this.command.getType()) {
case INSERT:
param = this.method.convertArgsToSqlCommandParam(args);
result = this.rowCountResult(sqlSession.insert(this.command.getName(), param));
break;
case UPDATE:
param = this.method.convertArgsToSqlCommandParam(args);
result = this.rowCountResult(sqlSession.update(this.command.getName(), param));
break;
case DELETE:
param = this.method.convertArgsToSqlCommandParam(args);
result = this.rowCountResult(sqlSession.delete(this.command.getName(), param));
break;
case SELECT:
if (this.method.returnsVoid() && this.method.hasResultHandler()) {
this.executeWithResultHandler(sqlSession, args);
result = null;
} else if (this.method.returnsMany()) {
result = this.executeForMany(sqlSession, args);
} else if (this.method.returnsMap()) {
result = this.executeForMap(sqlSession, args);
} else if (this.method.returnsCursor()) {
result = this.executeForCursor(sqlSession, args);
} else if (IPage.class.isAssignableFrom(this.method.getReturnType())) {
// 这里比较坑,其实我们的mpGenMapper.selectPage()方法调用返回是IPage类型,下图是debug的结果。
// 继续分析this.executeForIPage(sqlSession, args)方法
result = this.executeForIPage(sqlSession, args);
} else {
param = this.method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(this.command.getName(), param);
if (this.method.returnsOptional() && (result == null || !this.method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + this.command.getName());
}
if (result == null && this.method.getReturnType().isPrimitive() && !this.method.returnsVoid()) {
throw new BindingException("Mapper method '" + this.command.getName() + " attempted to return null from a method with a primitive return type (" + this.method.getReturnType() + ").");
} else {
return result;
}
}
this.executeForIPage(sqlSession, args)分析:
private2.5. SqlSessionTemplate.selectList 2.6. DefaultSqlSession.selectListObject executeForIPage(SqlSession sqlSession, Object[] args) { IPage result = null; Object[] var4 = args; int var5 = args.length; for(int var6 = 0; var6 < var5; ++var6) { Object arg = var4[var6]; if (arg instanceof IPage) { result = (IPage)arg; break; } } Assert.notNull(result, "can't found IPage for args!", new Object[0]); Object param = this.method.convertArgsToSqlCommandParam(args); //debug看看下一步应该去哪儿? List list = sqlSession.selectList(this.command.getName(), param); result.setRecords(list); return result; }
根据下图,下面应该是要去DefaultSqlSession.selectList方法,此时已经进入到mybatis的源码范围了:
到这里,终于快要接近事情的真相了:
2.7. Plugin.query在DefaultSqlSession.selectList方法里,这个executor是Plugin类型
实际进入到了Plugin.invoke方法
需要看看Plugin里面是怎么实现的:
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set methods = signatureMap.get(method.getDeclaringClass());
if (methods != null && methods.contains(method)) {
// 看起来很美好,就是对interceptor进行了调用,这里,也就是调用了MybatisPlusInterceptor.interceptor方法
return interceptor.intercept(new Invocation(target, method, args));
}
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
2.8. 对实现了mybatis inceptor的拦截器的调用(也就是MybatisPlusInterceptor)
下面跟进看看 MybatisPlusInterceptor.interceptor 方法:
public Object intercept(Invocation invocation) throws Throwable {
Object target = invocation.getTarget();
Object[] args = invocation.getArgs();
if (target instanceof Executor) {
final Executor executor = (Executor) target;
Object parameter = args[1];
boolean isUpdate = args.length == 2;
MappedStatement ms = (MappedStatement) args[0];
if (!isUpdate && ms.getSqlCommandType() == SqlCommandType.SELECT) {
RowBounds rowBounds = (RowBounds) args[2];
ResultHandler resultHandler = (ResultHandler) args[3];
BoundSql boundSql;
if (args.length == 4) {
boundSql = ms.getBoundSql(parameter);
} else {
// 几乎不可能走进这里面,除非使用Executor的代理对象调用query[args[6]]
boundSql = (BoundSql) args[5];
}
for (InnerInterceptor query : interceptors) {
// 其实也可以联想到,我们的 JavaConfig里面配置的时候,就是add了一些interceptor,其中之一就是关于分页的,断点进来看看是不是如此?
if (!query.willDoQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql)) {
return Collections.emptyList();
}
query.beforeQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql);
}
CacheKey cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
return executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
} else if (isUpdate) {
for (InnerInterceptor update : interceptors) {
if (!update.willDoUpdate(executor, ms, parameter)) {
return -1;
}
update.beforeUpdate(executor, ms, parameter);
}
}
} else {
// StatementHandler
final StatementHandler sh = (StatementHandler) target;
// 目前只有StatementHandler.getBoundSql方法args才为null
if (null == args) {
for (InnerInterceptor innerInterceptor : interceptors) {
innerInterceptor.beforeGetBoundSql(sh);
}
} else {
Connection connections = (Connection) args[0];
Integer transactionTimeout = (Integer) args[1];
for (InnerInterceptor innerInterceptor : interceptors) {
innerInterceptor.beforePrepare(sh, connections, transactionTimeout);
}
}
}
return invocation.proceed();
}
2.9. MybatisPlusIntercepotr中持有PaginationInnerInterceptor内部拦截器
2.10. PaginationInnerInterceptor.willDoQuery和beforeQuery
其实也可以看出,PaginationInnerInterceptor的核心方法,是willDoQuery和beforeQuery
willDoQuery是查询总数,来确定是否要进行分页查询,实际分页查询参数组装,应该是在beforeQuery方法中:
这里的page参数传递机制,是不是用的ThreadLocal?是如何把分页参数拼接到sql里的?待继续分析
2.11. 如何取出page分页信息?从PaginationInnerInterceptor.willDoQuery里面,有找IPage的代码
public boolean willDoQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
// 从parameter中找page参数,debug看一下目前parameter是什么类型、数据包
IPage> page = ParameterUtils.findPage(parameter).orElse(null);
if (page == null || page.getSize() < 0 || !page.searchCount()) {
return true;
}
BoundSql countSql;
MappedStatement countMs = buildCountMappedStatement(ms, page.countId());
if (countMs != null) {
countSql = countMs.getBoundSql(parameter);
} else {
countMs = buildAutoCountMappedStatement(ms);
String countSqlStr = autoCountSql(page.optimizeCountSql(), boundSql.getSql());
PluginUtils.MPBoundSql mpBoundSql = PluginUtils.mpBoundSql(boundSql);
countSql = new BoundSql(countMs.getConfiguration(), countSqlStr, mpBoundSql.parameterMappings(), parameter);
PluginUtils.setAdditionalParameter(countSql, mpBoundSql.additionalParameters());
}
CacheKey cacheKey = executor.createCacheKey(countMs, parameter, rowBounds, countSql);
List
下面都是倒查参数的过程:
ParameterUtils.findPage(parameter)方法比较简单,就是过滤找IPage类型的分页对象
public static OptionalfindPage(Object parameterObject) { if (parameterObject != null) { if (parameterObject instanceof Map) { Map, ?> parameterMap = (Map)parameterObject; Iterator var2 = parameterMap.entrySet().iterator(); while(var2.hasNext()) { Entry entry = (Entry)var2.next(); if (entry.getValue() != null && entry.getValue() instanceof IPage) { return Optional.of((IPage)entry.getValue()); } } } else if (parameterObject instanceof IPage) { return Optional.of((IPage)parameterObject); } } return Optional.empty(); } }
下面就是要分析一下,parameter是什么时候包装并设置值的。要倒推来看。
在com.baomidou.mybatisplus.core.override.MybatisMapperMethod#executeForIPage这个方法处,进行了参数的包装:
上一个调用者是com.baomidou.mybatisplus.core.override.MybatisMapperMethod#execute
再继续网上,就找到了com.baomidou.mybatisplus.core.override.MybatisMapperProxy#invoke
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// this.cachedInvoker(method)取出来的是MybatisMapperProxy.MapperMethodInvoker类型,调用其invoke方法,参数就是放在了args里
return Object.class.equals(method.getDeclaringClass()) ? method.invoke(this, args) : this.cachedInvoker(method).invoke(proxy, method, args, this.sqlSession);
} catch (Throwable var5) {
throw ExceptionUtil.unwrapThrowable(var5);
}
}
也就回到了最开始使用mapper.selectPage调用的时候,参数就是这么传进来的。一路下来,没有看到使用ThreadLocal的地方。大多数的调用都是使用的代理。
这里就是有个存疑,mpGenMapper.selectPage(userPage, null).getRecords(),这里的mpGenMapper是怎么被注入为一个MybatisMapperProxy对象的?这个应该要另外从mybatis、springboot源码看起。
2.12. 如何将page分页信息绑定到sql中?实际执行分页sql处理的,是在com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor#beforeQuery,如下图
这个方法不长,贴一下看看:
@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;
}
// 处理 orderBy 拼接
boolean addOrdered = false;
String buildSql = boundSql.getSql();
List orders = page.orders();
if (!CollectionUtils.isEmpty(orders)) {
addOrdered = true;
buildSql = this.concatOrderBy(buildSql, orders);
}
// size 小于 0 不构造分页sql
if (page.getSize() < 0) {
if (addOrdered) {
PluginUtils.mpBoundSql(boundSql).sql(buildSql);
}
return;
}
handlerLimit(page);
IDialect dialect = findIDialect(executor);
final Configuration configuration = ms.getConfiguration();
// 分析的核心在这里,看方法名字就知道是构造分页sql,dialect这里肯定是mysqlDialect
DialectModel model = dialect.buildPaginationSql(buildSql, page.offset(), page.getSize());
PluginUtils.MPBoundSql mpBoundSql = PluginUtils.mpBoundSql(boundSql);
// 这一块代码的作用是?
List mappings = mpBoundSql.parameterMappings();
Map additionalParameter = mpBoundSql.additionalParameters();
model.consumers(mappings, configuration, additionalParameter);
mpBoundSql.sql(model.getDialectSql());
mpBoundSql.parameterMappings(mappings);
}
继续看一下MySqlDialect.buildPaginationSql()方法,也很简单:
public class MySqlDialect implements IDialect {
@Override
public DialectModel buildPaginationSql(String originalSql, long offset, long limit) {
StringBuilder sql = new StringBuilder(originalSql).append(" LIMIT ").append(FIRST_MARK);
if (offset != 0L) {
// 核心就是拼接 LIMIT,并且将分页的offset、limit传入到DialectModel构造函数中去。
// DialectModel也是baomidou的类,需要进一步分析是如何与mybatis产生交互的。
sql.append(StringPool.COMMA).append(SECOND_MARK);
return new DialectModel(sql.toString(), offset, limit).setConsumerChain();
} else {
return new DialectModel(sql.toString(), limit).setConsumer(true);
}
}
}
这一块代码,网上的解释还不多。



