Spring Boot + JSqlParser:全面解析數據隔離最佳實踐
在構建多租戶系統或需要進行數據權限控制的應用時,數據隔離是一個至關重要的課題。不同租戶之間的數據隔離不僅能夠確保數據的安全性,還能提高系統的靈活性和可維護性。隨著業務的擴展和需求的變化,單純依靠傳統的分表分庫策略往往難以滿足日益復雜的業務場景,而更加精細的權限控制和數據隔離機制顯得尤為關鍵。
在這種背景下,Spring Boot結合Mybatis的強大攔截器機制,以及JSqlParser作為SQL解析工具,為我們提供了一個行之有效的解決方案。通過在數據庫訪問層對SQL進行動態過濾和改造,我們可以在不同的查詢、插入、更新、刪除操作中靈活地加入租戶信息,從而實現多租戶數據的有效隔離。本文將深入介紹如何利用這兩者的優勢,借助攔截器與SQL解析技術,在不修改現有數據結構的基礎上,實現對數據的透明隔離。
工具簡介
MyBatis 攔截器
MyBatis 提供了豐富的攔截機制,允許在 SQL 執行的各個階段插入自定義邏輯。本文將通過攔截 StatementHandler 接口的 prepare 方法來修改 SQL 語句,實現數據隔離的目標。
JSqlParser
JSqlParser 是一個開源的 SQL 解析工具,支持 SQL 語句的解析、重構等多種操作。它能夠將 SQL 字符串轉化為抽象語法樹(AST),并允許程序操作和修改 SQL 語句的各個部分。通過對解析后的 AST 進行修改(例如添加環境變量過濾條件),我們可以在 SQL 查詢中實現動態的數據隔離。
實現步驟
添加依賴
在 pom.xml 文件中添加 MyBatis 和 JSqlParser 的依賴:
<!-- 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 自帶這兩個依賴并且確保它們的兼容性。避免重復添加,避免版本沖突。
定義攔截器
我們通過自定義攔截器來修改所有查詢 SQL,動態加入基于環境變量的過濾條件。
package com.icoderoad.interceptor;import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.Expression;
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.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.select.*;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.plugin.*;import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;import java.sql.Connection;@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;@Overridepublic Object intercept(Invocation invocation) throws Throwable {Object target = invocation.getTarget();if (target instanceof StatementHandler) {StatementHandler statementHandler = (StatementHandler) target;BoundSql boundSql = statementHandler.getBoundSql();String originalSql = boundSql.getSql();String newSql = applyEnvFilter(originalSql);boundSql.setSql(newSql); // 更新SQL語句}return invocation.proceed(); // 執行SQL}private String applyEnvFilter(String originalSql) {Statement statement;try {statement = CCJSqlParserUtil.parse(originalSql);} catch (JSQLParserException e) {throw new RuntimeException("SQL解析失敗: " + originalSql, e);}if (statement instanceof Select) {Select select = (Select) statement;PlainSelect selectBody = (PlainSelect) select.getSelectBody();Expression newWhereExpression = addEnvCondition(selectBody.getWhere());selectBody.setWhere(newWhereExpression);}return statement.toString(); // 返回修改后的SQL語句}private Expression addEnvCondition(Expression whereExpression) {// 生成用于數據隔離的 WHERE 條件AndExpression andExpression = new AndExpression();EqualsTo equalsTo = new EqualsTo();equalsTo.setLeftExpression(new Column("env"));equalsTo.setRightExpression(new StringValue(env));if (whereExpression == null) {return equalsTo;} else {andExpression.setLeftExpression(whereExpression);andExpression.setRightExpression(equalsTo);return andExpression;}}
}
測試查詢
假設有以下 SQL 查詢:
<select id="queryAllByOrgLevel" resultType="com.icoderoad.entity.AllInfo">SELECT a.username, a.code, o.org_code, o.org_name, o.levelFROM admin aLEFT JOIN organize o ON a.org_id = o.idWHERE a.dr = 0 AND o.level = #{level}
</select>
修改前:
原始 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 = ?
修改后:
經過攔截器處理后:
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、UPDATE 和 DELETE 操作,我們同樣可以在 SQL 語句中添加 env 字段:
INSERT
在插入數據時,env 字段會自動添加到 SQL 語句中:
INSERT INTO admin (id, username, code, org_id, env) VALUES (?, ?, ?, ?, 'test')
UPDATE
更新操作會在 WHERE 子句中添加 env 條件:
UPDATE admin SET username = ?, code = ?, org_id = ? WHERE id = ? AND env = 'test'
DELETE
刪除操作也會被加上 env 條件:
DELETE FROM admin WHERE id = ? AND env = 'test'
為什么攔截 prepare 方法?
在 MyBatis 中,prepare 方法負責準備 SQL 語句和參數綁定,而 query 和 update 方法主要執行已經準備好的 PreparedStatement。通過攔截 prepare 方法,我們可以確保 SQL 在執行前就已經被修改,從而實現對數據隔離的控制。