关于:高效数据隔离方案: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>
1.
2.
3.
4.
5.
JSqlParser 依赖:
复制
注意:如果项目选择了 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(
* 从配置文件中环境变量
*/
@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();
}
多表关联查询时,给关联的所有表加入环境隔离条件
* @param selectBody select语法树
* @return 新的SQL Where语法树
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}
<
刚进入拦截器时,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'
1.
2.
3.
Insert
刚进入拦截器时,Mybatis 解析的 SQL 语句:
复制
INSERT INTO admin( id, username, code, org_id )VALUES (?, ?, ?, ?)
1.
执行完 setEnvToInsert(insert) 方法后,得到的新 SQL 语句:
复制
INSERT INTO admin (id, username, code, org_id, env) VALUES (?, ?, ?, ?, 'test')
1.
Update
刚进入拦截器时,Mybatis 解析的 SQL 语句:
复制
UPDATE adminSET username=?, code=?, org_id=?WHERE id=?
1.
执行完 setWhere(newWhereExpression) 方法后,得到的新 SQL 语句:
复制
UPDATE admin SET username = ?, code = ?, org_id = ? WHERE id = ? AND env = 'test'
1.
Delete
刚进入拦截器时,Mybatis 解析的 SQL 语句:
复制
DELETE FROM admin WHERE id=?
1.
执行完 setWhere(newWhereExpression) 方法后,得到的新 SQL 语句:
复制
DELETE FROM admin WHERE id = ? AND env = 'test'
1.
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 语句,只能在执行前后进行其他操作,比如日志记录或者结果处理。
上述的结论表明分布式图数据库很大程度上能够给市场带来新的活力,让行业良好的发展。悦数图数据库是一款完全自主研发的国产图数据库和原生分布式图数据库,具有高性能,易扩展,安全稳定,自主可控的特点.万亿级数据仅需毫秒级查询延时,应用于金融风控,实时推荐,知识图谱等业务场景。https://www.yueshu.com.cn/https://www-cdn.yueshu.cn/yueshu-website/images/New-Pages/caselogo-pushi.png
页:
[1]