Backend Development 21 min read

Log Masking with Guava: Solving Map Deep‑Clone Issues Using transformEntries

This article explains how to mask sensitive fields in Java logs, demonstrates the pitfalls of shallow‑copying nested Maps during masking, and shows a robust solution using Guava's Maps.transformEntries to perform lazy, view‑based deep transformation without mutating the original data structures.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
Log Masking with Guava: Solving Map Deep‑Clone Issues Using transformEntries

What Is Log Masking?

Log masking is a sub‑field of information‑security that hides sensitive data (such as names, phone numbers, or account numbers) before it is written to log files, preventing accidental or malicious data leaks.

Before Masking

Consider a simple Person class with fields id , name , phone , and account . Logging the object directly produces a JSON string that contains the raw personal data.

public class Person {
    private Long id;
    private String name;
    private String phone;
    private String account;
    // setter and getter ...
}
log.info("个人信息:{}", JsonUtils.toJSONString(person));

The resulting log entry looks like:

个人信息:{"id":1,"name":"张无忌","phone":"17709141590","account":"14669037943256249"}

After Masking

Masking replaces part of each sensitive field with asterisks:

个人信息:{"id":1,"name":"**忌","phone":"177******90","account":"146*********49"}

To achieve this we create a masking component and annotate fields with @Sensitive to indicate the masking rule.

public class Person {
    // id is not sensitive
    private Long id;

    @Sensitive(type = SensitiveType.Name)
    private String name;

    @Sensitive(type = SensitiveType.Phone)
    private String phone;

    @Sensitive(type = SensitiveType.Account)
    private String account;
    // setter and getter ...
}

Logging then uses the component:

log.info("个人信息:{}", DataMask.toJSONString(person));

Problems Encountered

The initial implementation copied a Map with mapClone.putAll(map) , which is only a shallow copy. When the map contains nested maps, the masking process mutates the original nested objects, violating the requirement that the original data must remain unchanged.

Example test code shows that after masking a map with an inner map, the inner map inside the original object is also altered.

@Test
public void testToJSONString() {
    Map
personMap = new HashMap<>();
    personMap.put("name","张无忌");
    personMap.put("phone","17709141590");
    personMap.put("account","14669037943256249");
    personMap.put("id",1L);

    Map
innerMap = new HashMap();
    innerMap.put("name","张无忌的女儿");
    innerMap.put("phone","18809141567");
    innerMap.put("account","17869037943255678");
    innerMap.put("id",2L);
    personMap.put("daughter",innerMap);

    System.out.println("脱敏后:"+DataMask.toJSONString(personMap));
    System.out.println("脱敏后的原始Map对象:"+personMap);
}

The output confirms that the inner map values were changed in the original map.

Root Cause and Solution

The issue stems from not performing a deep clone of the map. A proper deep‑clone can be achieved by using Guava's view‑based transformation instead of copying.

Guava provides Maps.transformEntries , which returns a lazy view where each entry is transformed based on both key and value. This avoids mutating the source map and works recursively for nested maps and collections.

Maps#transformEntries(Map<K,V1>, Maps.EntryTransformer<? super K,? super V1,V2>)

Using this, we implement a MaskEntryTransformer that handles primitive types, nested maps, and collections.

public class MaskEntryTransformer implements Maps.EntryTransformer
{
    private static final Maps.EntryTransformer
MASK_ENTRY_TRANSFORMER = new MaskEntryTransformer();
    private MaskEntryTransformer() {}
    public static Maps.EntryTransformer
getInstance() { return MASK_ENTRY_TRANSFORMER; }
    @Override
    public Object transformEntry(Object objectKey, Object value) {
        if (value == null) return null;
        if (value instanceof Map) {
            Map valueMap = (Map) value;
            return Maps.transformEntries(valueMap, this);
        }
        if (value instanceof Collection) {
            Collection valueCollection = (Collection) value;
            if (valueCollection.isEmpty()) return valueCollection;
            return Collections2.transform(valueCollection, input -> {
                if (input == null) return null;
                if (input instanceof Map) return Maps.transformEntries((Map) input, this);
                if (input instanceof Collection) return Collections2.transform((Collection) input, this);
                if (!(objectKey instanceof String)) return input;
                String key = (String) objectKey;
                return transformPrimitiveType(key, input);
            });
        }
        if (!(objectKey instanceof String)) return value;
        String key = (String) objectKey;
        return transformPrimitiveType(key, value);
    }
    private Object transformPrimitiveType(final String key, final Object value) {
        // apply masking rule based on key
        return value; // placeholder for actual masking logic
    }
}

The masking utility now simply creates a transformed view and serializes it:

public class DataMask {
    public static String toJSONString(Object object) {
        if (object == null) return null;
        try {
            if (object instanceof Map) {
                Map maskMap = Maps.transformEntries((Map) object, MaskEntryTransformer.getInstance());
                return JsonUtil.toJSONString(maskMap);
            }
            return JsonUtil.toJSONString(object, jsonFilter);
        } catch (Exception e) {
            return object.toString();
        }
    }
}

Guava View Concept

Guava’s view objects (e.g., Splitter#split , Lists#partition , Sets#difference ) implement lazy evaluation: the transformation is performed only when the view is accessed. This approach is performance‑friendly for one‑time traversals but may be less optimal for repeated iterations.

Compatibility Note

Guava’s API compatibility is deliberately limited; many methods are marked @Beta and can change between releases. When building reusable components, avoid @Beta APIs to ensure long‑term stability.

Conclusion

Guava provides a powerful, lazy‑evaluation mechanism that can replace manual deep‑clone logic for log masking. By using Maps.transformEntries together with a custom EntryTransformer , developers can mask nested map structures safely, efficiently, and without altering the original data.

BackendJavaGuavamapLog Masking
Code Ape Tech Column
Written by

Code Ape Tech Column

Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn

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.