基于 MyBatis 拦截器,通过动态 SQL 解析引擎,一处标注,全局生效。告别满项目注解标注,让数据治理回归简单。
一、背景与动机
在日常开发中,我们经常会遇到以下几类需求:
- 敏感字段加密:手机号、身份证号、银行卡号需要入库加密、查询解密
- 数据权限隔离:不同部门/租户的数据需要隔离,查询时需要拼接对应的 WHERE 条件
- 异构数据库迁移:项目从 MySQL 迁移到达梦等国产数据库,很多SQL 语法不兼容,需要重写
- 字段自动填充:创建时间、修改人、租户 ID 等字段需要在插入和更新时频繁维护
市面上的方案,要么只能兼容部分写法,要么要求在 Controller、Service、Mapper、入参响应层进行大量改动。一张业务表的加密功能,可能需要修改项目中十几处甚至几十处代码。
【sangsang 】 的目标就是:对业务代码零侵入,一处标注,全局生效。
二、【sangsang】 是什么
【sangsang】 是一个基于 MyBatis 拦截器的透明数据治理框架,通过动态 SQL 解析引擎(基于 JSQLParser)在 SQL 执行前后自动改写 SQL,实现对业务代码零侵入的数据治理。
核心设计理念:
- 零侵入 :所有 Controller、Service、Mapper、入参响应层都不需要改动,只在
@TableName标注的实体类字段上做标注 - 全局生效:一处标注,全项目应用。不需要在每一个 Mapper 方法上加注解
- 灵活扩展:支持自定义策略和算法
- 性能优化:LRU 缓存 SQL 解析结果,避免重复解析
技术栈:Java 1.8+、MyBatis、JSQLParser
开源协议:Apache 2.0
三、核心功能
3.1 字段加解密
最常见的场景:用户表中存储了手机号、身份证号等敏感信息,需要在入库时加密、查询时解密。
sangsang 提供两种加密模式:
| 模式 | 原理 | 特点 |
|---|---|---|
| db 模式(推荐) | 利用数据库自身的加解密函数,框架自动改写 SQL | 支持密文模糊查询,支持列运算 |
| pojo 模式 | 在 Java 层对 SQL 入参和响应进行加解密处理 | 算法选择性强,无数据库计算开销 |
db 模式工作原理:
sql
-- 原始 SQL
SELECT user_name, phone FROM tb_user WHERE phone = ?
-- 框架自动改写后
SELECT
数据库解密函数(phone) AS phone,
user_name
FROM tb_user
WHERE phone = 数据库加密函数(?)
pojo模式工作原理:
diff
-- 不改变执行的sql
-- 在mybatis执行sql前后,将入参和响应的字段调用java代码自动加解密处理
快速接入
Step 1:引入依赖
xml
<dependency>
<groupId>io.gitee.tired-of-the-water</groupId>
<artifactId>sangsang-core</artifactId>
<version>3.7.0-alpha</version>
</dependency>
Step 2:开启配置
ini
# 配置实体类扫描路径(框架从此路径缓存需要加密的字段)
sangsang.scanEntityPackage[0]=com.sangsang.*.entity
# 加密模式:db 或 pojo
sangsang.encryptor.patternType=db
# 自定义密钥(可选,有默认值)
sangsang.encryptor.secretKey=TIREDTHEWATER
Step 3:创建 @TableName 实体类并标注字段
kotlin
@TableName("tb_user") //标识这个是tb_user表的实体类
public class User {
@FieldEncryptor//标识这个字段需要密文存储,采用默认的算法
private String phone;
@FieldEncryptor//标识这个字段需要密文存储,采用默认的算法
private String idCard;
}
若项目未使用 MyBatis-Plus 没有实体类,在扫描路径下创建一个
@TableName标注的类即可,仅保留需要标注的字段。
完成以上三步后,项目中所有涉及 tb_user 表 phone 和 idCard 字段的 SQL 均会自动处理加解密,包括 INSERT、UPDATE、SELECT 以及 WHERE 条件中的各种场景。
核心亮点:密文模糊查询
传统加密方案下,加密后的数据无法进行 LIKE 查询,而 db 模式利用数据库函数可以在密文状态下直接完成模糊匹配,解决了加密场景下的一个核心痛点。
更多能力
- 多算法共存 :不同字段可以使用不同的加密算法,通过
@FieldEncryptor(算法类.class)指定 - 自定义算法 :实现
FieldEncryptorStrategy接口即可扩展 - 分库分表支持 :通过
@ShardingTableEncryptor注解 +ShardingTableStrategy接口,避免为每张分表重复创建实体类
3.2 异构数据库语法转换
信创改造、数据库迁移是很多企业正在面临的课题。从 MySQL 迁移到达梦(DM)、从 Oracle 切换到 MySQL,SQL 语法的差异往往让人头疼。
sangsang 基于 JSQLParser 解析 SQL 语法树,自动将源数据库语法转换为目标数据库语法:
- MySQL -> 达梦:已完成,有项目生产验证
- Oracle -> MySQL:开发中,目前仅实现了 SELECT 常见语法的转换(3.7.0 分支)
转换效果示例:
sql
-- 原始 MySQL 语法
SELECT `user_name`, `phone` FROM `tb_user` WHERE `user_name` LIKE 'admin%'
SELECT DATE_FORMAT(create_time, '%Y-%m-%d') FROM tb_user
-- 自动转换为达梦语法
SELECT "user_name", "phone" FROM "tb_user" WHERE "user_name" LIKE 'admin%'
SELECT TO_CHAR(create_time, 'YYYY-MM-DD') FROM tb_user
框架自动处理:反引号转双引号、日期函数转换、分页语法转换、标识符转换等。
快速接入
Step 1:引入依赖
xml
<dependency>
<groupId>io.gitee.tired-of-the-water</groupId>
<artifactId>sangsang-core</artifactId>
<version>3.7.0-alpha</version>
</dependency>
Step 2:开启配置
ini
# 选择语法转换模式
sangsang.transformation.patternType=mysql2dm
Step 3:切换数据库驱动和连接地址
略
完成以上三步,所有 MySQL 语法的 SQL 会自动转换为达梦语法执行,业务代码和 Mapper XML 中的 SQL 完全不需要修改。
更多能力
- 自定义语法转换器 :实现
ColumnTransform、FunctionTransform等接口即可扩展,参考com.sangsang.transformation.mysql2dm包下的实现 - 扩展其他数据库:框架预留了扩展口,可借助大模型快速实现其他异构数据库的转换(如 PostgreSQL -> MySQL 等)
3.3 数据权限隔离
多租户系统、多部门系统中,不同用户只能看到属于自己的数据。传统方案需要在每一个查询 SQL 中手动拼接权限条件,维护成本极高。
sangsang 提供了零侵入的数据权限隔离方案,效果示例:
sql
-- 原始 SQL
SELECT * FROM tb_order WHERE phone = 'xxx' OR name = 'yyy'
-- PC 端(按组织隔离)自动改写后
SELECT * FROM tb_order
WHERE (phone = 'xxx' OR name = 'yyy')
AND org_id = 当前登录用户的组织id
-- APP 端(按个人隔离)自动改写后
SELECT * FROM tb_order
WHERE (phone = 'xxx' OR name = 'yyy')
AND id = 当前登录用户的id
快速接入
Step 1:引入依赖
xml
<dependency>
<groupId>io.gitee.tired-of-the-water</groupId>
<artifactId>sangsang-core</artifactId>
<version>3.7.0-alpha</version>
</dependency>
Step 2:开启配置
ini
# 配置实体类扫描路径
sangsang.scanEntityPackage[0]=com.sangsang.*.entity
# 开启数据权限隔离
sangsang.isolation.enable=true
Step 3:实现数据隔离策略
typescript
@Component
public class OrgIsolationStrategy implements DataIsolationStrategy<Long> {
@Override
public String getIsolationField(String tableName) {
// 返回隔离字段名,一般从 ThreadLocal 获取当前用户信息后决定
return "org_id";
}
@Override
public IsolationRelationEnum getIsolationRelation(String tableName) {
// 支持 "="、"IN"、"LIKE" 三种模式
return IsolationRelationEnum.EQUALS;
}
@Override
public Long getIsolationData(String tableName) {
// 从 ThreadLocal 获取当前登录用户的隔离字段值
return UserContext.getOrgId();
}
}
Step 4:创建 @TableName 实体类并标注
kotlin
@TableName("tb_order") //标识这个是tb_order表的实体类
@DataIsolation(OrgIsolationStrategy.class) // 指定隔离策略
public class Order {
private Long id;
private String orderNo;
private Long orgId; // 实体类中必须包含隔离字段
}
支持的隔离模式:
| 模式 | 说明 | 示例 |
|---|---|---|
= |
精确匹配 | org_id = 123 |
IN |
多值匹配 | org_id IN (1, 2, 3) |
LIKE |
模糊前缀匹配 | org_seq LIKE '123%' |
更多能力
- 复杂场景策略:PC 端按组织隔离,APP 端按个人隔离,在同一个 Strategy 中根据用户类型动态返回不同隔离字段和模式
- DML 语句隔离 :默认仅对 SELECT 生效,配置
sangsang.isolation.supportDML=true可对增删改也生效 - IN 分段处理:自动分段避免 Oracle 1000 条 IN 限制
- 禁用隔离 :通过
@ForbidIsolation注解在特定 Mapper 方法或 Service 方法上跳过隔离
3.4 数据变更字段自动填充
几乎每个项目都有"创建时间"、"修改时间"、"操作人"这类需要自动维护的字段。sangsang 让你不用再在每个 Service 中手动设置这些字段。
效果示例:
sql
-- 原始 SQL
INSERT INTO tb_order(order_no, amount) VALUES ('ORD001', 1000.00)
-- 自动填充后
INSERT INTO tb_order(create_time, create_by, order_no, amount)
VALUES ('2025-01-01 12:00:00', 'admin', 'ORD001', 1000.00)
快速接入
Step 1:引入依赖
xml
<dependency>
<groupId>io.gitee.tired-of-the-water</groupId>
<artifactId>sangsang-core</artifactId>
<version>3.7.0-alpha</version>
</dependency>
Step 2:开启配置
ini
# 配置实体类扫描路径
sangsang.scanEntityPackage[0]=com.sangsang.*.entity
# 开启数据变更时自动填充
sangsang.fieldDefault.enable=true
Step 3:实现默认值策略
typescript
@Component
public class CreateTimeStrategy implements FieldDefaultStrategy<LocalDateTime> {
@Override
public boolean whetherToHandle(SqlCommandEnum sqlCommandEnum) {
// 指定在 INSERT 时生效
return SqlCommandEnum.INSERT.equals(sqlCommandEnum);
}
@Override
public LocalDateTime getDefaultValue() {
return LocalDateTime.now();
}
}
Step 4:在 @TableName 实体类中标注字段
kotlin
@TableName("tb_order") //标识这个是tb_order表的实体类
public class Order {
@FieldDefault(CreateTimeStrategy.class) // 新增时自动填充创建时间
private LocalDateTime createTime;
@FieldDefault(CreateByStrategy.class) // 新增时自动填充创建人
private String createBy;
@FieldDefault(UpdateTimeStrategy.class) // 新增和修改时自动填充修改时间
private LocalDateTime updateTime;
@FieldDefault(UpdateByStrategy.class) // 新增和修改时自动填充修改人
private String updateBy;
}
支持在
@TableName实体类中统一标注,子类自动继承。也可用于租户 ID、组织 ID 等字段的自动维护。
更多能力
- 强制覆盖 :
@FieldDefault(value = CreateTimeStrategy.class, mandatoryOverride = true)强制使用策略值覆盖 SQL 中已有的值 - 配合数据权限隔离 :结合
@DataIsolation注解,可实现租户数据自动写入 + 查询隔离的完整方案
3.5 数据库字段查询脱敏
注意:此功能与框架"一处标注,全局生效"理念不一致(需要在每个查询处标注),后续版本考虑移除。
对于查询返回的敏感数据,框架支持在 SQL 执行后对结果集进行脱敏处理。与传统序列化脱敏不同,【sangsang 】的脱敏方案可以获取完整响应对象,天然支持业务差异化脱敏。
效果示例:
css
-- 查询结果
| 订单号 | 订单类型 | 物料名称 |
|--------|----------|-------------|
| 001 | A | 敏感物料名字 |
| 002 | B | 普通物料名字 |
-- 脱敏后(A 类型订单自动脱敏)
| 订单号 | 订单类型 | 物料名称 |
|--------|----------|--------------|
| 001 | A | ***脱敏物料*** |
| 002 | B | 普通物料名字 |
快速接入
Step 1:开启配置
ini
sangsang.desensitize.enable=true
Step 2:实现脱敏策略
typescript
public class BusinessDesensitizeStrategy implements DesensitizeStrategy {
@Override
public String desensitize(String s, Object o) {
// s: 待脱敏的字符串
// o: 整个返回对象,可获取业务信息进行差异化脱敏
if (o instanceof OrderVo && "A".equals(((OrderVo) o).getOrderType())) {
return s.substring(0, 2) + "***";
}
return s; // 其他情况不脱敏
}
}
Step 3:字段标注
less
// 响应是实体类 - 在 SQL 返回类字段上标注
public class OrderVo {
private String orderType;
@FieldDesensitize(BusinessDesensitizeStrategy.class)
private String materialName;
}
// 响应是 String - 在 Mapper 方法上标注
@MapperDesensitize(@FieldDesensitize(BusinessDesensitizeStrategy.class))
List<String> getOrderNames(String orderId);
// 响应是 Map - 指定字段名脱敏
@MapperDesensitize({
@FieldDesensitize(value = BusinessDesensitizeStrategy.class, fieldName = "material_name"),
@FieldDesensitize(value = PhoneDesensitizeStrategy.class, fieldName = "phone")
})
Map<String, Object> getOrderDetail(String orderId);
四、与其他方案的对比
| 对比项 | sangsang | 常见注解方案 | AOP/拦截器方案 |
|---|---|---|---|
| 业务代码侵入 | 零侵入 | 需在多处加注解 | 需修改 Service 层 |
| 标注范围 | 实体类字段(一处标注) | 每个 Mapper/方法 | 每个 Service 方法 |
| SQL 改写能力 | 支持复杂 SQL 改写 | 有限 | 不支持 |
| 密文模糊查询 | 支持(db 模式) | 通常不支持 | 不支持 |
| 异构数据库转换 | 支持 | 不支持 | 不支持 |
| 维护成本 | 低 | 高 | 中 |
五、快速体验
项目提供了在线演示平台,可以直观地看到框架改写前后的 SQL:
| 功能 | 演示地址 |
|---|---|
| DB 模式加解密 | http://43.133.199.2:8001/ |
| 数据权限自动隔离 | http://43.133.199.2:8003/ |
| 异构数据库语法转换 | http://43.133.199.2:8004/ |
也可以拉取源码本地运行 demo 模块,再使用演示平台,有个更为直观的感受
项目提供了详细的调试指南和测试用例,方便深入了解
六、适用场景
【sangsang】 适合以下场景:
- 中小型项目:并发数据量不是很大,不介意拦截器自动处理的性能损耗
- 敏感数据加密:金融、医疗、政务等对数据安全有要求的行业
- 信创改造:从 MySQL/Oracle 迁移到国产数据库(达梦等)
- 多租户 SaaS:需要数据权限自动隔离的 SaaS 平台
- 遗留系统改造:在不修改大量业务代码的前提下增加数据治理能力
- 新项目启动:从一开始就建立规范的数据治理体系
七、总结
【sangsang】 的核心价值在于 "零侵入" 三个字。它不是在业务代码中到处添加注解和逻辑,而是在 MyBatis 层面通过 SQL 改写来透明地完成数据治理工作。这种设计方式带来的好处是显而易见的:
- 接入成本极低:只需要引入依赖 + 开启配置+标注注解
- 维护成本低:不需要在项目中到处维护注解和逻辑
- 风险可控:不触碰业务代码,引入风险小
- 功能全面:加密、权限隔离、数据库迁移、自动填充,一个框架覆盖多个场景
如果你正在寻找一个轻量、零侵入、开箱即用的数据治理框架,不妨试试 【sangsang】
项目地址
- Gitee :gitee.com/tired-of-th...
git中有详细的使用文档和本地调试文档,方便你快速了解项目
欢迎 Star、提 Issue、交流讨论。如果觉得有帮助,点个 Star 是对作者最大的肯定。
技术交流:QQ 群 1072901252(群文件有 MySQL SM4 国密算法扩展包,可以让mysql也用上SM4加密算法)