前言

一个开源项目支持可扩展是很有必要的,可以基于此扩展来定制功能。就像Mybatis-Plus借助Mybatis简化开发,提升效率一样,就是在Mybatis的基础上进行了功能增强,其中MybatisPlusInterceptor就是一个扩展点。包括分页插件、多租户插件等等。

原理

首先,看下MybatisPlusInterceptor类,其实现了Mybatis的Interceptor实现sql前后的功能扩展。

// com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor

// MybatisPlus的内部拦截器,如果要实现自定义扩展
// 就可以通过实现InnerInterceptor完成。
private List<InnerInterceptor> interceptors = new ArrayList<>();

// 关键方法
// MybatisPlus的拦截器的核心逻辑
@Override
public Object intercept(Invocation invocation) throws Throwable {}

// MybatisPlus通过Plugin类代理Executor与StatementHandler接口
@Override
public Object plugin(Object target) {
     if (target instanceof Executor || target instanceof StatementHandler) {
          return Plugin.wrap(target, this);
      }
      return target;
}

Interceptor的加载

拦截器如何完成加载呢?
首先在配置文件中配置Bean,交由Spring管理MybatisPlusInterceptor。

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    // 分页插件
    interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
    // 乐观锁插件
    interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
    return interceptor;
}

在项目启动的时候,我们知道,会自动装配MybatisPlusAutoConfiguration类(SpringBoot自动配置),在MybatisPlusAutoConfiguration的实例创建时,如下:

private final Interceptor[] interceptors;

// 省略部分参数
public MybatisPlusAutoConfiguration(ObjectProvider<Interceptor[]> interceptorsProvider) {
        this.interceptors = interceptorsProvider.getIfAvailable();
       // 省略部分赋值
}

interceptors会通过 ObjectProvider<Interceptor[]> interceptorsProvider进行赋值。

ObjectProvider是Spring提供的用于延迟获取bean的一个接口,此处通过 getIfAvailable() 方法获取 Interceptor 数组实例。

后面在进行SqlSessionFactory创建的时候,将interceptors添加到了InterceptorChain(拦截器链,get/set拦截器实例)的interceptors集合中。

至此,启动的过程中完成了MybatisPlusInterceptor的启动配置

Executor的类结构

image

Executor是Mybatis中处理sql执行的4大核心组件之一,是MyBatis的执行器,它负责执行SQL语句。
还有另外三大组件ParameterHandler、StatementHandler、ResultSetHandler,分别用于对sql的参数处理、发送执行、结果转换。

Interceptor拦截Executor执行过程

1、在调用查询时,从openSqlSession()方法创建DefaultSqlSession开始看:

会进入
org.apache.ibatis.session.defaults.DefaultSqlSessionFactory#openSessionFromDataSource

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
   Transaction tx = null;
   try {
	  final Environment environment = configuration.getEnvironment();
	  final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
	  tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
	  // 创建Executor,往下看
	  final Executor executor = configuration.newExecutor(tx, execType);
	  // 创建SqlSession并返回
	  return new DefaultSqlSession(configuration, executor, autoCommit);
   } catch (Exception e) {} finally {}
}

2、debug看下创建Executor
image-1677852701626
默认的executorType是SIMPLE。
首先创建SimpleExecutor再创建CachingExecutor,将创建的SimpleExecutor传入给CachingExecutor的delegate变量。
同时将CachingExecutor对象传给BaseExecutor(SimpleExecutor的父类)的wrapper变量。

// CachingExecutor构造器
public CachingExecutor(Executor delegate) {
    this.delegate = delegate;
    delegate.setExecutorWrapper(this);
  }

赋值后关系如下:
image-1677852893415

3、使用Plugin代理Executor
创建完CachingExecutor后,接着看(步骤2图中代码):
executor = (Executor) interceptorChain.pluginAll(executor);

3.1、此处的target是CachingExecutor,继续看调用MybatisPlusInterceptor的plugin方法
image-1677853796962

循环进行target的代理,此处是Plugin代理Executor

3.2、MybatisPlusInterceptor的plugin方法
如果目标对象属于Executor或者StatementHandler(依靠PreparedStatement执行sql的接口)
image-1677853850083

3.3、最后交由Plugin类完成动态代理
image-1677853862986

其中Plugin#signatureMap标识MybatisPluginInterceptor的注解,表示需要拦截的方法,有如下:

@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}),
@Signature(type = StatementHandler.class, method = "getBoundSql", args = {}),
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),

So,将来执行Executor的查询方法时,会调用Plugin的invoke方法执行代理行为

经过了2,3步,完成了DefaultSqlSession创建,其中包括Executor的创建与代理,接下来继续跟进,在Executor执行的时候,Interceptor是如何进行拦截处理其扩展逻辑的。

4、在反射执行DefaultSqlSession查询时,交由Executor对象调用query查询,可以看到它是CachingExecutor的代理对象。
image-1677854185961

5、此时会进入到Plugin类的invoke方法,拦截器通过配置注解@Signature确定是否对当前的方法进行扩展
image-1677896434565

6、此时进入了MybatisPlusIntercepor的intercept,用于扩展自定义功能,可以对sql进行干预,是否执行等。

@Override
public Object intercept(Invocation invocation) throws Throwable {
	// target代理的Executor
    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) {
	        // 可以直接用args下标获取数据,因为拦截器会拦截的方法签名已经决定了
            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];
            }
            // MybatisPlus的内部拦截器,我们扩展实现其重写方法即可
            for (InnerInterceptor query : interceptors) {
            	// 是否进行查询
                if (!query.willDoQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql)) {
                    return Collections.emptyList();
                }
                // 查询之前做什么,例如分页sql处理
                query.beforeQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql);
            }
            // 缓存key
            CacheKey cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
            // list(new QueryWrapper())方法执行到这里
            return executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
        } else if (isUpdate) {
	        // 更新场景,对sql的干预
            for (InnerInterceptor update : interceptors) {
                if (!update.willDoUpdate(executor, ms, parameter)) {
                    return -1;
                }
                update.beforeUpdate(executor, ms, parameter);
            }
        }
    } else {
        // StatementHandler,对于进一步查询时的sql进行干预,此时已经获取到数据库连接
        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();
}

7、调用到CachingExecutor方法,如果满足缓存条件,则返回缓存数据(缓存的具体逻辑先跳过),进行进一步查询,进入SimpleExecutor
image-1677897514315
此处创建StatementHandler,其中prepareStatement()会进行数据库连接获取。(和Executor一样,StatementHandler也会交由Plugin进行代理,拦截器拦截StatementHandler的prepare和getBoundSql方法(见MybatisPlusInterceptor的注解)(具体StatementHandler的详细逻辑可以参考Executor进行debug))

贴一张StatementHandler的继承结构图:
image-1677897805686

执行完SimpleExecutor#doQuery()方法的“return handler.query(stmt, resultHandler);”便完成了正常的数据库查询。

至此,完成了数据库的查询MybatisPlusIntercetor的拦截器扩展

分页插件

再来看下分页插件的实现 PaginationInnerInterceptor
测试方法如下:

// 定义分页参数,查询第1页,每页查询2条数据
IPage page = new Page(1, 2);
IPage<User> userPage = userService.getBaseMapper()
                .selectPage(page, new QueryWrapper<>());

两个重写的核心方法

// PaginationInnerInterceptor implements InnerInterceptor

// 判断是否进行sql查询,通过查询count是否有数据完成
@Override
public boolean willDoQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    IPage<?> page = ParameterUtils.findPage(parameter).orElse(null);
    // 如果不存在IPage参数或者分页大小小于0或者配置不进行count查询,则直接返回
    if (page == null || page.getSize() < 0 || !page.searchCount()) {
        return true;
    }

    BoundSql countSql;
    // 获取指定的page.countId()的Statement,默认是null,直接返回countMs=null
    MappedStatement countMs = buildCountMappedStatement(ms, page.countId());
    if (countMs != null) {
        countSql = countMs.getBoundSql(parameter);
    } else {
	    // 根据分页sql构建一个查询对应分页的MappedStatement
        // MappedStatement的名称使用:原名称 + _mpCount
        countMs = buildAutoCountMappedStatement(ms);
        // 获取自动优化的 countSql,例如查询带?或分组,join情况等等
        String countSqlStr = autoCountSql(page, boundSql.getSql());
        PluginUtils.MPBoundSql mpBoundSql = PluginUtils.mpBoundSql(boundSql);
        // 创建对应的BoundSql,包括sql语句,查询参数等
        countSql = new BoundSql(countMs.getConfiguration(), countSqlStr, mpBoundSql.parameterMappings(), parameter);
        PluginUtils.setAdditionalParameter(countSql, mpBoundSql.additionalParameters());
    }

    CacheKey cacheKey = executor.createCacheKey(countMs, parameter, rowBounds, countSql);
    // 交由executor进行查询
    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);
    // 获取到total和页数后,根据页码决定是否进行继续查询
    // 如果继续,返回true,则进行真正的数据查询了
    return continuePage(page);
}

// 进行sql查询时,分页参数的拼接
@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.isNotEmpty(orders)) {
        addOrdered = true;
        buildSql = this.concatOrderBy(buildSql, orders);
    }

    // size 小于 0 且不限制返回值则不构造分页sql
    Long _limit = page.maxLimit() != null ? page.maxLimit() : maxLimit;
    if (page.getSize() < 0 && null == _limit) {
        if (addOrdered) {
            PluginUtils.mpBoundSql(boundSql).sql(buildSql);
        }
        return;
    }

    handlerLimit(page, _limit);
    // 查询分页方言类
    IDialect dialect = findIDialect(executor);

    final Configuration configuration = ms.getConfiguration();
    // 根据数据库方言进行分页语句的拼接,因为是使用mysql,所以会拼接limit进行分页
    DialectModel model = dialect.buildPaginationSql(buildSql, page.offset(), page.getSize());
    // 可以理解为一个代理类或适配器,还是对原BoundSql进行修改
    PluginUtils.MPBoundSql mpBoundSql = PluginUtils.mpBoundSql(boundSql);

    List<ParameterMapping> mappings = mpBoundSql.parameterMappings();
    Map<String, Object> additionalParameter = mpBoundSql.additionalParameters();
    // 消费参数,根据添加的参数,来补充参数名称与值
    // 例如mysql,limit ? 或 limit ?,?  有可能1个或2个,两个的参数名称
    // FIRST_PARAM_NAME = "mybatis_plus_first";
    // SECOND_PARAM_NAME = "mybatis_plus_second";
    // 将参数名称加入到List<ParameterMapping> mappings中
    // 将参数对应的值加到Map<String, Object> additionalParameter中
    model.consumers(mappings, configuration, additionalParameter);
    // 修改原sqlBound的sql属性,如:SELECT  id,name,age,email,tel_phone,deleted  FROM user 
 WHERE  deleted=0 LIMIT ?
    mpBoundSql.sql(model.getDialectSql());
    // mappings重新被赋给sqlBound,因为mappings被放到一个新集合中,所以才会被重新复制,而additionalParameter是直接get的,所以不需要。
    mpBoundSql.parameterMappings(mappings);
}

至此,分析了分页插件的实现过程,就是通过先count来决定要不要继续进行select,count的MappedStatement是有原selectd的MappedStatement生成的;在进行select时进行分页参数的拼接。


总结

1、MybatisPlusInterceptor实现了Mybatis的Interceptor接口完成了插件扩展,可以在执行sql前后进行干预,如多租户和分页等功能。
2、如果想扩展新的功能,可以实现Mybatis-Plus的InnerInterceptor接口,重写其方法,如willDoQuery、beforeQuery等。
3、介绍了分页插件的实现,通过出入参IPage进行判断及MybatisPlusInterceptor功能进行sql的干预实现。