⚡SimpleDAO 企业实战教程(08)脱敏 + 审计扩展 · 框架不设限
相关开源地址:
- 核心框架源码:https://gitee.com/gao_zhenzhong/simple-dao
- 系统底座:https://gitee.com/gao_zhenzhong/simple-dao-starter
- 代码生成器:https://gitee.com/gao_zhenzhong/simple-dao-coder
- 实战案例(本集源码):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
说明:完全继承BaseCondition,addCondition()中定义查询条件。与前面案例完全相同,一行未改。
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)📌 本集核心总结
脱敏与实体无关:实体只负责数据存取,脱敏是展示层行为,不该写在实体里或
TypeHandler中。脱敏与框架无关:无论是 MyBatis、JPA 还是 SimpleDAO,脱敏的正确做法都是在 Service 层用 AOP 做。SimpleDAO 不提供脱敏功能,它只提供“不设限”的扩展能力。
脱敏与数据库无关:脱敏逻辑在数据离开数据库、准备返回给前端时执行,和底层数据源(MySQL/Oracle/MongoDB/CSV)无关。
审计字段自定义:通过实现
UserIdProvider接口,让createBy/updateBy从业务上下文获取真实用户 ID,一行配置覆盖默认行为。逻辑删除字段名可配置:
simple-dao.logic-delete.field: is_deleted一行配置即可覆盖默认的dr字段,适配不同项目规范。分层规范:脱敏、字典翻译等业务逻辑放在 Service 层,不侵入 DAO 层,纠正 MyBatis 写
TypeHandler的错误分层方式。全程零 XML,SQL 完整可见:所有 SQL 日志打印完整带参语句,复制即跑,DBA 可直接优化。
系列八集,至此完结。感谢陪伴,源码见置顶。🚀