Backend Development 21 min read

Using Java Annotations for Log Masking and Bank API Request Simplification

This article explains how to employ custom Java annotations together with reflection to mask sensitive fields in logs, define flexible desensitization rules, and dramatically reduce repetitive code when building fixed‑length, signed bank API requests, improving maintainability and extensibility of backend services.

Full-Stack Internet Architecture
Full-Stack Internet Architecture
Full-Stack Internet Architecture
Using Java Annotations for Log Masking and Bank API Request Simplification

Preface

Logging is essential for troubleshooting, but logs must not expose personal data such as phone numbers, ID cards, or addresses. The author introduces a solution that uses annotations to achieve fast and reliable log desensitization while also exploring other annotation use‑cases.

Log Desensitization Scenario Overview

Typical log output prints a model object as a JSON string, for example:

public class Request {
    /** User name */
    private String name;
    /** ID card */
    private String idcard;
    /** Phone number */
    private String phone;
    /** Image base64 */
    private String imgBase64;
}

Creating an instance:

Request request = new Request();
request.setName("爱新觉罗");
request.setIdcard("450111112222");
request.setPhone("18611111767");
request.setImgBase64("xxx");

Logging with FastJSON:

log.info(JSON.toJSONString(request));

Resulting log contains raw sensitive data, which raises two problems:

Security – personal fields should be masked.

Redundancy – large base64 strings increase storage cost and provide little diagnostic value.

Desired masked output:

{"idcard":"450******222","name":"爱**罗","phone":"186****1767","imgBase64":""}

Masking rules differ per field:

ID card – keep first three and last three characters.

Name – keep first and last characters.

Phone – keep first three and last four characters.

imgBase64 – replace with an empty string.

Annotation Definition and Implementation Principle

Java annotations (introduced in JDK 5) allow metadata to be retained at runtime (RetentionPolicy.RUNTIME) and accessed via reflection. Common built‑in annotations include @Override, @Deprecated, and @SuppressWarnings. Custom annotations are defined with @interface and can carry attributes.

Key meta‑annotations:

@Documented – included in Javadoc.

@Retention – determines how long the annotation is retained (SOURCE, CLASS, RUNTIME).

@Target – restricts where the annotation can be applied (FIELD, METHOD, TYPE, etc.).

Using Annotations for Log Desensitization

Define a SensitiveInfo annotation that carries a SensitiveType enum:

// Sensitive type enum
public enum SensitiveType { ID_CARD, PHONE, NAME, IMG_BASE64 }

@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveInfo {
    SensitiveType type();
}

Apply the annotation to the model fields:

public class Request {
    @SensitiveInfo(type = SensitiveType.NAME)
    private String name;
    @SensitiveInfo(type = SensitiveType.ID_CARD)
    private String idcard;
    @SensitiveInfo(type = SensitiveType.PHONE)
    private String phone;
    @SensitiveInfo(type = SensitiveType.IMG_BASE64)
    private String imgBase64;
}

Implement a ValueFilter that inspects each field via reflection, checks for the annotation, and applies the appropriate masking logic:

private static ValueFilter getValueFilter() {
    return (obj, key, value) -> {
        try {
            Field[] fields = obj.getClass().getDeclaredFields();
            for (Field field : fields) {
                if (!field.getName().equals(key)) continue;
                SensitiveInfo annotation = field.getAnnotation(SensitiveInfo.class);
                if (annotation != null) {
                    switch (annotation.type()) {
                        case PHONE:   return maskPhone((String) value);
                        case ID_CARD: return maskIdCard((String) value);
                        case NAME:    return maskName((String) value);
                        case IMG_BASE64: return "";
                        default:      return value;
                    }
                }
            }
        } catch (Exception e) {
            log.error("To JSON String fail", e);
        }
        return value;
    };
}

This approach decouples masking rules from field names, making the solution extensible (e.g., handling aliases like tel, telephone) by simply adding the annotation to the relevant fields.

Advanced Annotation Use – Eliminating Repetitive Bank API Code

When integrating with a bank, each API requires fixed‑length, ordered parameter concatenation, type‑specific padding, and an MD5 signature. The original implementation repeats formatting logic for every request.

Define two annotations:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Inherited
public @interface BankAPI {
    String url() default "";
    String desc() default "";
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
@Inherited
public @interface BankAPIField {
    int order() default -1;   // position in the concatenated string
    int length() default -1; // fixed length
    String type() default ""; // S=string, N=number, M=money
}

Apply them to request POJOs:

@BankAPI(url = "/createUser", desc = "Create User API")
@Data
public class CreateUserAPI extends AbstractAPI {
    @BankAPIField(order = 1, type = "S", length = 10)
    private String name;
    @BankAPIField(order = 2, type = "S", length = 18)
    private String identity;
    @BankAPIField(order = 3, type = "N", length = 5)
    private int age;
    @BankAPIField(order = 4, type = "S", length = 11)
    private String mobile;
}

@BankAPI(url = "/bank/pay", desc = "Pay API")
@Data
public class PayAPI extends AbstractAPI {
    @BankAPIField(order = 1, type = "N", length = 20)
    private long userId;
    @BankAPIField(order = 2, type = "M", length = 10)
    private BigDecimal amount;
}

Unified request handling using reflection:

private static String remoteCall(AbstractAPI api) throws IOException {
    BankAPI bankAPI = api.getClass().getAnnotation(BankAPI.class);
    StringBuilder sb = new StringBuilder();
    Arrays.stream(api.getClass().getDeclaredFields())
        .filter(f -> f.isAnnotationPresent(BankAPIField.class))
        .sorted(Comparator.comparingInt(f -> f.getAnnotation(BankAPIField.class).order()))
        .peek(f -> f.setAccessible(true))
        .forEach(f -> {
            BankAPIField fieldAnno = f.getAnnotation(BankAPIField.class);
            Object value = "";
            try { value = f.get(api); } catch (IllegalAccessException e) { e.printStackTrace(); }
            switch (fieldAnno.type()) {
                case "S":
                    sb.append(String.format("%-" + fieldAnno.length() + "s", value.toString()).replace(' ', '_'));
                    break;
                case "N":
                    sb.append(String.format("%" + fieldAnno.length() + "s", value.toString()).replace(' ', '0'));
                    break;
                case "M":
                    if (!(value instanceof BigDecimal))
                        throw new RuntimeException("Money field must be BigDecimal");
                    sb.append(String.format("%0" + fieldAnno.length() + "d",
                        ((BigDecimal) value).setScale(2, RoundingMode.DOWN)
                            .multiply(new BigDecimal("100")).longValue()));
                    break;
                default:
                    break;
            }
        });
    sb.append(DigestUtils.md2Hex(sb.toString()));
    String param = sb.toString();
    long begin = System.currentTimeMillis();
    String result = Request.Post("http://localhost:45678/reflection" + bankAPI.url())
        .bodyString(param, ContentType.APPLICATION_JSON)
        .execute().returnContent().asString();
    log.info("Call {} url:{} param:{} cost:{}ms", bankAPI.desc(), bankAPI.url(), param, System.currentTimeMillis() - begin);
    return result;
}

public static String createUser(CreateUserAPI req) throws IOException { return remoteCall(req); }
public static String pay(PayAPI req) throws IOException { return remoteCall(req); }

All request methods now delegate to remoteCall , eliminating duplicated formatting, signing, and HTTP‑call code. The annotation‑driven approach makes the system highly maintainable and extensible.

Conclusion

Annotations provide a powerful way to attach metadata to code elements, and when combined with reflection they enable generic processing such as log masking and uniform API request construction. This reduces boilerplate, improves code decoupling, and greatly enhances extensibility for backend Java projects.

JavaBackend DevelopmentreflectionAnnotationscode reuseLog MaskingBank API
Full-Stack Internet Architecture
Written by

Full-Stack Internet Architecture

Introducing full-stack Internet architecture technologies centered on Java

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.