MybatisPlus源码解析4:租户拦截器
本文主要是对租户拦截器进行解析,不同租户之间的数据隔离的,一个租户的数据查询、更新、插入、删除操作都不会对他的租户的数据产生任何影响
1.项目结构
源码地址:https://github.com/lmhdsad/mybatis-plus-source-study/tree/main/mybatis-plus-plugin-tenant
项目结构:
2. 源码分析 MybatisPlusInterceptor
@SuppressWarnings({"rawtypes"})
@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}),
}
)
public class MybatisPlusInterceptor implements Interceptor {
@Setter
private List<InnerInterceptor> interceptors = new ArrayList<>();
@Override
public Object intercept(Invocation invocation) throws Throwable {
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) {
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];
}
for (InnerInterceptor query : interceptors) {
if (!query.willDoQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql)) {
return Collections.emptyList();
}
query.beforeQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql);
}
CacheKey cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
return executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
} else if (isUpdate) {
for (InnerInterceptor update : interceptors) {
if (!update.willDoUpdate(executor, ms, parameter)) {
return -1;
}
update.beforeUpdate(executor, ms, parameter);
}
}
} else {
// StatementHandler
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();
}
@Override
public Object plugin(Object target) {
if (target instanceof Executor || target instanceof StatementHandler) {
return Plugin.wrap(target, this);
}
return target;
}
public void addInnerInterceptor(InnerInterceptor innerInterceptor) {
this.interceptors.add(innerInterceptor);
}
public List<InnerInterceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}
/**
* 使用内部规则,拿分页插件举个栗子:
* <p>
* - key: "@page" ,value: "com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor"
* - key: "page:limit" ,value: "100"
* <p>
* 解读1: key 以 "@" 开头定义了这是一个需要组装的 `InnerInterceptor`, 以 "page" 结尾表示别名
* value 是 `InnerInterceptor` 的具体的 class 全名
* 解读2: key 以上面定义的 "别名 + ':'" 开头指这个 `value` 是定义的该 `InnerInterceptor` 属性需要设置的值
* <p>
* 如果这个 `InnerInterceptor` 不需要配置属性也要加别名
*/
@Override
public void setProperties(Properties properties) {
PropertyMapper pm = PropertyMapper.newInstance(properties);
Map<String, Properties> group = pm.group(StringPool.AT);
group.forEach((k, v) -> {
InnerInterceptor innerInterceptor = ClassUtils.newInstance(k);
innerInterceptor.setProperties(v);
addInnerInterceptor(innerInterceptor);
});
}
}
- 可以看到MybatisPlusInterceptor 实现了Mybatis的拦截器Interceptor,对StatementHandler和Executor进行了代理。
- MybatisPlusInterceptor 中定义了MP自己实现的内部拦截器interceptors,这些拦截器可供用户自己配置(拿来就用),或者重写少量关键的业务逻辑。
见名知意,其中的TenantLineInterceptor就是我们的主题 - 由于拦截的方法过多且复杂,intercept方法主要是判断了拦截的是哪个方法(判断方法签名逻辑:判断拦截对象的类型Executor or MappedStatement、判断是读还是写逻辑、判断参数的个数),然后调用相应的InnerInterceptor对其进行增强。
- 总的来说MybatisPlusInterceptor只是拦截器的总入口,具体实现的逻辑在InnerInterceptor里面。
3. 租户拦截器-TenantLineInnerInterceptor
原理:在执行Sql之前,通过CCJSqlParserUtil对Sql的语义进行了解析,然后拼接租户的信息,最终形成新的Sql,然后通过Mybatis执行Sql。需要注意的是,租户拦截器的整个执行流程是在业务Sql执行之前,只是对这个原始Sql进行了拼接而已
就拿查询来讲,在MybatisPlusInterceptor#intercept方法里面调用了query.beforeQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql)
3.1. 入口 - TenantLineInnerInterceptor#beforeQuery
@Override
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
if (InterceptorIgnoreHelper.willIgnoreTenantLine(ms.getId())) {
return;
}
PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
mpBs.sql(parserSingle(mpBs.sql(), null));
}
- 通过缓存判断是否需要对Sql进行拼接
- 对boundSql进行封装,以便更简单的通过反射对Sql和参数进行修改
- 调用parserSingle方法对boundSql.sql进行解析,最后修改到boundSql.sql
3.2. 解析流程 - JsqlParserSupport#parserSingle & #processParser
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);
}
}
/**
* 执行 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;
}
- 打印了原始的Sql
- 通过CCJSqlParserUtil对sql进行了解析,并封装成Statement对象
- 打印了解析后完成的Sql
- Insert、Select、Update、Delete分别对Statement进行处理
3.3. 构建表达式- BaseMultiTableInnerInterceptor#processPlainSelect & builderExpression
/**
* 处理 PlainSelect
*/
protected void processPlainSelect(final PlainSelect plainSelect, final String whereSegment) {
//#3087 github
List<SelectItem> selectItems = plainSelect.getSelectItems();
if (CollectionUtils.isNotEmpty(selectItems)) {
selectItems.forEach(selectItem -> processSelectItem(selectItem, whereSegment));
}
// 处理 where 中的子查询
Expression where = plainSelect.getWhere();
processWhereSubSelect(where, whereSegment);
// 处理 fromItem
FromItem fromItem = plainSelect.getFromItem();
List<Table> list = processFromItem(fromItem, whereSegment);
List<Table> mainTables = new ArrayList<>(list);
// 处理 join
List<Join> joins = plainSelect.getJoins();
if (CollectionUtils.isNotEmpty(joins)) {
mainTables = processJoins(mainTables, joins, whereSegment);
}
// 当有 mainTable 时,进行 where 条件追加
if (CollectionUtils.isNotEmpty(mainTables)) {
plainSelect.setWhere(builderExpression(where, mainTables, whereSegment));
}
}
/**
* 处理条件
*/
protected Expression builderExpression(Expression currentExpression, List<Table> tables, final String whereSegment) {
// 没有表需要处理直接返回
if (CollectionUtils.isEmpty(tables)) {
return currentExpression;
}
// 构造每张表的条件
List<Expression> expressions = tables.stream()
.map(item -> buildTableExpression(item, currentExpression, whereSegment))
.filter(Objects::nonNull)
.collect(Collectors.toList());
// 没有表需要处理直接返回
if (CollectionUtils.isEmpty(expressions)) {
return currentExpression;
}
// 注入的表达式
Expression injectExpression = expressions.get(0);
// 如果有多表,则用 and 连接
if (expressions.size() > 1) {
for (int i = 1; i < expressions.size(); i++) {
injectExpression = new AndExpression(injectExpression, expressions.get(i));
}
}
if (currentExpression == null) {
return injectExpression;
}
if (currentExpression instanceof OrExpression) {
return new AndExpression(new Parenthesis(currentExpression), injectExpression);
} else {
return new AndExpression(currentExpression, injectExpression);
}
}
/**
* 构建租户条件表达式
*
* @param table 表对象
* @param where 当前where条件
* @param whereSegment 所属Mapper对象全路径(在原租户拦截器功能中,这个参数并不需要参与相关判断)
* @return 租户条件表达式
* @see BaseMultiTableInnerInterceptor#buildTableExpression(Table, Expression, String)
*/
@Override
public Expression buildTableExpression(final Table table, final Expression where, final String whereSegment) {
if (tenantLineHandler.ignoreTable(table.getName())) {
return null;
}
return new EqualsTo(getAliasColumn(table), tenantLineHandler.getTenantId());
}
- processPlainSelect方法对Where中的子查询、连表进行了解析
- 如果主表不为空,调用builderExpression方法构建表达式。Sql的封装最终保存在Expression里面,通过toString方法可以获取到最终的sql。而BinaryExpression表达式中有左表达式leftExpression(比如:tenant_id)、右表达式(t1)和中间的条件操作(=),而表达式的拼接位:左+操作符+右。所以,最终拼接成了 tenant_id = “t1”。
- buildTableExpression通过buildTableExpression方法,调用tenantLineHandler获取租户的字段名称(左表达式),租户的值(右表达式)构建了EqualsTo类型的表达式,它的操作符为“=”。