如何利用Spring AOP在Java DTO创建时对指定字段进行脱敏?
解决方案
首先明确:Spring AOP无法直接拦截非Spring Bean的构造器调用,而DTO(比如你用@Builder生成的User实例)通常不是Spring托管的Bean,所以拦截构造器的思路不可行。更合理的方案是在DTO实例化完成后、返回给前端前的环节进行脱敏,确保最终API返回的是脱敏数据,同时保留数据库中的原始信息。
以下是两种可行的AOP实现方案:
方案一:拦截Service层返回DTO的方法
通过环绕通知拦截Service层中返回DTO的方法,对返回的实例进行脱敏处理,适用于大多数业务场景。
完整Aspect代码
@Aspect @Component public class PersonalInfoAspect { // 切点:拦截com.example.service包下所有方法的返回值 @Pointcut("execution(* com.example.service.*.*(..))") public void serviceMethodPointcut() {} // 环绕通知:处理返回的DTO脱敏 @Around("serviceMethodPointcut()") public Object desensitizeDto(ProceedingJoinPoint joinPoint) throws Throwable { // 执行原方法,获取返回的DTO实例 Object result = joinPoint.proceed(); if (result == null) { return null; } // 对带有@PersonalInfo注解的字段进行脱敏 processDesensitization(result); return result; } // 反射遍历字段并执行脱敏逻辑 private void processDesensitization(Object obj) throws IllegalAccessException { Class<?> clazz = obj.getClass(); Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { if (field.isAnnotationPresent(PersonalInfo.class)) { field.setAccessible(true); Object fieldValue = field.get(obj); if (fieldValue instanceof String) { String originalValue = (String) fieldValue; // 根据字段类型执行对应脱敏逻辑,这里以姓名为例 String desensitizedValue = desensitizeName(originalValue); field.set(obj, desensitizedValue); } } } } // 姓名脱敏逻辑:保留首字,其余替换为* private String desensitizeName(String name) { if (name == null || name.length() <= 1) { return name; } return name.substring(0, 1) + "*".repeat(name.length() - 1); } }
方案二:拦截Builder的build()方法
如果你习惯用@Builder模式创建DTO,可以直接拦截Builder的build()方法,在实例生成后立即脱敏。
对应Aspect代码片段
@Aspect @Component public class PersonalInfoAspect { // 切点:拦截User.Builder的build()方法 @Pointcut("execution(* com.example.dto.User$UserBuilder.build(..))") public void userBuilderBuildPointcut() {} @Around("userBuilderBuildPointcut()") public Object desensitizeFromBuilder(ProceedingJoinPoint joinPoint) throws Throwable { // 执行build()获取User实例 User user = (User) joinPoint.proceed(); // 执行脱敏处理 processDesensitization(user); return user; } // 复用方案一中的processDesensitization和脱敏逻辑方法 private void processDesensitization(Object obj) throws IllegalAccessException { // ... 同方案一代码 } private String desensitizeName(String name) { // ... 同方案一代码 } }
扩展优化:支持多类型脱敏
如果需要对不同类型的个人信息(如手机号、邮箱)执行不同脱敏逻辑,可以扩展@PersonalInfo注解,添加脱敏类型属性:
扩展后的注解
@Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface PersonalInfo { DesensitizeType type() default DesensitizeType.NAME; enum DesensitizeType { NAME, PHONE, EMAIL } }
优化后的脱敏逻辑
private void processDesensitization(Object obj) throws IllegalAccessException { Class<?> clazz = obj.getClass(); Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { if (field.isAnnotationPresent(PersonalInfo.class)) { field.setAccessible(true); Object fieldValue = field.get(obj); if (fieldValue instanceof String) { String original = (String) fieldValue; PersonalInfo annotation = field.getAnnotation(PersonalInfo.class); String desensitized = switch (annotation.type()) { case NAME -> desensitizeName(original); case PHONE -> desensitizePhone(original); case EMAIL -> desensitizeEmail(original); }; field.set(obj, desensitized); } } } } // 手机号脱敏:保留前3位和后4位 private String desensitizePhone(String phone) { if (phone == null || phone.length() != 11) { return phone; } return phone.substring(0, 3) + "****" + phone.substring(7); } // 邮箱脱敏:保留前缀首字和域名 private String desensitizeEmail(String email) { if (email == null || !email.contains("@")) { return email; } String[] parts = email.split("@"); if (parts[0].length() <= 1) { return email; } return parts[0].substring(0, 1) + "****@" + parts[1]; }
关键说明
- 无需拦截构造器:Spring AOP的动态代理机制仅支持拦截Spring Bean的方法调用,非Bean的构造器无法被拦截,因此选择在DTO实例化完成后的环节处理更可靠。
- 数据库数据不受影响:脱敏仅发生在内存中的DTO实例上,不会修改数据库中存储的原始数据,符合你的需求。
内容的提问来源于stack exchange,提问作者kaven
相关产品推荐
相关产品推荐

