Spring Boot + JSqlParser:全面解析数据隔离最佳实践

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 的依赖:

xml 复制代码
<!-- 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,动态加入基于环境变量的过滤条件。

java 复制代码
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;

    @Override
    public 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 查询:

xml 复制代码
<select id="queryAllByOrgLevel" resultType="com.icoderoad.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>

修改前:

原始 SQL 查询:

bash 复制代码
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 = ?

修改后:

经过拦截器处理后:

bash 复制代码
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 语句中:

bash 复制代码
INSERT INTO admin (id, username, code, org_id, env) VALUES (?, ?, ?, ?, 'test')
UPDATE

更新操作会在 WHERE 子句中添加 env 条件:

bash 复制代码
UPDATE admin SET username = ?, code = ?, org_id = ? WHERE id = ? AND env = 'test'

DELETE

删除操作也会被加上 env 条件:

bash 复制代码
DELETE FROM admin WHERE id = ? AND env = 'test'

为什么拦截 prepare 方法?

在 MyBatis 中,prepare 方法负责准备 SQL 语句和参数绑定,而 query 和 update 方法主要执行已经准备好的 PreparedStatement。通过拦截 prepare 方法,我们可以确保 SQL 在执行前就已经被修改,从而实现对数据隔离的控制。

相关推荐
Lei活在当下2 小时前
先用起来,再理解,关于协程Coroutine应该知道的事
android·java·jvm
Java爱好狂.2 小时前
Java程序员体系化学习路线(2026最新版)
java·后端·java面试·java架构师·java程序员·java八股文·java学习路线
tongluowan0073 小时前
以ReentrantLock为例解释AQS的工作流程
java·模板方法模式·aqs·reentrantlock
装不满的克莱因瓶3 小时前
SpringBoot 如何将 lib 目录中jar包打包进最终的jar包里面
spring boot·后端·maven·jar·mvn
身如柳絮随风扬4 小时前
Java 项目打包与部署完全指南:JAR vs WAR,从构建到运行
java·firefox·jar
云烟成雨TD4 小时前
Spring AI Alibaba 1.x 系列【62】时光旅行(Time-Travel)
java·人工智能·spring
浩少7024 小时前
【无标题】
java·开发语言
一棵白菜4 小时前
java 学习
java
卷毛的技术笔记5 小时前
Java后端硬核实战:用Spring AI Alibaba+Redis给LLM装上“超强记忆中枢”
java·人工智能·redis·后端·spring·ai·系统架构
AKA__Zas6 小时前
初识多线程(3.0)
java·开发语言·学习方法