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.
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;
}Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.