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.

Selected Java Interview Questions
Selected Java Interview Questions
Selected Java Interview Questions
Implementing Sensitive Data Encryption and Decryption in Spring Boot Using MyBatis Interceptors and Custom Annotations

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaSpring BootMyBatisInterceptorencryption
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

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.