Backend Development 7 min read

Implementing Global Data Desensitization with Hutool and Spring AOP

This article demonstrates how to build a reusable data‑desensitization component in Java by defining custom annotations, extending Hutool's DesensitizedUtil, and creating a Spring AOP aspect that automatically masks sensitive fields in returned objects such as pages, lists, or single entities.

Selected Java Interview Questions
Selected Java Interview Questions
Selected Java Interview Questions
Implementing Global Data Desensitization with Hutool and Spring AOP

We create a global data desensitization solution using Hutool's DesensitizedUtil and Spring AOP.

First we define a method‑level annotation @DataDesensitized to mark the entry point.

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataDesensitized {
}

Then we define a field‑level annotation @Desensitized that references DesensitizedUtil.DesensitizedType for built‑in masking strategies.

@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Desensitized {
    // select a built‑in desensitization type
    DesensitizedUtil.DesensitizedType type();
}

Next we implement the aspect DataDesensitizedAspect which intercepts methods annotated with @DataDesensitized , inspects the returned value (a PageInfo , a List , or a single object), and applies desensitization to all fields marked with @Desensitized .

@Aspect
@Component
@Slf4j
public class DataDesensitizedAspect {
    @AfterReturning(pointcut = "@annotation(dd)", returning = "result")
    public void doAfterReturning(JoinPoint joinPoint, DataDesensitized dd, Object result) {
        // role‑based control could be added here
        boolean need = true;
        if (!need) {
            return;
        }
        // handle PageInfo, List, or single object
        if (result instanceof PageInfo) {
            PageInfo page = (PageInfo) result;
            List records = page.getList();
            for (Object record : records) {
                objReplace(record);
            }
        } else if (result instanceof List) {
            List list = (List) result;
            for (Object obj : list) {
                objReplace(obj);
            }
        } else {
            objReplace(result);
        }
    }

    public static
void objReplace(T t) {
        try {
            Field[] declaredFields = ReflectUtil.getFields(t.getClass());
            for (Field field : declaredFields) {
                Desensitized des = field.getAnnotation(Desensitized.class);
                // only process String fields with the annotation
                if (des != null && "class java.lang.String".equals(field.getGenericType().toString())) {
                    Object fieldValue = ReflectUtil.getFieldValue(t, field);
                    if (fieldValue == null || StringUtils.isEmpty(fieldValue.toString())) {
                        continue;
                    }
                    DesensitizedUtil.DesensitizedType type = des.type();
                    String hide = DesensitizedUtil.desensitized(fieldValue.toString(), type);
                    ReflectUtil.setFieldValue(t, field, hide);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

We demonstrate usage by annotating a service method with @DataDesensitized and marking VO fields with @Desensitized , specifying types such as CHINESE_NAME , MOBILE_PHONE , and ID_CARD .

@Override
@DataProtection
public PageInfo<OrderDetailsVo> queryOrderDetails(QueryParam param) {
    return mapper.queryOrderDetails(param);
}

@Data
public class OrderDetailsVo {
    private String orderNo;
    private String sn;

    @Desensitized(type = DesensitizedUtil.DesensitizedType.CHINESE_NAME)
    private String username;

    @Desensitized(type = DesensitizedUtil.DesensitizedType.MOBILE_PHONE)
    private String mobile;

    @Desensitized(type = DesensitizedUtil.DesensitizedType.ID_CARD)
    private String idCard;
}

When a custom masking range is required (e.g., masking characters 5‑18 of a 20‑character SN), we extend the DesensitizedType enum and enhance @Desensitized to include startInclude and endExclude attributes.

public enum DesensitizedType {
    // custom marker
    CUSTOM,
    USER_ID,
    CHINESE_NAME,
    ID_CARD,
    FIXED_PHONE,
    MOBILE_PHONE,
    ADDRESS,
    EMAIL,
    PASSWORD,
    CAR_LICENSE,
    BANK_CARD
}

@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Desensitized {
    DesensitizedType type() default DesensitizedType.CUSTOM;
    int startInclude() default 0;
    int endExclude() default -1;
}

The aspect logic is updated to handle the CUSTOM type by using StrUtil.hide with the provided start and end positions; otherwise it falls back to Hutool's built‑in desensitization.

public static
void objReplace(T t) {
    try {
        Field[] declaredFields = ReflectUtil.getFields(t.getClass());
        for (Field field : declaredFields) {
            Desensitized des = field.getAnnotation(Desensitized.class);
            if (des != null && "class java.lang.String".equals(field.getGenericType().toString())) {
                Object fieldValue = ReflectUtil.getFieldValue(t, field);
                if (fieldValue == null || StringUtils.isEmpty(fieldValue.toString())) {
                    continue;
                }
                String value = fieldValue.toString();
                String hide = "";
                if (des.type() == DesensitizedType.CUSTOM) {
                    int startInclude = des.startInclude();
                    int endExclude = des.endExclude();
                    if (endExclude == -1) {
                        endExclude = value.length();
                    }
                    hide = StrUtil.hide(value, startInclude, endExclude);
                } else {
                    DesensitizedUtil.DesensitizedType type = DesensitizedUtil.DesensitizedType.valueOf(des.type().toString());
                    hide = DesensitizedUtil.desensitized(value, type);
                }
                ReflectUtil.setFieldValue(t, field, hide);
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

Finally, the VO class is updated to use the custom start/end parameters for the SN field, completing a reusable data‑masking component.

@Data
public class OrderDetailsVo {
    private String orderNo;

    @Desensitized(startInclude = 5, endExclude = 18)
    private String sn;

    @Desensitized(type = DesensitizedType.CHINESE_NAME)
    private String username;

    @Desensitized(type = DesensitizedType.MOBILE_PHONE)
    private String mobile;

    @Desensitized(type = DesensitizedType.ID_CARD)
    private String idCard;
}
JavaAnnotationsHutoolData DesensitizationSpring AOPAspect-Oriented Programming
Selected Java Interview Questions
Written by

Selected Java Interview Questions

A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.