How to Implement Configurable Data Masking in Spring Boot with Custom Annotations
This article explains how to create a flexible, annotation‑driven data‑masking solution for Spring Boot APIs by defining custom masking strategies, integrating a Jackson serializer, and configuring the ObjectMapper to automatically mask sensitive fields in response objects.
During a casual afternoon, a product manager asked for a feature that masks sensitive data returned by certain APIs. To avoid writing repetitive masking code for each endpoint, a configurable, annotation‑based solution is designed.
Approach
1. Define a custom data‑masking annotation and a strategy interface so that each field can specify which masking operation to use.
2. Intercept controller responses and apply the masking logic. Instead of using @ControllerAdvice with heavy reflection, a custom Jackson serializer (similar to @JsonFormat) is created to handle the masking efficiently.
Code
1. Custom annotation with configurable masking strategy
@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataMasking {
DataMaskingFunc maskFunc() default DataMaskingFunc.NO_MASK;
}2. Custom serializer (StringSerializer) for masking
public interface DataMaskingOperation {
String MASK_CHAR = "*";
String mask(String content, String maskChar);
}
public enum DataMaskingFunc {
NO_MASK((str, maskChar) -> str),
ALL_MASK((str, maskChar) -> {
if (StringUtils.hasLength(str)) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < str.length(); i++) {
sb.append(StringUtils.hasLength(maskChar) ? maskChar : DataMaskingOperation.MASK_CHAR);
}
return sb.toString();
} else {
return str;
}
});
private final DataMaskingOperation operation;
private DataMaskingFunc(DataMaskingOperation operation) { this.operation = operation; }
public DataMaskingOperation operation() { return this.operation; }
}
public final class DataMaskingSerializer extends StdScalarSerializer<Object> {
private final DataMaskingOperation operation;
public DataMaskingSerializer() { super(String.class, false); this.operation = null; }
public DataMaskingSerializer(DataMaskingOperation operation) { super(String.class, false); this.operation = operation; }
@Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException {
String content = (operation == null) ?
DataMaskingFunc.ALL_MASK.operation().mask((String) value, null) :
operation.mask((String) value, null);
gen.writeString(content);
}
@Override
public JsonNode getSchema(SerializerProvider provider, Type typeHint) { return createSchemaNode("string", true); }
@Override
public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint) throws JsonMappingException {
visitStringFormat(visitor, typeHint);
}
}3. Custom AnnotationIntrospector to bind annotation to serializer
@Slf4j
public class DataMaskingAnnotationIntrospector extends NopAnnotationIntrospector {
@Override
public Object findSerializer(Annotated am) {
DataMasking annotation = am.getAnnotation(DataMasking.class);
if (annotation != null) {
return new DataMaskingSerializer(annotation.maskFunc().operation());
}
return null;
}
}4. Override ObjectMapper to register the introspector
@Configuration(proxyBeanMethods = false)
public class DataMaskConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({Jackson2ObjectMapperBuilder.class})
static class JacksonObjectMapperConfiguration {
@Bean
@Primary
ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
ObjectMapper objectMapper = builder.createXmlMapper(false).build();
AnnotationIntrospector ai = objectMapper.getSerializationConfig().getAnnotationIntrospector();
AnnotationIntrospector newAi = AnnotationIntrospectorPair.pair(ai, new DataMaskingAnnotationIntrospector());
objectMapper.setAnnotationIntrospector(newAi);
return objectMapper;
}
}
}5. Apply the annotation to response objects
public class User implements Serializable {
/** 主键ID */
private Long id;
/** 姓名 */
@DataMasking(maskFunc = DataMaskingFunc.ALL_MASK)
private String name;
/** 年龄 */
private Integer age;
/** 邮箱 */
@DataMasking(maskFunc = DataMaskingFunc.ALL_MASK)
private String email;
}Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Backend Technology
Focus on Java-related technologies: SSM, Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading. Occasionally cover DevOps tools like Jenkins, Nexus, Docker, and ELK. Also share technical insights from time to time, committed to Java full-stack development!
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.
