从PaginationInterceptor分页实现过程看mybatis-plus插件原理解析

前言:懒惰的我在项目中配置的mybatis-plus PaginationInterceptor分页插件突然失效了,在网上搜了大量文章还是没找到根本原因,只能把mybatis插件加载源码一撸到底了。

1、Mybatis-plus PaginationInterceptor加载原理源码解析

mybatis-plus PaginationInterceptor加载顺序:


源码分析:

1)MybatisPlusAutoConfiguration类,主要用来自动装配实例化SqlSessionFactory类对象

关键方法:sqlSessionFactory实例化sqlSessionFactory并加载所有mybatis plugins插件(包括分页page插件)

黄色代码为把所有mybatis plugins插件装配到sqlSessionFactory中。

注意:该实例化方法上添加了@ConditionalOnMissingBean注解,表示只有上下文中没有实例化sqlSessionFactory才会执行,所以当现有系统中自定义了sqlSessionFactory实例化方法,则该方法不会执行。

Java

@Bean

@ConditionalOnMissingBean

public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {

// TODO使用 MybatisSqlSessionFactoryBean 而不是 SqlSessionFactoryBean

    MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();

    factory.setDataSource(dataSource);

    factory.setVfs(SpringBootVFS.class);

    if (StringUtils.hasText(this.properties.getConfigLocation())) {

        factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));

    }

    applyConfiguration(factory);

    if (this.properties.getConfigurationProperties() != null) {

        factory.setConfigurationProperties(this.properties.getConfigurationProperties());

    }

    if (!ObjectUtils.isEmpty(this.interceptors)) {

        factory.setPlugins(this.interceptors);

    }

//此处省略一万行代码....

    return factory.getObject();

}

2)MybatisSqlSessionFactoryBean类:sqlSessionFactory的工厂类,实际去构建sqlSessionFactory对象和加载plus插件,主要方法代码如下:该方法主要调targetConfiguration.addInterceptor(plugin)把所有已经实例化的mybatis插件装载进MybatisConfiguration类中,MybatisConfiguration类主要是承载了创建SqlSessionFactory对象的所有上下文配置信息。

Java

protected SqlSessionFactory buildSqlSessionFactory() throws Exception {

    final MybatisConfiguration targetConfiguration;

//此处省略一万行代码...

    if (!isEmpty(this.plugins)) {

        Stream.of(this.plugins).forEach(plugin -> {

            targetConfiguration.addInterceptor(plugin);

            LOGGER.debug(() -> "Registered plugin: '" + plugin + "'");

        });

    }

//此处省略一万行代码...

    return sqlSessionFactory;

}

3) MybatisConfiguration类继承Configuration类:SqlSessionFactory对象的所有上下文配置信息。该类主要提供了PaginationInterceptor插件的添加和使用等方法:

Java

//添加拦截器

public void addInterceptor(Interceptor interceptor) {

  interceptorChain.addInterceptor(interceptor);

}

Java

//执行sql时,先执行所有sqlSessionFactory的拦截器插件

@Override

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {

    executorType = executorType == null ? defaultExecutorType : executorType;

    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;

    Executor executor;

    if (ExecutorType.BATCH == executorType) {

        executor = new MybatisBatchExecutor(this, transaction);

    } else if (ExecutorType.REUSE == executorType) {

        executor = new MybatisReuseExecutor(this, transaction);

    } else {

        executor = new MybatisSimpleExecutor(this, transaction);

    }

    if (cacheEnabled) {

        executor = new MybatisCachingExecutor(executor);

    }

    executor = (Executor) interceptorChain.pluginAll(executor);

    return executor;

}

4)InterceptorChain类:这个类用于存储拦截器对象list,并提供添加和调用的方法

Java

public class InterceptorChain {

  private final List<Interceptor> interceptors = new ArrayList<>();

//依次调用拦截器方法,注意这里时for循环调用,即,拦截器调用是有先后顺序的。

  public Object pluginAll(Object target) {

    for (Interceptor interceptor : interceptors) {

      target = interceptor.plugin(target);

    }

    return target;

  }

//添加拦截器

  public void addInterceptor(Interceptor interceptor) {

    interceptors.add(interceptor);

  }

//获取所有拦截器

  public List<Interceptor> getInterceptors() {

    return Collections.unmodifiableList(interceptors);

  }

}

到这里整个mybatis-plus插件的整个加载过程就已经结束了,我们page分页失效的问题也找到了,因为我们新增了自定义的sqlSessionFactory实例化类,所以没有走到MybatisAutoConfigin类去创建sqlSessionFactory对象了,从而也不存在加载page插件这么一回事了。解决方案如下:

Java

@Bean(name = "sqlSessionFactory")

public SqlSessionFactory sqlSessionFactory(@Qualifier("dataSource") DataSource dataSource, @Autowired PaginationInterceptor paginationInterceptor) throws Exception {

    Resource[] resources = new PathMatchingResourcePatternResolver().getResources(DefaultDataSourceConfig.MAPPER_LOCATION);

    final MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();

    sessionFactory.setDataSource(dataSource);

    sessionFactory.setMapperLocations(resources);

//手动把page分页插件加载到sesionFactory中去

    sessionFactory.setPlugins(paginationInterceptor);

//开启驼峰映射DTO

    Objects.requireNonNull(sessionFactory.getObject()).getConfiguration().setMapUnderscoreToCamelCase(true);

    return sessionFactory.getObject();

}

值得注意的一点是,mybatis-plus sqlSessionFactory级别插件的加载是跟sqlSessionFactory的自动装备强耦合在一起的,从而会导致其他自定义sqlSessionFactory不能自动装配mybatis-plus的插件,需要在自定义sqlSessionFactory中手动添加。改进方案其实可以借鉴Pagehepler分页插件的加载实现原理,使插件和sesionFactory的创建结偶,这样自定义sqlSessionFactory实例化类也能支持自动装载插件了。

附上PageHelper插件装载源码:

Java

@Configuration

@ConditionalOnBean(SqlSessionFactory.class)

@EnableConfigurationProperties(PageHelperProperties.class)

@AutoConfigureAfter(MybatisAutoConfiguration.class)

public class PageHelperAutoConfiguration {

//获取到所有sqlSessionFactory list

    @Autowired

    private List<SqlSessionFactory> sqlSessionFactoryList;

    @Autowired

    private PageHelperProperties properties;

    /**

*接受分页插件额外的属性

     *

     * @return

     */

    @Bean

    @ConfigurationProperties(prefix = PageHelperProperties.PAGEHELPER_PREFIX)

    public Properties pageHelperProperties() {

        return new Properties();

    }

//装载pageHepler插件

    @PostConstruct

    public void addPageInterceptor() {

        PageInterceptor interceptor = new PageInterceptor();

        Properties properties = new Properties();

//先把一般方式配置的属性放进去

        properties.putAll(pageHelperProperties());

//在把特殊配置放进去,由于close-conn 利用上面方式时,属性名就是 close-conn 而不是 closeConn,所以需要额外的一步

        properties.putAll(this.properties.getProperties());

        interceptor.setProperties(properties);

        for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {

//对所有的sqlSessionFactory装载pagehepler插件

            sqlSessionFactory.getConfiguration().addInterceptor(interceptor);

        }

    }

}

2、Mybatis-plus sqlSessionFactory执行插件源码解析

前面忽略一万步,感兴趣的同学可以自行沿着插件执行的源码往上扩展查看整个sqlSessionFactory是如何执行的,这里我们主要看sqlSessionFactory执行sql时,如何去执行插件的这一块

主要类及方法:

MybatisConfiguration:sqlSessionFactory上下文类。前文其实已经有提到了,该类主要提供了插件的添加和使用的方法。在sqlSessionFactory执行sql时,会调用newExecutor方法来调用依次调用所有的插件。

Java

//执行sql时,先执行所有sqlSessionFactory的拦截器插件

@Override

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {

    executorType = executorType == null ? defaultExecutorType : executorType;

    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;

    Executor executor;

    if (ExecutorType.BATCH == executorType) {

        executor = new MybatisBatchExecutor(this, transaction);

    } else if (ExecutorType.REUSE == executorType) {

        executor = new MybatisReuseExecutor(this, transaction);

    } else {

        executor = new MybatisSimpleExecutor(this, transaction);

    }

    if (cacheEnabled) {

        executor = new MybatisCachingExecutor(executor);

    }

    executor = (Executor) interceptorChain.pluginAll(executor);

    return executor;

}

InterceptorChain类:pluginAll方法依次去调用了所有拦截器的plugin方法

Java

public class InterceptorChain {

  private final List<Interceptor> interceptors = new ArrayList<>();

//依次调用拦截器方法,注意这里时for循环调用,即,拦截器调用是有先后顺序的。

  public Object pluginAll(Object target) {

    for (Interceptor interceptor : interceptors) {

//依次调用拦截器的plugin方法

      target = interceptor.plugin(target);

    }

    return target;

  }

//添加拦截器

  public void addInterceptor(Interceptor interceptor) {

    interceptors.add(interceptor);

  }

//获取所有拦截器

  public List<Interceptor> getInterceptors() {

    return Collections.unmodifiableList(interceptors);

  }

}

这里我们还是围绕着PaginationInterceptor分页插件来看具体调用

PaginationInterceptor类:mybatis-plus分页插件类

主要方法:

生成代理类

Java

@Override

public Object plugin(Object target) {

    if (target instanceof StatementHandler) {

        return Plugin.wrap(target, this);

    }

    return target;

}

TypeScript

//生成代理类

public static Object wrap(Object target, Interceptor interceptor) {

  Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);

  Class<?> type = target.getClass();

  Class<?>[] interfaces = getAllInterfaces(type, signatureMap);

  if (interfaces.length > 0) {

    return Proxy.newProxyInstance(

        type.getClassLoader(),

        interfaces,

        new Plugin(target, interceptor, signatureMap));

  }

  return target;

}

//生成代理类具体实现的方法

@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)) {

//执行拦截器中的intercept方法

      return interceptor.intercept(new Invocation(target, method, args));

    }

    return method.invoke(target, args);

  } catch (Exception e) {

    throw ExceptionUtil.unwrapThrowable(e);

  }

}

Java

@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对象

    IPage<?> page = null;

    if (paramObj instanceof IPage) {

        page = (IPage<?>) paramObj;

    } else if (paramObj instanceof Map) {

        for (Object arg : ((Map<?, ?>) paramObj).values()) {

            if (arg instanceof IPage) {

                page = (IPage<?>) arg;

                break;

            }

        }

    }

    /*

*不需要分页的场合,如果 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 = Optional.ofNullable(this.dbType).orElse(JdbcUtils.getDbType(connection.getMetaData().getURL()));

    IDialect dialect = Optional.ofNullable(this.dialect).orElse(DialectFactory.getDialect(dbType));

    String buildSql = concatOrderBy(originalSql, page);

    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();

}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容