這是一個或許對你有用的社群
《專案實戰(影片)》:從書中學,往事上“練” 《網際網路高頻面試題》:面朝簡歷學習,春暖花開 《架構 x 系統設計》:摧枯拉朽,掌控面試高頻場景題 《精進 Java 學習指南》:系統學習,網際網路主流技術棧 《必讀 Java 原始碼專欄》:知其然,知其所以然
這是一個或許對你有用的開源專案
國產 Star 破 10w+ 的開源專案,前端包括管理後臺 + 微信小程式,後端支援單體和微服務架構。功能涵蓋 RBAC 許可權、SaaS 多租戶、資料許可權、商城、支付、工作流、大屏報表、微信公眾號、ERP、CRM、AI 大模型等等功能:
Boot 多模組架構:https://gitee.com/zhijiantianya/ruoyi-vue-pro Cloud 微服務架構:https://gitee.com/zhijiantianya/yudao-cloud 影片教程:https://doc.iocoder.cn 【國內首批】支援 JDK 17/21 + SpringBoot 3.3、JDK 8/11 + Spring Boot 2.7 雙版本
本文將介紹如何在 Spring Boot 專案中利用Mybatis的強大攔截器機制結合JSqlParser —— 一個功能豐富的 SQL 解析器,來輕鬆實現資料隔離的目標。本文根據示例展示如何根據當前的執行環境來實現資料隔離。
工具介紹
Mybatis攔截器
本文將透過攔截StatementHandler
介面的prepare
方法修改SQL語句,實現資料隔離的目的。
JSqlParser
-
能夠將 SQL 字串轉換成一個可操作的抽象語法樹(AST),這使得程式能夠理解和操作 SQL 語句的各個組成部分。 -
根據需求對解析出的AST進行修改,比如新增額外的過濾條件,然後再將AST轉換回SQL字串,實現需求定製化的SQL語句構建。

基於 Spring Boot + MyBatis Plus + Vue & Element 實現的後臺管理系統 + 使用者小程式,支援 RBAC 動態許可權、多租戶、資料許可權、工作流、三方登入、支付、簡訊、商城等功能
專案地址:https://github.com/YunaiV/ruoyi-vue-pro 影片教程:https://doc.iocoder.cn/video/
詳細步驟
1. 匯入依賴
<dependency>
<groupId>
org.mybatis.spring.boot
</groupId>
<artifactId>
mybatis-spring-boot-starter
</artifactId>
<version>
3.0.3
</version>
</dependency>
<dependency>
<groupId>
com.github.jsqlparser
</groupId>
<artifactId>
jsqlparser
</artifactId>
<version>
4.6
</version>
</dependency>
JSqlParser
的依賴。2. 定義一個攔截器
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
})
}
)
publicclassDataIsolationInterceptorimplementsInterceptor
{
/**
* 從配置檔案中環境變數
*/
@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) {
thrownew
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);
}
elseif
(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();
}
elseif
(statement
instanceof
Insert) {
Insert insert = (Insert) statement;
setEnvToInsert(insert);
return
insert.toString();
}
elseif
(statement
instanceof
Update) {
Update update = (Update) statement;
Expression newWhereExpression = setEnvToWhereExpression(update.getWhere(),
null
);
// 將新的where設定到Update中
update.setWhere(newWhereExpression);
return
update.toString();
}
elseif
(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 語法樹
*/
privatevoidsetEnvToInsert(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
<
selectid
=
"queryAllByOrgLevel"
resultType=
"com.lyx.mybatis.entity.AllInfo"
>
SELECT
a.username,a.code,o.org_code,o.org_name,o.level
FROMadmin
a
leftjoin
organize o
on
a.org_id=o.id
WHERE
a.dr=
0and
o.level=
#{level}
</
select
>
SELECT
a.username,a.code,o.org_code,o.org_name,o.level
FROMadmin
a
leftjoin
organize o
on
a.org_id=o.id
WHERE
a.dr=
0and
o.level=?
setEnvToStatement(originalSql)
方法後,得到的新 SQL 語句:SELECT
a.username, a.code, o.org_code, o.org_name, o.level
FROMadmin
a
LEFTJOIN
organize o
ON
a.org_id = o.id
WHERE
a.dr =
0AND
o.level = ?
AND
a.env =
'test'AND
o.env =
'test'
-
Insert
INSERTINTOadmin
(
id
, username, code, org_id )
VALUES
( ?, ?, ?, ? )
setEnvToInsert(insert)
方法後,得到的新 SQL 語句:INSERTINTOadmin
(
id
, username, code, org_id, env)
VALUES
(?, ?, ?, ?,
'test'
)
-
Update
UPDATEadminSET
username=?, code=?, org_id=?
WHEREid
=?
setWhere(newWhereExpression)
方法後,得到的新 SQL 語句:UPDATEadminSET
username = ?, code = ?, org_id = ?
WHEREid
= ?
AND
env =
'test'
-
Delete
DELETEFROMadminWHEREid
=?
setWhere(newWhereExpression)
方法後,得到的新 SQL 語句:DELETEFROMadminWHEREid
= ?
AND
env =
'test'
4. 為什麼要攔截 StatementHandler 介面的 prepare 方法?
@Signature
註解中攔截的是 StatementHandler
介面的 prepare
方法,為什麼攔截的是 prepare
方法而不是 query 和 update 方法?為什麼攔截 query 和 update 方法修改 SQL 語句後仍然執行的是原 SQL ?prepare
方法中被構建和引數化的。prepare
方法是負責準備 PreparedStatement
物件的,這個物件表示即將要執行的 SQL 語句。在 prepare
方法中可以對 SQL 語句進行修改,而這些修改將會影響最終執行的 SQL 。prepare
方法之後被呼叫的。它們主要的作用是執行已經準備好的 PreparedStatement
物件。在這個階段,SQL 語句已經被建立並綁定了引數值,所以攔截這兩個方法並不能改變已經準備好的 SQL 語句。WHERE
子句、改變排序規則等),那麼需要在 SQL 語句被準備之前進行攔截,即在 prepare
方法的執行過程中進行。-
解析配置和對映檔案: MyBatis 啟動時,首先載入配置檔案和對映檔案,解析裡面的 SQL 語句。 -
生成 StatementHandler 和 BoundSql: 當執行一個操作,比如查詢或更新時,MyBatis 會建立一個 StatementHandler 物件,幷包裝了 BoundSql 物件,後者包含了即將要執行的 SQL 語句及其引數。 -
執行 prepare 方法: StatementHandler 的 prepare 方法被呼叫,完成 PreparedStatement 的建立和引數設定。 -
執行 query 或 update: 根據執行的是查詢操作還是更新操作,MyBatis 再呼叫 query 或 update 方法來實際執行 SQL 。
prepare
方法進行攔截,我們可以在 SQL 語句被最終確定之前更改它,從而使修改生效。如果在 query 或 update 方法中進行攔截,則無法更改 SQL 語句,只能在執行前後進行其他操作,比如日誌記錄或者結果處理。




