从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 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 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 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 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 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, Set> 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 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 mappings = new ArrayList<>(boundSql.getParameterMappings());

    Map additionalParameters = (Map) 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();

}

你可能感兴趣的:(从PaginationInterceptor分页实现过程看mybatis-plus插件原理解析)