likes
comments
collection
share

MybatisPlus两个limit引发的思考(下)——mapper接口的初始化及PaginationInterceptor是如何拼接limit的

作者站长头像
站长
· 阅读数 13

mapper接口的初始化

我们还是要先了解下MapperFactoryBean:

  • 是一个工厂bean,用于创建Mybatis Mapper代理实例,该代理对象实现了指定的Mapper接口。继承了SqlSessionDaoSupport类并实现了FactoryBean接口。

  • 它覆盖了getObject()方法,该方法返回了一个代理对象,该代理对象实现了指定的Mapper接口,该代理实例会自动执行相应的SQL语句并将结果映射到Java对象中。

  • 可以配置mapper接口所需要的SqlSessionTemplate和DataSource等信息,以便进行数据库操作。

下面我们来看看具体是怎么实例化的,是怎么通过MapperFactoryBean来创建最终的代理MybatisMapperProxy的。

这里我在MapperFactoryBean的构造器上打了个断点: MybatisPlus两个limit引发的思考(下)——mapper接口的初始化及PaginationInterceptor是如何拼接limit的

MybatisPlus两个limit引发的思考(下)——mapper接口的初始化及PaginationInterceptor是如何拼接limit的 源头上是从ApplicationContext#refresh的registerBeanPostProcessor进行注册BeanPostProcessor时会检查所有的bean definitions,如果可以进行实例化就会进行实例化。

MapperFactoryBean已经实例化了,但不是最终的bean。

我们在AbstractAutowireCapableBeanFactory#createBean打个断点:

MybatisPlus两个limit引发的思考(下)——mapper接口的初始化及PaginationInterceptor是如何拼接limit的 这里可以看到MapperFactoryBean已经完成了实例化并注入了属性,下面要对bean进行初始化。

完成MapperFactoryBean的初始化之后会调用MapperFactoryBean#getObject创建MybatisMapperProxy:

@Override
public T getObject() throws Exception {
  return getSqlSession().getMapper(this.mapperInterface);
}

MybatisPlus两个limit引发的思考(下)——mapper接口的初始化及PaginationInterceptor是如何拼接limit的

SqlSessionTemplate#getMapper:

@Override
public <T> T getMapper(Class<T> type) {
  return getConfiguration().getMapper(type, this);
}

MybatisConfiguration#getMapper:

@Override
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mybatisMapperRegistry.getMapper(type, sqlSession);
}

MybatisMapperProxyFactory:

public T newInstance(SqlSession sqlSession) {
    final MybatisMapperProxy<T> mapperProxy = new MybatisMapperProxy<>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
}
protected T newInstance(MybatisMapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy);
}

可以看到最终使用MybatisMapperProxy对mapper接口进行了代理,使用的是jdk动态代理,MybatisMapperProxy继承了InvocationHandler:

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
        if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, args);
        } else if (method.isDefault()) {
            if (privateLookupInMethod == null) {
                return invokeDefaultMethodJava8(proxy, method, args);
            } else {
                return invokeDefaultMethodJava9(proxy, method, args);
            }
        }
    } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
    }
    final MybatisMapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
}

时序图

MybatisPlus两个limit引发的思考(下)——mapper接口的初始化及PaginationInterceptor是如何拼接limit的 简单的描述下整个过程:

  1. 这里是MapperScannerConfigurer#postProcessBeanDefinitionRegistry中的processPropertyPlaceHolder寻找PropertyResourceConfigurer类型的bean触发的

从这里可以看到并非一定由寻找PropertyResourceConfigurer类型的bean触发,寻找其他类型的bean同样也会触发,如果实在spring容器初始化没有发生寻找没有触发也没事,发生依赖时也会触发MapperFactoryBean实例化&初始化,可能不会直接从factoryBeanInstanceCache中获取。

  1. 调用MapperFactoryBean的构造器进行实例化
  2. 在初始化的时候调用MapperFactoryBean#getObject进而调用MybatisMapperProxyFactory#newInstantce,使用sqlSessionTemplate和mapperInterface生成代理bean。

PaginationInterceptor如何生效的

直接在PaginationInterceptor#intercept上打个断点:

@Override
public Object intercept(Invocation invocation) throws Throwable {
    StatementHandler statementHandler = PluginUtils.realTarget(invocation.getTarget());
    MetaObject metaObject = SystemMetaObject.forObject(statementHandler);

    // SQL 解析
    this.sqlParser(metaObject);

    // 先判断是不是SELECT操作  (2019-04-10 00:37:31 跳过存储过程)
    MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
    if (SqlCommandType.SELECT != mappedStatement.getSqlCommandType()
        || StatementType.CALLABLE == mappedStatement.getStatementType()) {
        return invocation.proceed();
    }

    // 针对定义了rowBounds,做为mapper接口方法的参数
    BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
    Object paramObj = boundSql.getParameterObject();

    // 判断参数里是否有page对象 #1
    IPage<?> page = ParameterUtils.findPage(paramObj).orElse(null);

    /*
     * 不需要分页的场合,如果 size 小于 0 返回结果集
     */
    if (null == page || page.getSize() < 0) {
        return invocation.proceed();
    }

    if (this.limit > 0 && this.limit <= page.getSize()) {
        //处理单页条数限制
        handlerLimit(page);
    }

    String originalSql = boundSql.getSql();
    Connection connection = (Connection) invocation.getArgs()[0];

    if (page.isSearchCount() && !page.isHitCount()) {
        SqlInfo sqlInfo = SqlParserUtils.getOptimizeCountSql(page.optimizeCountSql(), countSqlParser, originalSql);
        this.queryTotal(sqlInfo.getSql(), mappedStatement, boundSql, page, connection);
        if (page.getTotal() <= 0) {
            return null;
        }
    }
    DbType dbType = this.dbType == null ? JdbcUtils.getDbType(connection.getMetaData().getURL()) : this.dbType;
    IDialect dialect = Optional.ofNullable(this.dialect).orElseGet(() -> DialectFactory.getDialect(dbType));
    String buildSql = concatOrderBy(originalSql, page);
    // #2
    DialectModel model = dialect.buildPaginationSql(buildSql, page.offset(), page.getSize());
    Configuration configuration = mappedStatement.getConfiguration();
    List<ParameterMapping> mappings = new ArrayList<>(boundSql.getParameterMappings());
    Map<String, Object> additionalParameters = (Map<String, Object>) metaObject.getValue("delegate.boundSql.additionalParameters");
    model.consumers(mappings, configuration, additionalParameters);
    metaObject.setValue("delegate.boundSql.sql", model.getDialectSql());
    metaObject.setValue("delegate.boundSql.parameterMappings", mappings);
    return invocation.proceed();
}
  1. 从BoundSql中获得IPage参数
  2. 这里会多拼接一个limit,最后赋值给了metaObject:
@Override
public DialectModel buildPaginationSql(String originalSql, long offset, long limit) {
    String sql = originalSql + " LIMIT " + FIRST_MARK + StringPool.COMMA + SECOND_MARK;
    return new DialectModel(sql, offset, limit).setConsumerChain();
}