高效數據隔離方案:SpringBoot+JSqlParser 全解析
在構建多租戶系統或需要數據權限控制的應用時,數據隔離是一個關鍵問題,而解決這一問題的有效方案之一是在項目的數據庫訪問層實現數據過濾。
本文將介紹如何在 Spring Boot 項目中利用Mybatis的強大攔截器機制結合JSqlParser —— 一個功能豐富的 SQL 解析器,來輕松實現數據隔離的目標。本文根據示例展示如何根據當前的運行環境來實現數據隔離。
工具介紹
Mybatis攔截器
Mybatis 支持在 SQL 執行的不同階段攔截并插入自定義邏輯。
本文將通過攔截 StatementHandler 接口的 prepare方法修改SQL語句,實現數據隔離的目的。
JSqlParser
JSqlParser 是一個開源的 SQL 語句解析工具,它可以對 SQL 語句進行解析、重構等各種操作:
- 能夠將 SQL 字符串轉換成一個可操作的抽象語法樹(AST),這使得程序能夠理解和操作 SQL 語句的各個組成部分。
- 根據需求對解析出的AST進行修改,比如添加額外的過濾條件,然后再將AST轉換回SQL字符串,實現需求定制化的SQL語句構建。
SELECT語法樹簡圖:
詳細步驟
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 條件
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.RowConstructor;
import net.sf.jsqlparser.expression.StringValue;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.delete.Delete;
import net.sf.jsqlparser.statement.insert.Insert;
import net.sf.jsqlparser.statement.select.*;
import net.sf.jsqlparser.statement.update.Update;
import net.sf.jsqlparser.statement.values.ValuesStatement;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.List;
@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 ?關注公眾號:碼猿技術專欄,回復關鍵詞:1111 獲取阿里內部JVM調優手冊!
這是因為 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 語句,只能在執行前后進行其他操作,比如日志記錄或者結果處理。