Implementing Sensitive Data Encryption and Decryption in Spring Boot Using MyBatis Interceptors and Custom Annotations
This article demonstrates how to automatically encrypt sensitive fields such as ID numbers and phone numbers before storing them in a database and decrypt them after retrieval by leveraging MyBatis plugins, custom annotations, and Spring Boot components, providing a clean, reusable solution for backend applications.
In many production projects, sensitive information like identity numbers, phone numbers, and real names must be stored encrypted in the database, but manually encrypting and decrypting these fields in business code is error‑prone and exposes encryption rules to developers.
The article introduces a solution that uses a Spring Boot + MyBatis interceptor combined with custom annotations to transparently encrypt data before it is persisted and decrypt it after it is read.
MyBatis plugins allow interception at various points in the SQL execution lifecycle. The supported interception types include Executor, ParameterHandler, ResultSetHandler, and StatementHandler. The following snippet shows the core methods of the Interceptor interface:
public interface Interceptor {
// core interception logic
Object intercept(Invocation invocation) throws Throwable;
// plugin chain
default Object plugin(Object target) {return Plugin.wrap(target, this);}
// custom properties
default void setProperties(Properties properties) {}
}To encrypt input parameters, the ParameterHandler interceptor is used; to decrypt output results, the ResultSetHandler interceptor is employed.
Two custom annotations are defined: @SensitiveData marks a class whose instances contain sensitive fields, and @SensitiveField marks the specific fields that need encryption/decryption.
/**
* Annotation for classes containing sensitive data
*/
@Inherited
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveData { }
/**
* Annotation for fields that require encryption
*/
@Inherited
@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveField { }An EncryptUtil interface abstracts the encryption operation, allowing different algorithms (e.g., AES, PBE) to be plugged in.
public interface EncryptUtil {
/**
* Encrypts the given fields of the parameter object.
*/
<T> T encrypt(Field[] declaredFields, T paramsObject) throws IllegalAccessException;
}The AES implementation ( AESEncrypt) uses a custom AESUtil to perform the actual encryption.
@Component
public class AESEncrypt implements EncryptUtil {
@Autowired
AESUtil aesUtil;
@Override
public <T> T encrypt(Field[] declaredFields, T paramsObject) throws IllegalAccessException {
for (Field field : declaredFields) {
SensitiveField sensitiveField = field.getAnnotation(SensitiveField.class);
if (sensitiveField != null) {
field.setAccessible(true);
Object object = field.get(paramsObject);
if (object instanceof String) {
String value = (String) object;
field.set(paramsObject, aesUtil.encrypt(value));
}
}
}
return paramsObject;
}
}The encryption interceptor ( EncryptInterceptor) is registered with MyBatis via @Intercepts and intercepts the ParameterHandler.setParameters method. It reflects on the parameter object, checks for the @SensitiveData annotation, and invokes the encryption utility on all annotated fields.
@Slf4j
@Component
@Intercepts({
@Signature(type = ParameterHandler.class, method = "setParameters", args = PreparedStatement.class)
})
public class EncryptInterceptor implements Interceptor {
private final EncryptDecryptUtil encryptUtil;
@Autowired
public EncryptInterceptor(EncryptDecryptUtil encryptUtil) { this.encryptUtil = encryptUtil; }
@Override
public Object intercept(Invocation invocation) throws Throwable {
ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();
Field parameterField = parameterHandler.getClass().getDeclaredField("parameterObject");
parameterField.setAccessible(true);
Object parameterObject = parameterField.get(parameterHandler);
if (parameterObject != null) {
Class<?> parameterObjectClass = parameterObject.getClass();
SensitiveData sensitiveData = AnnotationUtils.findAnnotation(parameterObjectClass, SensitiveData.class);
if (sensitiveData != null) {
Field[] declaredFields = parameterObjectClass.getDeclaredFields();
encryptUtil.encrypt(declaredFields, parameterObject);
}
}
return invocation.proceed();
}
@Override
public Object plugin(Object o) { return Plugin.wrap(o, this); }
@Override
public void setProperties(Properties properties) { }
}Similarly, a DecryptUtil interface and its AES implementation ( AESDecrypt) are defined to handle decryption.
public interface DecryptUtil {
/**
* Decrypts the given result object.
*/
<T> T decrypt(T result) throws IllegalAccessException;
} @Component
public class AESDecrypt implements DecryptUtil {
@Autowired
AESUtil aesUtil;
@Override
public <T> T decrypt(T result) throws IllegalAccessException {
Class<?> resultClass = result.getClass();
Field[] declaredFields = resultClass.getDeclaredFields();
for (Field field : declaredFields) {
SensitiveField sensitiveField = field.getAnnotation(SensitiveField.class);
if (sensitiveField != null) {
field.setAccessible(true);
Object object = field.get(result);
if (object instanceof String) {
String value = (String) object;
field.set(result, aesUtil.decrypt(value));
}
}
}
return result;
}
}The decryption interceptor ( DecryptInterceptor) intercepts ResultSetHandler.handleResultSets. It checks whether the returned object (a list or a single entity) is annotated with @SensitiveData and, if so, applies the decryption utility to each element.
@Slf4j
@Component
@Intercepts({
@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
public class DecryptInterceptor implements Interceptor {
@Autowired
DecryptUtil aesDecrypt;
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object resultObject = invocation.proceed();
if (resultObject == null) return null;
if (resultObject instanceof ArrayList) {
ArrayList resultList = (ArrayList) resultObject;
if (!CollectionUtils.isEmpty(resultList) && needToDecrypt(resultList.get(0))) {
for (Object result : resultList) {
aesDecrypt.decrypt(result);
}
}
} else {
if (needToDecrypt(resultObject)) {
aesDecrypt.decrypt(resultObject);
}
}
return resultObject;
}
private boolean needToDecrypt(Object object) {
Class<?> objectClass = object.getClass();
SensitiveData sensitiveData = AnnotationUtils.findAnnotation(objectClass, SensitiveData.class);
return sensitiveData != null;
}
@Override
public Object plugin(Object target) { return Plugin.wrap(target, this); }
@Override
public void setProperties(Properties properties) { }
}After configuring these interceptors and annotating the entity classes with @SensitiveData and @SensitiveField, MyBatis will automatically encrypt data before it is persisted and decrypt it after it is fetched, eliminating the need for manual encryption logic in the business layer.
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.
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.
