17.Mybatis-Plus入门系列(17)-多租户插件TenantLineInnerInterceptor源码解析
17.Mybatis-Plus入门系列(17)-多租户插件TenantLineInnerInterceptor源码解析
核心类
InnerInterceptor接口
InnerInterceptor内置插件接口,是MP提供的插件功能顶级接口。
定义了一些Slelect查询,Update更新时,进行前置处理的一些方法。
实现了此接口的实现类,可以添加到Mybatis插件中,最终实现拦截器功能,实际还是调用的Mybatis插件。
public interface InnerInterceptor {
/**
* 判断是否执行 {@link Executor#query(MappedStatement, Object, RowBounds, ResultHandler, CacheKey, BoundSql)}
* <p>
* 如果不执行query操作,则返回 {@link Collections#emptyList()}
*/
default boolean willDoQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
return true;
}
/**
* {@link Executor#query(MappedStatement, Object, RowBounds, ResultHandler, CacheKey, BoundSql)} 操作前置处理
* <p>
* 改改sql啥的
*/
default void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
// do nothing
}
/**
* 判断是否执行 {@link Executor#update(MappedStatement, Object)}
* <p>
* 如果不执行update操作,则影响行数的值为 -1
*/
default boolean willDoUpdate(Executor executor, MappedStatement ms, Object parameter) throws SQLException {
return true;
}
/**
* {@link Executor#update(MappedStatement, Object)} 操作前置处理
* <p>
* 改改sql啥的
*/
default void beforeUpdate(Executor executor, MappedStatement ms, Object parameter) throws SQLException {
// do nothing
}
/**
* {@link StatementHandler#prepare(Connection, Integer)} 操作前置处理
* <p>
* 改改sql啥的
*/
default void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
// do nothing
}
/**
* {@link StatementHandler#getBoundSql()} 操作前置处理
* <p>
* 只有 {@link BatchExecutor} 和 {@link ReuseExecutor} 才会调用到这个方法
*
* @param sh StatementHandler(可能是代理对象)
*/
default void beforeGetBoundSql(StatementHandler sh) {
// do nothing
}
default void setProperties(Properties properties) {
// do nothing
}
MybatisPlusInterceptor
MybatisPlusInterceptor类实现了原生Mybatis的Interceptor接口。
从它的Intercepts中,可以看到对执行器对象Executor的query和update进行了拦截。
@Intercepts(
{
@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}),
}
)
其内部维护了一个InnerInterceptor数组,我们可以在MybatisPlusConfig配置类中,添加这些拦截器。
@Setter
private List<InnerInterceptor> interceptors = new ArrayList<>();
在拦截逻辑中,可以看到,对Executor执行时,循环调用每个InnerInterceptor实现类的beforeQuery或者beforeUpdate方法。
JsqlParserSupport
JsqlParserSupport是一个抽象类,可以理解为JsqlParser框架的工具类,定义了一些处理Insert、Delete、Update、Select对象,执行 SQL 解析等方法。
public abstract class JsqlParserSupport {
/**
* 日志
*/
protected final Log logger = LogFactory.getLog(this.getClass());
public String parserSingle(String sql, Object obj) {
if (logger.isDebugEnabled()) {
logger.debug("original SQL: " + sql);
}
try {
Statement statement = CCJSqlParserUtil.parse(sql);
return processParser(statement, 0, sql, obj);
} catch (JSQLParserException e) {
throw ExceptionUtils.mpe("Failed to process, Error SQL: %s", e.getCause(), sql);
}
}
public String parserMulti(String sql, Object obj) {
if (logger.isDebugEnabled()) {
logger.debug("original SQL: " + sql);
}
try {
// fixed github pull/295
StringBuilder sb = new StringBuilder();
Statements statements = CCJSqlParserUtil.parseStatements(sql);
int i = 0;
for (Statement statement : statements.getStatements()) {
if (i > 0) {
sb.append(StringPool.SEMICOLON);
}
sb.append(processParser(statement, i, sql, obj));
i++;
}
return sb.toString();
} catch (JSQLParserException e) {
throw ExceptionUtils.mpe("Failed to process, Error SQL: %s", e.getCause(), sql);
}
}
/**
* 执行 SQL 解析
*
* @param statement JsqlParser Statement
* @return sql
*/
protected String processParser(Statement statement, int index, String sql, Object obj) {
if (logger.isDebugEnabled()) {
logger.debug("SQL to parse, SQL: " + sql);
}
if (statement instanceof Insert) {
this.processInsert((Insert) statement, index, sql, obj);
} else if (statement instanceof Select) {
this.processSelect((Select) statement, index, sql, obj);
} else if (statement instanceof Update) {
this.processUpdate((Update) statement, index, sql, obj);
} else if (statement instanceof Delete) {
this.processDelete((Delete) statement, index, sql, obj);
}
sql = statement.toString();
if (logger.isDebugEnabled()) {
logger.debug("parse the finished SQL: " + sql);
}
return sql;
}
/**
* 新增
*/
protected void processInsert(Insert insert, int index, String sql, Object obj) {
throw new UnsupportedOperationException();
}
/**
* 删除
*/
protected void processDelete(Delete delete, int index, String sql, Object obj) {
throw new UnsupportedOperationException();
}
/**
* 更新
*/
protected void processUpdate(Update update, int index, String sql, Object obj) {
throw new UnsupportedOperationException();
}
/**
* 查询
*/
protected void processSelect(Select select, int index, String sql, Object obj) {
throw new UnsupportedOperationException();
}
}
TenantLineHandler
租户处理器是一个接口,提供了TenantId 行级处理租户的功能,定义了一些获取租户 ID、获取租户字段名、忽略表的一些方法,这个接口需要用户自己去实现。
public interface TenantLineHandler {
/**
* 获取租户 ID 值表达式,只支持单个 ID 值
* <p>
*
* @return 租户 ID 值表达式
*/
Expression getTenantId();
/**
* 获取租户字段名
* <p>
* 默认字段名叫: tenant_id
*
* @return 租户字段名
*/
default String getTenantIdColumn() {
return "tenant_id";
}
/**
* 根据表名判断是否忽略拼接多租户条件
* <p>
* 默认都要进行解析并拼接多租户条件
*
* @param tableName 表名
* @return 是否忽略, true:表示忽略,false:需要解析并拼接多租户条件
*/
default boolean ignoreTable(String tableName) {
return false;
}
}
TenantLineInnerInterceptor
TenantLineInnerInterceptor就是实现多租户的插件了,其继承了JsqlParserSupport类,实现了InnerInterceptor接口。
内部维护了一个租户处理器。
private TenantLineHandler tenantLineHandler;
也提供了很多,执行SQL时,对SQL语句进行处理的方法。具体方法功能,流程分析的时候介绍。
Select流程分析
1. 进入MybatisPlusInterceptor
Executor执行时,会进入MybatisPlusInterceptor插件,获取我们需要的参数,然后进入TenantLineInnerInterceptor的beforeQuery方法。
beforeQuery方法的形参说明如下:
* @param executor Executor(可能是代理对象)
* @param ms MappedStatement
* @param parameter parameter
* @param rowBounds rowBounds
* @param resultHandler resultHandler
* @param boundSql boundSql
2. 进入TenantLineInnerInterceptor
首先进入beforeQuery,对SQL进行前置处理。
@Override
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
// 1. 缓存中查询该方法是不是配置了忽略此插件
if (InterceptorIgnoreHelper.willIgnoreTenantLine(ms.getId())) {
return;
}
// 2. 获取BoundSql
PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
// 3. 重新装载SQL,并设置到BoundSql中
mpBs.sql(parserSingle(mpBs.sql(), null));
}
对SQL进行前置处理,调用的是JsqlParserSupport的parserSingle方法,使用工具类解析SQL。
// 1. 调用JSqlParser工具类解析SQL为Statement
Statement statement = CCJSqlParserUtil.parse(sql);
// 2. 处理Statement
return processParser(statement, 0, sql, obj);
然后继续进入processParser方法,会对当前SQL的类型进行判断,然后处理。
if (statement instanceof Insert) {
this.processInsert((Insert)statement, index, sql, obj);
} else if (statement instanceof Select) {
this.processSelect((Select)statement, index, sql, obj);
} else if (statement instanceof Update) {
this.processUpdate((Update)statement, index, sql, obj);
} else if (statement instanceof Delete) {
this.processDelete((Delete)statement, index, sql, obj);
}
sql = statement.toString();
if (this.logger.isDebugEnabled()) {
this.logger.debug("parse the finished SQL: " + sql);
}
// 返回SQL
return sql;
因为我们是Select类型,所以进入TenantLineInnerInterceptor的processSelect方法。
@Override
protected void processSelect(Select select, int index, String sql, Object obj) {
// 1. 处理Select
processSelectBody(select.getSelectBody());
// 2. 处理 WithItem
List<WithItem> withItemsList = select.getWithItemsList();
if (!CollectionUtils.isEmpty(withItemsList)) {
withItemsList.forEach(this::processSelectBody);
}
}
processSelectBody方法会根据SelectBody实际类型,执行不同的处理方法。
protected void processSelectBody(SelectBody selectBody) {
if (selectBody == null) {
return;
}
if (selectBody instanceof PlainSelect) {
processPlainSelect((PlainSelect) selectBody);
} else if (selectBody instanceof WithItem) {
WithItem withItem = (WithItem) selectBody;
processSelectBody(withItem.getSelectBody());
} else {
SetOperationList operationList = (SetOperationList) selectBody;
List<SelectBody> selectBodys = operationList.getSelects();
if (CollectionUtils.isNotEmpty(selectBodys)) {
selectBodys.forEach(this::processSelectBody);
}
}
}
然后调用processPlainSelect方法处理 PlainSelect。
/**
* 处理 PlainSelect
*/
protected void processPlainSelect(PlainSelect plainSelect) {
// 1. 获取From 后面的元素(第一个)user u
FromItem fromItem = plainSelect.getFromItem();
// 2. 获取Where 条件 u.user_name = ?
Expression where = plainSelect.getWhere();
// 3. 添加 租户过滤SQL AND u.tenant_id = '0001'
processWhereSubSelect(where);
if (fromItem instanceof Table) {
Table fromTable = (Table) fromItem;
if (!tenantLineHandler.ignoreTable(fromTable.getName())) {
//#1186 github
// 调用tenantLineHandler 添加Where表达式。
plainSelect.setWhere(builderExpression(where, fromTable));
}
} else {
processFromItem(fromItem);
}
//#3087 github
List<SelectItem> selectItems = plainSelect.getSelectItems();
if (CollectionUtils.isNotEmpty(selectItems)) {
selectItems.forEach(this::processSelectItem);
}
// 4. 处理Join 语句 LEFT JOIN user_role ur ON u.user_id = ur.user_id
List<Join> joins = plainSelect.getJoins();
if (CollectionUtils.isNotEmpty(joins)) {
joins.forEach(j -> {
processJoin(j);
processFromItem(j.getRightItem());
});
}
}
对Where处理。
/**
* 处理where条件内的子查询
* <p>
* 支持如下:
* 1. in
* 2. =
* 3. >
* 4. <
* 5. >=
* 6. <=
* 7. <>
* 8. EXISTS
* 9. NOT EXISTS
* <p>
* 前提条件:
* 1. 子查询必须放在小括号中
* 2. 子查询一般放在比较操作符的右边
*
* @param where where 条件
*/
protected void processWhereSubSelect(Expression where) {
if (where == null) {
return;
}
if (where instanceof FromItem) {
processFromItem((FromItem) where);
return;
}
if (where.toString().indexOf("SELECT") > 0) {
// 有子查询
if (where instanceof BinaryExpression) {
// 比较符号 , and , or , 等等
BinaryExpression expression = (BinaryExpression) where;
processWhereSubSelect(expression.getLeftExpression());
processWhereSubSelect(expression.getRightExpression());
} else if (where instanceof InExpression) {
// in
InExpression expression = (InExpression) where;
ItemsList itemsList = expression.getRightItemsList();
if (itemsList instanceof SubSelect) {
processSelectBody(((SubSelect) itemsList).getSelectBody());
}
} else if (where instanceof ExistsExpression) {
// exists
ExistsExpression expression = (ExistsExpression) where;
processWhereSubSelect(expression.getRightExpression());
} else if (where instanceof NotExpression) {
// not exists
NotExpression expression = (NotExpression) where;
processWhereSubSelect(expression.getExpression());
} else if (where instanceof Parenthesis) {
Parenthesis expression = (Parenthesis) where;
processWhereSubSelect(expression.getExpression());
}
}
}
最终返回拼接了租户过滤条件的SQL ,设置到boundSql中,
3. 再次进入MybatisPlusInterceptor
Executor对象在进入MybatisPlusInterceptor获取到租户条件后,直接执行了以下代码,会创建缓存及执行Query。
CacheKey cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
return executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
我们知道Executor会调用StatementHandler去执行SQL,因为MybatisPlusInterceptor对StatementHandler的prepare也进行了拦截,所以会再次进入拦截器中。
会直接进入MybatisPlusInterceptor的以下逻辑。
在这里,会执行InnerInterceptor拦截器的beforePrepare方法。
TenantLineInnerInterceptor的beforePrepare如下所示:
/**
* StatementHandler =》prepare
* @param sh StatementHandler(可能是代理对象)
* @param connection Connection
* @param transactionTimeout transactionTimeout
*/
@Override
public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
// 1. 转换为MP的 StatementHandler
PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh);
MappedStatement ms = mpSh.mappedStatement();
SqlCommandType sct = ms.getSqlCommandType();
// 2. 不是查询,则会进入
if (sct == SqlCommandType.INSERT || sct == SqlCommandType.UPDATE || sct == SqlCommandType.DELETE) {
if (InterceptorIgnoreHelper.willIgnoreTenantLine(ms.getId())) return;
PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql();
mpBs.sql(parserMulti(mpBs.sql(), null));
}
}
更新流程
更新语句直接进入MybatisPlusInterceptor的如下方法执行beforeUpdate。
最终和查询差不多,也会生成携带租户的SQL:
来源:https://yunyanchengyu.blog.csdn.net/article/details/120076078