1. MyBatis-Plus分页功能实现
环境:
1.1. 主要的pom
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<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;
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;
@RestController
@RequestMapping("/mp-gen")
public class MpGenController {
@Autowired
IMpGenService mpGenService;
@RequestMapping("/listall")
public List<MpGen> listAll() {
return mpGenService.list();
}
@Resource
MpGenMapper mpGenMapper;
@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
logic-delete-value: 1
logic-not-delete-value: 0
type-enums-package: com.example.mybatisplus.mybatisenums
logging:
level:
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));
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 {
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 {
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())) {
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);
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)) {
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 {
boundSql = (BoundSql) args[5];
}
for (InnerInterceptor query : interceptors) {
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 {
final StatementHandler sh = (StatementHandler) target;
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 {
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)) {
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 {
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;
}
boolean addOrdered = false;
String buildSql = boundSql.getSql();
List<OrderItem> orders = page.orders();
if (!CollectionUtils.isEmpty(orders)) {
addOrdered = true;
buildSql = this.concatOrderBy(buildSql, orders);
}
if (page.getSize() < 0) {
if (addOrdered) {
PluginUtils.mpBoundSql(boundSql).sql(buildSql);
}
return;
}
handlerLimit(page);
IDialect dialect = findIDialect(executor);
final Configuration configuration = ms.getConfiguration();
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) {
sql.append(StringPool.COMMA).append(SECOND_MARK);
return new DialectModel(sql.toString(), offset, limit).setConsumerChain();
} else {
return new DialectModel(sql.toString(), limit).setConsumer(true);
}
}
}
这一块代码,网上的解释还不多。
|