2.19.1-mybatis-plus分页源码分析
mybatis plus 分页源码分析
文章目录
- 1. MyBatis-Plus分页功能实现
- 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中?
此项目gitee源代码:
- git clone https://gitee.com/kelvin11/springboot-blank, 关于mybatis plus的源码在
2框架demo/2.1mybatisplus
这里目标:分析MyBatis-Plus源码是如何实现分页操作的
步骤
- 先进行基础代码的搭建
- 分析一个查询分页源码走读
1. MyBatis-Plus分页功能实现
环境:
springboot:2.3.7.RELEASE
mybatis-plus-boot-starter:3.4.3
druid、mysql驱动
1.1. 主要的pom
<!-- 阿里druid数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version>
</dependency>
<!--druid需要,否则可能ClassNotFoundException: org.apache.log4j.Priority-->
<!--经过测试,druid配置改一下,'wall'用于防火墙,SpringBoot中没有log4j,我改成了log4j2,就不需要这个jar包了-->
<!--<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>-->
<!-- Mysql驱动包 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--mybatis-plus jar包-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
1.2. mapper
package com.example.mybatisplus.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.mybatisplus.domain.MpGen;
/**
* <p>
* mybatis plus测试表 Mapper 接口
* </p>
*
* @author KelvinLiu
* @since 2021-11-26
*/
public interface MpGenMapper extends BaseMapper<MpGen> {
}
1.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;
/**
* <p>
* mybatis plus测试表 前端控制器
* </p>
*
* @author KelvinLiu
* @since 2021-11-26
*/
@RestController
@RequestMapping("/mp-gen")
public class MpGenController {
@Autowired
IMpGenService mpGenService;
/**
* 测试枚举值的序列化。性别字段,在数据库是0和1。
* https://baomidou.com/guide/enum.html
*
* [{"id":1,"name":"Jone","age":18,"gender":"女","email":"test1@baomidou.com","delFlag":0},
* {"id":2,"name":"Jack","age":20,"gender":"男","email":"test2@baomidou.com","delFlag":0},
* {"id":3,"name":"Tom","age":28,"gender":"男","email":"test3@baomidou.com","delFlag":0},
* {"id":4,"name":"Sandy","age":21,"gender":"女","email":"test4@baomidou.com","delFlag":0},
* {"id":5,"name":"Billie","age":24,"gender":"男","email":"updateToThis@123.com","delFlag":0},
* {"id":7,"name":"kelvin","age":30,"gender":"男","email":"updateToThis@123.com","delFlag":0}]
*
* @return
*/
@RequestMapping("/listall")
public List<MpGen> listAll() {
return mpGenService.list();
}
@Resource
MpGenMapper mpGenMapper;
/**
* 分页源码分析入口
* @return
*/
@RequestMapping("/page")
public List<MpGen> testPagination() {
//参数一是当前页,参数二是每页个数
IPage<MpGen> 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;
/**
* @ClassName MybatisPlusConfig
* @Description
* @Author liukun
* @Date 2021/11/29 上午9:49
*/
@Configuration
public class MybatisPlusConfig {
/**
* 这个是分页的核心配置:
* 1. 注册一个 MybatisPlusInterceptor(实现了org.apache.ibatis.plugin.Interceptor),所以mybatis在执行过程中会使用此拦截器
* 2. MybatisPlusInterceptor内部持有一个拦截器列表,所以叫 xxxInnerInterceptor 。其中实现分页参数添加的拦截器是PaginationInnerInterceptor
* @return
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
// 乐观锁
// interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
// interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
/*@Bean
public OptimisticLockerInnerInterceptor optimisticLockerInterceptor() {
return new OptimisticLockerInnerInterceptor();
}*/
/*@Bean
public MybatisPlusInterceptor optimisticLockerInnerInterceptor(){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
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是个代理对象MybatisMapperProxy
mpGenMapper
是个代理对象,是 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)分析:
private <E> Object executeForIPage(SqlSession sqlSession, Object[] args) {
IPage<E> 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<E> list = sqlSession.selectList(this.command.getName(), param);
result.setRecords(list);
return result;
}
2.5. SqlSessionTemplate.selectList
2.6. DefaultSqlSession.selectList
根据下图,下面应该是要去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<Method> 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<Object> result = executor.query(countMs, parameter, rowBounds, resultHandler, cacheKey, countSql);
long total = 0;
if (CollectionUtils.isNotEmpty(result)) {
// 个别数据库 count 没数据不会返回 0
Object o = result.get(0);
if (o != null) {
total = Long.parseLong(o.toString());
}
}
page.setTotal(total);
return continuePage(page);
}
下面都是倒查参数的过程:
ParameterUtils.findPage(parameter)方法比较简单,就是过滤找IPage类型的分页对象
public static Optional<IPage> findPage(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<OrderItem> 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<ParameterMapping> mappings = mpBoundSql.parameterMappings();
Map<String, Object> 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);
}
}
}
这一块代码,网上的解释还不多。
更多推荐
所有评论(0)