工具介绍
Mybatis拦截器
Mybatis 支持在 SQL 执行的不同阶段拦截并插入自定义逻辑。
本文将通过拦截 StatementHandler
接口的 prepare
方法修改SQL语句,实现数据隔离的目的。
JSqlParser
JSqlParser 是一个开源的 SQL 语句解析工具,它可以对 SQL 语句进行解析、重构等各种操作:
-
能够将 SQL 字符串转换成一个可操作的抽象语法树(AST),这使得程序能够理解和操作 SQL 语句的各个组成部分。
-
根据需求对解析出的AST进行修改,比如添加额外的过滤条件,然后再将AST转换回SQL字符串,实现需求定制化的SQL语句构建。
SELECT语法树简图:
这或许是一个对你有用的开源项目,mall项目是一套基于 SpringBoot3 + Vue 的电商系统(Github标星60K),后端支持多模块和 2024最新微服务架构 ,采用Docker和K8S部署。包括前台商城项目和后台管理系统,能支持完整的订单流程!涵盖商品、订单、购物车、权限、优惠券、会员、支付等功能!
Boot项目:https://github.com/macrozheng/mall
Cloud项目:https://github.com/macrozheng/mall-swarm
视频教程:https://www.macrozheng.com/video/
项目演示:
详细步骤
1. 导入依赖
Mybatis 依赖:
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
JSqlParser 依赖:
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
<version>4.6</version>
</dependency>
注意: 如果项目选择了 Mybatis Plus 作为数据持久层框架,那么就无需另外添加 Mybatis 和 JSqlParser 的依赖。Mybatis Plus 自身已经包含了这两项依赖,并且保证了它们之间的兼容性。重复添加这些依赖可能会引起版本冲突,从而干扰项目的稳定性。
2. 定义一个拦截器
拦截所有 query 语句并在条件中加入 env 条件
@Component
@Intercepts(
{
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
}
)
public class DataIsolationInterceptor implements Interceptor {
/**
* 从配置文件中环境变量
*/
@Value("${spring.profiles.active}")
private String env;
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object target = invocation.getTarget();
//确保只有拦截的目标对象是 StatementHandler 类型时才执行特定逻辑
if (target instanceof StatementHandler) {
StatementHandler statementHandler = (StatementHandler) target;
// 获取 BoundSql 对象,包含原始 SQL 语句
BoundSql boundSql = statementHandler.getBoundSql();
String originalSql = boundSql.getSql();
String newSql = setEnvToStatement(originalSql);
// 使用MetaObject对象将新的SQL语句设置到BoundSql对象中
MetaObject metaObject = SystemMetaObject.forObject(boundSql);
metaObject.setValue("sql", newSql);
}
// 执行SQL
return invocation.proceed();
}
private String setEnvToStatement(String originalSql) {
net.sf.jsqlparser.statement.Statement statement;
try {
statement = CCJSqlParserUtil.parse(originalSql);
} catch (JSQLParserException e) {
throw new RuntimeException("EnvironmentVariableInterceptor::SQL语句解析异常:"+originalSql);
}
if (statement instanceof Select) {
Select select = (Select) statement;
PlainSelect selectBody = select.getSelectBody(PlainSelect.class);
if (selectBody.getFromItem() instanceof Table) {
Expression newWhereExpression;
if (selectBody.getJoins() == null || selectBody.getJoins().isEmpty()) {
newWhereExpression = setEnvToWhereExpression(selectBody.getWhere(), null);
} else {
// 如果是多表关联查询,在关联查询中新增每个表的环境变量条件
newWhereExpression = multipleTableJoinWhereExpression(selectBody);
}
// 将新的where设置到Select中
selectBody.setWhere(newWhereExpression);
} else if (selectBody.getFromItem() instanceof SubSelect) {
// 如果是子查询,在子查询中新增环境变量条件
// 当前方法只能处理单层子查询,如果有多层级的子查询的场景需要通过递归设置环境变量
SubSelect subSelect = (SubSelect) selectBody.getFromItem();
PlainSelect subSelectBody = subSelect.getSelectBody(PlainSelect.class);
Expression newWhereExpression = setEnvToWhereExpression(subSelectBody.getWhere(), null);
subSelectBody.setWhere(newWhereExpression);
}
// 获得修改后的语句
return select.toString();
} else if (statement instanceof Insert) {
Insert insert = (Insert) statement;
setEnvToInsert(insert);
return insert.toString();
} else if (statement instanceof Update) {
Update update = (Update) statement;
Expression newWhereExpression = setEnvToWhereExpression(update.getWhere(),null);
// 将新的where设置到Update中
update.setWhere(newWhereExpression);
return update.toString();
} else if (statement instanceof Delete) {
Delete delete = (Delete) statement;
Expression newWhereExpression = setEnvToWhereExpression(delete.getWhere(),null);
// 将新的where设置到delete中
delete.setWhere(newWhereExpression);
return delete.toString();
}
return originalSql;
}
/**
* 将需要隔离的字段加入到SQL的Where语法树中
* @param whereExpression SQL的Where语法树
* @param alias 表别名
* @return 新的SQL Where语法树
*/
private Expression setEnvToWhereExpression(Expression whereExpression, String alias) {
// 添加SQL语法树的一个where分支,并添加环境变量条件
AndExpression andExpression = new AndExpression();
EqualsTo envEquals = new EqualsTo();
envEquals.setLeftExpression(new Column(StringUtils.isNotBlank(alias) ? String.format("%s.env", alias) : "env"));
envEquals.setRightExpression(new StringValue(env));
if (whereExpression == null){
return envEquals;
} else {
// 将新的where条件加入到原where条件的右分支树
andExpression.setRightExpression(envEquals);
andExpression.setLeftExpression(whereExpression);
return andExpression;
}
}
/**
* 多表关联查询时,给关联的所有表加入环境隔离条件
* @param selectBody select语法树
* @return 新的SQL Where语法树
*/
private Expression multipleTableJoinWhereExpression(PlainSelect selectBody){
Table mainTable = selectBody.getFromItem(Table.class);
String mainTableAlias = mainTable.getAlias().getName();
// 将 t1.env = ENV 的条件添加到where中
Expression newWhereExpression = setEnvToWhereExpression(selectBody.getWhere(), mainTableAlias);
List<Join> joins = selectBody.getJoins();
for (Join join : joins) {
FromItem joinRightItem = join.getRightItem();
if (joinRightItem instanceof Table) {
Table joinTable = (Table) joinRightItem;
String joinTableAlias = joinTable.getAlias().getName();
// 将每一个join的 tx.env = ENV 的条件添加到where中
newWhereExpression = setEnvToWhereExpression(newWhereExpression, joinTableAlias);
}
}
return newWhereExpression;
}
/**
* 新增数据时,插入env字段
* @param insert Insert 语法树
*/
private void setEnvToInsert(Insert insert) {
// 添加env列
List<Column> columns = insert.getColumns();
columns.add(new Column("env"));
// values中添加环境变量值
List<SelectBody> selects = insert.getSelect().getSelectBody(SetOperationList.class).getSelects();
for (SelectBody select : selects) {
if (select instanceof ValuesStatement){
ValuesStatement valuesStatement = (ValuesStatement) select;
ExpressionList expressions = (ExpressionList) valuesStatement.getExpressions();
List<Expression> values = expressions.getExpressions();
for (Expression expression : values){
if (expression instanceof RowConstructor) {
RowConstructor rowConstructor = (RowConstructor) expression;
ExpressionList exprList = rowConstructor.getExprList();
exprList.addExpressions(new StringValue(env));
}
}
}
}
}
}
3. 测试
Select
Mapper:
<select id="queryAllByOrgLevel" resultType="com.lyx.mybatis.entity.AllInfo">
SELECT a.username,a.code,o.org_code,o.org_name,o.level
FROM admin a left join organize o on a.org_id=o.id
WHERE a.dr=0 and o.level=#{level}
</select>
刚进入拦截器时,Mybatis 解析的 SQL 语句:
SELECT a.username,a.code,o.org_code,o.org_name,o.level
FROM admin a left join organize o on a.org_id=o.id
WHERE a.dr=0 and o.level=?
执行完 setEnvToStatement(originalSql)
方法后,得到的新 SQL 语句:
SELECT a.username, a.code, o.org_code, o.org_name, o.level
FROM admin a LEFT JOIN organize o ON a.org_id = o.id
WHERE a.dr = 0 AND o.level = ? AND a.env = 'test' AND o.env = 'test'
Insert
刚进入拦截器时,Mybatis 解析的 SQL 语句:
INSERT INTO admin ( id, username, code, org_id ) VALUES ( ?, ?, ?, ? )
执行完 setEnvToInsert(insert)
方法后,得到的新 SQL 语句:
INSERT INTO admin (id, username, code, org_id, env) VALUES (?, ?, ?, ?, 'test')
Update
刚进入拦截器时,Mybatis 解析的 SQL 语句:
UPDATE admin SET username=?, code=?, org_id=? WHERE id=?
执行完 setWhere(newWhereExpression)
方法后,得到的新 SQL 语句:
UPDATE admin SET username = ?, code = ?, org_id = ? WHERE id = ? AND env = 'test'
Delete
刚进入拦截器时,Mybatis 解析的 SQL 语句:
DELETE FROM admin WHERE id=?
执行完 setWhere(newWhereExpression)
方法后,得到的新 SQL 语句:
DELETE FROM admin WHERE id = ? AND env = 'test'
4. 为什么要拦截 StatementHandler 接口的 prepare 方法?
可以注意到,在这个例子中定义拦截器时@Signature
注解中拦截的是 StatementHandler
接口的 prepare
方法,为什么拦截的是 prepare
方法而不是 query
和 update
方法?为什么拦截 query
和 update
方法修改 SQL 语句后仍然执行的是原 SQL ?
这是因为 SQL 语句是在 prepare
方法中被构建和参数化的。prepare
方法是负责准备 PreparedStatement
对象的,这个对象表示即将要执行的 SQL 语句。在 prepare
方法中可以对 SQL 语句进行修改,而这些修改将会影响最终执行的 SQL 。
而 query
和 update
方法是在 prepare
方法之后被调用的。它们主要的作用是执行已经准备好的 PreparedStatement
对象。在这个阶段,SQL 语句已经被创建并绑定了参数值,所以拦截这两个方法并不能改变已经准备好的 SQL 语句。
简单来说,如果想要修改SQL语句的内容(比如增加 WHERE 子句、改变排序规则等),那么需要在 SQL 语句被准备之前进行拦截,即在 prepare
方法的执行过程中进行。
以下是 MyBatis 执行过程中的几个关键步骤:
-
解析配置和映射文件:MyBatis 启动时,首先加载配置文件和映射文件,解析里面的 SQL 语句。
-
生成
StatementHandler
和BoundSql
:当执行一个操作,比如查询或更新时,MyBatis 会创建一个StatementHandler
对象,并包装了BoundSql
对象,后者包含了即将要执行的 SQL 语句及其参数。 -
执行
prepare
方法:StatementHandler
的prepare
方法被调用,完成PreparedStatement
的创建和参数设置。 -
执行
query
或update
:根据执行的是查询操作还是更新操作,MyBatis 再调用query
或update
方法来实际执行 SQL 。 -
通过在
prepare
方法进行拦截,我们可以在 SQL 语句被最终确定之前更改它,从而使修改生效。如果在query
或update
方法中进行拦截,则无法更改 SQL 语句,只能在执行前后进行其他操作,比如日志记录或者结果处理。