⚡SimpleDAO 企业实战教程(08)脱敏 + 审计扩展 · 框架不设限

相关开源地址

  1. 核心框架源码:https://gitee.com/gao_zhenzhong/simple-dao
  2. 系统底座:https://gitee.com/gao_zhenzhong/simple-dao-starter
  3. 代码生成器:https://gitee.com/gao_zhenzhong/simple-dao-coder
  4. 实战案例(本集源码):https://gitee.com/gao_zhenzhong/simple-dao-demo

前言

脱敏这件事,在绝大多数 Java 项目里都被做错了。

最常见的做法是:MyBatis 里写一个@Masks注解 +TypeHandler,JPA 里写一个@Convert+AttributeConverter,在数据从数据库查出来的那一刻,就把手机号、身份证号「当场掩码」。

看起来没问题?问题很大。

脱敏的本质是“展示行为”,不是“持久行为”。它只在“数据要返回给谁看”的那一刻才有意义——同一个手机号,管理员要看全号,客服要看中间四位,用户自己只能看前后三位。同一个实体,在不同接口里脱敏规则完全不同。

你把脱敏塞进TypeHandler,等于把“展示逻辑”塞进了“持久层”。于是:

  • 脱敏规则和实体绑死了,换一个接口就要改实体
  • 脱敏规则和框架绑死了,换框架就要重写
  • 脱敏规则和数据库绑死了,换数据源就要重写

而真正正确的位置只有一个:Service 层,数据离开数据库、准备返回给前端的那一刻。

本集要证明的只有一件事:脱敏与实体无关,与框架无关,与数据库无关,只与调用行为有关。怎么做?用 AOP 做,用注解标记方法,在返回数据上做“后处理”。这不是 SimpleDAO 提供的功能,而是 SimpleDAO“不设限”的体现——你完全可以用它做任何你该做的事。

📋 前置知识

✅ 你只需要会❌ 你完全不需要会的
Java 基础(类、注解、泛型)MyBatis/MP、复杂ORM插件
基础 SQL(SELECT、WHERE、JOIN)XML动态标签、OGNL表达式
Spring Boot 基础配置Spring高级自动配置、复杂Maven配置
了解 Spring AOP 基本概念TypeHandler / Converter 细节
知道自定义注解长什么样注解处理器、反射底层原理

📚 全套教程总览

集数 · 标题时长核心内容
01 · 单表 CRUD + 审计 + 逻辑删除约 6 min零代码单表操作、自动填充审计、内置逻辑删除
02 · 联表查询 + 分页约 5 min单/多表统一API,无需ResultMap
03 · 条件进阶:IN + 子查询约 5 min替代MyBatis foreach标签,动态片段拼接
04 · 多表联查 + 复杂条件约 5 min多表关联、区间筛选通用写法
05 · 报表聚合:GROUP BY + 聚合函数约 6 min原生SQL报表无限制,不受ORM束缚
06 · mergeParams 多组条件合并约 6 min复杂报表拆分多条件,解耦复用
07 · 多租户 + 数据权限 · AOP 破局约 7 min不用MyBatis拦截器,Spring原生AOP扩展
08 · 脱敏 + 审计扩展 · 框架不设限约 7 min纠正MyBatis持久层脱敏错误分层思路

🚀 项目快速上手

本案例内置 H2 内存库,无需安装本地数据库,克隆案例直接启动即可运行。

完整项目层级结构

demo08_desensitize/ ├── pom.xml └── src/main/ ├── java/example/ │ ├── DemoApplication.java // 启动类 │ ├── common/ │ │ ├── config/ │ │ │ └── CustomUserIdProvider.java // 自定义用户ID提供者 │ │ └── desensitize/ │ │ ├── Desensitize.java // 脱敏注解 │ │ └── DesensitizeAspect.java // 脱敏AOP切面 │ └── sys/user/ │ ├── User.java // 用户实体 │ ├── UserCond.java // 查询条件类 │ ├── UserDao.java // 数据访问层 │ └── UserService.java // 业务层(含@Desensitize) └── resources/ ├── application.yml // 配置文件 ├── logback-spring.xml // 日志配置 └── schema.sql // 建表脚本

1. Maven 依赖 pom.xml

说明:仅依赖 Spring JDBC + SimpleDAO + AOP,无任何冗余插件。

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency><dependency><groupId>io.gitee.simpledao</groupId><artifactId>simple-dao</artifactId><version>1.2.1</version></dependency><dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency>

2. 配置文件 application.yml

说明:无框架专属复杂配置,仅标准 Spring 数据源 + 一行逻辑删除字段配置。

spring:datasource:url:jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1driver-class-name:org.h2.Driverusername:sapassword:sql:init:schema-locations:classpath:schema.sqlmode:alwayssimple-dao:show-sql:truelogic-delete:field:is_deleted# 逻辑删除字段名,默认 dr,可任意替换

3. 建表脚本 schema.sql

说明:逻辑删除字段名改为is_deleted,演示配置覆盖能力。

CREATETABLEIFNOTEXISTSsys_user(idBIGINTPRIMARYKEY,nameVARCHAR(50)NOTNULL,ageINTEGER,id_cardVARCHAR(30)UNIQUENOTNULL,phoneVARCHAR(20)UNIQUE,create_timeTIMESTAMP,create_byBIGINT,update_timeTIMESTAMP,update_byBIGINT,is_deletedTINYINTDEFAULT0);INSERTINTOsys_user(id,name,age,id_card,phone,create_time,create_by,is_deleted)VALUES(3681877765507776511,'张三',25,'110101199001011234','13812345678',CURRENT_TIMESTAMP,1000,0),(3681877765507776512,'李四',30,'110101199502021235','13987654321',CURRENT_TIMESTAMP,1000,0),(3681877765507776513,'王五',28,'110101199703031236','13611223344',CURRENT_TIMESTAMP,1000,0);

🔧 核心业务代码演示

第一层:实体 User.java

说明:与前面案例一致,@Table绑定表名,@Id标记主键。审计字段自动填充,无额外配置。

packageexample.sys.user;importcom.simple.common.base.annotation.Id;importcom.simple.common.base.annotation.Table;importlombok.AllArgsConstructor;importlombok.Builder;importlombok.Data;importlombok.NoArgsConstructor;importjava.time.LocalDateTime;@Data@Builder@AllArgsConstructor@NoArgsConstructor@Table("sys_user")publicclassUser{@IdprivateLongid;privateStringname;privateIntegerage;privateStringidCard;privateStringphone;privateLocalDateTimecreateTime;privateLongcreateBy;privateLocalDateTimeupdateTime;privateLongupdateBy;privateIntegerisDeleted;// 逻辑删除字段名改为 is_deleted}

第二层:条件类 UserCond.java

说明:完全继承BaseConditionaddCondition()中定义查询条件。与前面案例完全相同,一行未改。

packageexample.sys.user;importcom.simple.common.base.BaseCondition;importlombok.AllArgsConstructor;importlombok.Builder;importlombok.Getter;importlombok.NoArgsConstructor;importlombok.Setter;@Setter@Getter@Builder@AllArgsConstructor@NoArgsConstructorpublicclassUserCondextendsBaseCondition{privateStringname;privateIntegerage;privateStringphone;privateStringidCard;privateIntegerisDeleted;@OverrideprotectedvoidaddCondition(){add("AND name LIKE ?",name,3);add("AND age = ?",age);add("AND phone LIKE ?",phone,3);add("AND id_card LIKE ?",idCard,3);add("AND is_deleted = ?",isDeleted);}}

第三层:DAO 层 UserDao.java

说明:空类继承BaseDao,自动拥有 50+ CRUD 方法。本集演示中,DAO 层零改动。

packageexample.sys.user;importcom.simple.common.base.BaseDao;importorg.springframework.stereotype.Repository;@RepositorypublicclassUserDaoextendsBaseDao<User>{// 空类,拥有全部单表 CRUD 能力}

第四层:Service 层 UserService.java(本集核心)

说明:脱敏是展示层行为,和框架无关,和数据库无关,和实体无关。本集在 Service 层用@Desensitize注解 + AOP 实现脱敏,纠正 MyBatisTypeHandler的错误分层思路。

packageexample.sys.user;importexample.common.desensitize.Desensitize;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Service;importjava.util.List;@ServicepublicclassUserService{@AutowiredprivateUserDaouserDao;/** * 方法级脱敏:对返回列表中的 phone 和 idCard 字段进行脱敏 * 与框架无关,与数据库无关,与实体无关——只和“展示行为”有关 */@Desensitize(types={"phone","idCard"},fields={"phone","idCard"})publicList<User>list(UserCondcond){returnuserDao.list(cond);}publicUsersave(Useruser){returnuserDao.save(user);}publicUserfindById(Longid){returnuserDao.findById(id);}}

第五层:脱敏注解 @Desensitize

说明:方法级注解,声明哪些字段需要脱敏以及对应的脱敏类型。

packageexample.common.desensitize;importjava.lang.annotation.ElementType;importjava.lang.annotation.Retention;importjava.lang.annotation.RetentionPolicy;importjava.lang.annotation.Target;@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public@interfaceDesensitize{String[]types();// 脱敏类型:phone / idCardString[]fields();// 对应的字段名}

第六层:脱敏切面 DesensitizeAspect

说明:AOP 在 Service 方法返回后,对List中的每个对象按注解配置做字段替换。脱敏逻辑在展示层完成,不污染 DAO 层。

packageexample.common.desensitize;importorg.apache.commons.lang3.reflect.FieldUtils;importorg.aspectj.lang.ProceedingJoinPoint;importorg.aspectj.lang.annotation.Around;importorg.aspectj.lang.annotation.Aspect;importorg.springframework.stereotype.Component;importjava.lang.reflect.Field;importjava.util.List;@Aspect@ComponentpublicclassDesensitizeAspect{@Around("execution(public * example.sys..*.*Service.*(..)) && @annotation(anno)")publicObjectaround(ProceedingJoinPointjoinPoint,Desensitizeanno)throwsThrowable{Objectresult=joinPoint.proceed();if(resultinstanceofList<?>list){for(Objectitem:list){for(inti=0;i<anno.fields().length;i++){Fieldfield=FieldUtils.getField(item.getClass(),anno.fields()[i],true);if(field!=null){maskField(item,field,anno.types()[i]);}}}}returnresult;}privatevoidmaskField(Objectitem,Fieldfield,Stringtype)throwsIllegalAccessException{Stringoriginal=(String)FieldUtils.readField(field,item,true);if(original==null)return;Stringmasked=switch(type){case"phone"->original.substring(0,3)+"****"+original.substring(7);case"idCard"->original.substring(0,6)+"********"+original.substring(14);default->original;};FieldUtils.writeField(field,item,masked,true);}}

第七层:自定义 UserIdProvider(审计字段扩展)

说明:SimpleDAO 默认UserIdProvider返回1000L(开发环境会打印警告)。本集演示如何通过实现接口,让createBy/updateBy从业务上下文获取真实用户 ID。

packageexample.common.config;importcom.simple.common.base.UserIdProvider;importorg.springframework.stereotype.Component;@ComponentpublicclassCustomUserIdProviderimplementsUserIdProvider{@OverridepublicLonguserId(){// 实际项目可从 Session / Shiro / SpringSecurity / Token 中获取// 示例:SessionUtils.getUserId()// 示例:ShiroUtils.getUserId()// 示例:SecurityContextHolder.getContext().getAuthentication()return9999L;}}

📝 运行日志效果(完整可执行 SQL)

说明:SimpleDAO 打印带真实参数的完整 SQL,脱敏前后数据对比清晰可见。

# 1. 插入:审计字段自动填充 createBy=9999(来自 CustomUserIdProvider) [INFO] INSERT INTO sys_user (id,name,age,id_card,phone,create_time,create_by,is_deleted) VALUES (3708628123456789000,'张三',25,'110101199001011234','13800138000','2026-06-30 12:34:57',9999,0) # 2. 主键查询:完整数据(含敏感字段) [INFO] SELECT t.id,t.name,t.age,t.id_card,t.phone,t.create_time,t.create_by,t.is_deleted FROM sys_user t WHERE t.id=3708628123456789000 [INFO] 查询结果:User(id=3708628123456789000, name=张三, age=25, idCard=110101199001011234, phone=13800138000, createBy=9999, isDeleted=0) # 3. 脱敏列表查询:phone 和 idCard 自动脱敏 [INFO] SELECT t.id,t.name,t.age,t.id_card,t.phone,t.create_time,t.create_by,t.is_deleted FROM sys_user t [INFO] 脱敏结果:User(id=..., name=张三, idCard=110101********1234, phone=138****8000) [INFO] 脱敏结果:User(id=..., name=李四, idCard=110101********1235, phone=139****4321) [INFO] 脱敏结果:User(id=..., name=王五, idCard=110101********1236, phone=136****3344) # 4. 逻辑删除:is_deleted 被置为 1(配置覆盖生效) [INFO] UPDATE sys_user t SET is_deleted = 1 WHERE t.id IN (3708628123456789000)

📌 本集核心总结

  1. 脱敏与实体无关:实体只负责数据存取,脱敏是展示层行为,不该写在实体里或TypeHandler中。

  2. 脱敏与框架无关:无论是 MyBatis、JPA 还是 SimpleDAO,脱敏的正确做法都是在 Service 层用 AOP 做。SimpleDAO 不提供脱敏功能,它只提供“不设限”的扩展能力。

  3. 脱敏与数据库无关:脱敏逻辑在数据离开数据库、准备返回给前端时执行,和底层数据源(MySQL/Oracle/MongoDB/CSV)无关。

  4. 审计字段自定义:通过实现UserIdProvider接口,让createBy/updateBy从业务上下文获取真实用户 ID,一行配置覆盖默认行为。

  5. 逻辑删除字段名可配置simple-dao.logic-delete.field: is_deleted一行配置即可覆盖默认的dr字段,适配不同项目规范。

  6. 分层规范:脱敏、字典翻译等业务逻辑放在 Service 层,不侵入 DAO 层,纠正 MyBatis 写TypeHandler的错误分层方式。

  7. 全程零 XML,SQL 完整可见:所有 SQL 日志打印完整带参语句,复制即跑,DBA 可直接优化。

系列八集,至此完结。感谢陪伴,源码见置顶。🚀