前言
笔者默认大伙已经了解 Mybatis 插件机制(org.apache.ibatis.plugin.Interceptor)
探析
mybatis-plus-extension 介绍
mybatis-plus 扩展功能,包括分页,sql解析,spring集成。
通常情况下,都会结合 spring 去使用。所以便以此前提去分析。
在这个依赖中我们需要注意的是 com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor 类,他实现了 Mybatis 提供的 Interceptor,使它可以作为 Mybatis 的一个插件进行使用。
// Mybatis-plus 中自己的拦截器,更加细致的区分了 查询、增加、修改所需要的权限
@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();
}
// 进入查询前 Mybatis-plus 自己的拦截器中进行判断,这里也是实现数据权限的关键点
// 官方注释: 改改sql啥的
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();
}
InnerInterceptor 代码展示
此处就是 mybatis-plus 原样代码,未做任何修改
public interface InnerInterceptor {
/**
* 判断是否执行 {@link Executor#query(MappedStatement, Object, RowBounds, ResultHandler, CacheKey, BoundSql)}
* <p>
* 如果不执行query操作,则返回 {@link Collections#emptyList()}
*
* @param executor Executor(可能是代理对象)
* @param ms MappedStatement
* @param parameter parameter
* @param rowBounds rowBounds
* @param resultHandler resultHandler
* @param boundSql boundSql
* @return 新的 boundSql
*/
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啥的
*
* @param executor Executor(可能是代理对象)
* @param ms MappedStatement
* @param parameter parameter
* @param rowBounds rowBounds
* @param resultHandler resultHandler
* @param boundSql boundSql
*/
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
*
* @param executor Executor(可能是代理对象)
* @param ms MappedStatement
* @param parameter parameter
*/
default boolean willDoUpdate(Executor executor, MappedStatement ms, Object parameter) throws SQLException {
return true;
}
/**
* {@link Executor#update(MappedStatement, Object)} 操作前置处理
* <p>
* 改改sql啥的
*
* @param executor Executor(可能是代理对象)
* @param ms MappedStatement
* @param parameter parameter
*/
default void beforeUpdate(Executor executor, MappedStatement ms, Object parameter) throws SQLException {
// do nothing
}
/**
* {@link StatementHandler#prepare(Connection, Integer)} 操作前置处理
* <p>
* 改改sql啥的
*
* @param sh StatementHandler(可能是代理对象)
* @param connection Connection
* @param transactionTimeout transactionTimeout
*/
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
}
}
步骤
- com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor#beforeQuery
-
- com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor#beforeQuery (子类实现)
@Override
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
if (InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) {
return;
}
PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
// com.baomidou.mybatisplus.extension.parser.JsqlParserSupport
// 当前类继承了 JsqlParserSupport,用于处理 sql 改写
mpBs.sql(parserSingle(mpBs.sql(), ms.getId()));
}
- com.baomidou.mybatisplus.extension.parser.JsqlParserSupport#parserSingle
public String parserSingle(String sql, Object obj) {
if (logger.isDebugEnabled()) {
logger.debug("original SQL: " + sql);
}
try {
Statement statement = JsqlParserGlobal.parse(sql);
// 执行 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;
}
- com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor#processSelect
protected void processSelect(Select select, int index, String sql, Object obj) {
if (dataPermissionHandler == null) {
return;
}
if (dataPermissionHandler instanceof MultiDataPermissionHandler) {
// 参照 com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor.processSelect 做的修改
final String whereSegment = (String) obj;
processSelectBody(select, whereSegment);
List<WithItem> withItemsList = select.getWithItemsList();
if (!CollectionUtils.isEmpty(withItemsList)) {
withItemsList.forEach(withItem -> processSelectBody(withItem, whereSegment));
}
} else {
// 兼容原来的旧版 DataPermissionHandler 场景
if (select instanceof PlainSelect) {
this.setWhere((PlainSelect) select, (String) obj);
} else if (select instanceof SetOperationList) {
SetOperationList setOperationList = (SetOperationList) select;
List<Select> selectBodyList = setOperationList.getSelects();
selectBodyList.forEach(s -> this.setWhere((PlainSelect) s, (String) obj));
}
}
}
- com.baomidou.mybatisplus.extension.plugins.inner.BaseMultiTableInnerInterceptor#processSelectBody
protected void processSelectBody(Select selectBody, final String whereSegment) {
if (selectBody == null) {
return;
}
if (selectBody instanceof PlainSelect) {
// 根据判断,最终都会走到这里
processPlainSelect((PlainSelect) selectBody, whereSegment);
} else if (selectBody instanceof ParenthesedSelect) {
ParenthesedSelect parenthesedSelect = (ParenthesedSelect) selectBody;
processSelectBody(parenthesedSelect.getSelect(), whereSegment);
} else if (selectBody instanceof SetOperationList) {
SetOperationList operationList = (SetOperationList) selectBody;
List<Select> selectBodyList = operationList.getSelects();
if (CollectionUtils.isNotEmpty(selectBodyList)) {
selectBodyList.forEach(body -> processSelectBody(body, whereSegment));
}
}
}
/**
* 处理 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)) {
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()
// 这里就要到自己实现的时候了,BaseMultiTableInnerInterceptor 中是一个抽象方法
.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 ParenthesedExpressionList<>(currentExpression), injectExpression);
} else {
return new AndExpression(currentExpression, injectExpression);
}
}
- com.baomidou.mybatisplus.extension.plugins.inner.BaseMultiTableInnerInterceptor#buildTableExpression
-
- com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor#buildTableExpression
@Override
public Expression buildTableExpression(final Table table, final Expression where, final String whereSegment) {
if (dataPermissionHandler == null) {
return null;
}
// 只有新版数据权限处理器才会执行到这里
final MultiDataPermissionHandler handler = (MultiDataPermissionHandler) dataPermissionHandler;
// 这里就是自己需要根据情况去实现的方法了。
return handler.getSqlSegment(table, where, whereSegment);
}
可以参考笔者的一个简单实现 https://gitee.com/su_xing_kang/leyan-data-permiison/
总结
Mybatis-plus 是一款十分优秀的开源软件,能很方便的实现一些功能。在没有发现这个之前,我也自己基于 JdbcTemplate 、 JSqlParser 、 AspectJ 实现了一个数据权限,切面为 jsbcTemplate.query(…) 方法。但是比较难看,而且实现也比较难。做个记录让大家知道 mybatis-plus 还有这功能。需要注意的是,这个 com.baomidou.mybatisplus.extension.plugins.handler.MultiDataPermissionHandler 接口是在 3.5.2 才开始提供,旧版本的只能使用原来的,但实现大同小异,只是在功能上更加的丰富,使用上更加方便。