You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ObjectMapper转Map时忽略Null值方法引发结果不一致问题排查

问题:ObjectMapper转换Map时Null值处理逻辑受调用顺序影响

在使用ObjectMapper将对象转换为Map时,遇到了Null值处理逻辑受调用顺序影响的问题。编写的JsonUtil工具类中,toMap方法预期保留Null值,toMapIgnoreNull方法预期忽略Null值,但实际测试发现两者的行为会被调用顺序改变。

工具类代码

@Slf4j
public class JsonUtil {
    private static final ObjectMapper objectMapper;
    static {
        objectMapper = new ObjectMapper();
        JavaTimeModule javaTimeModule = new JavaTimeModule();
        javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        objectMapper.registerModule(javaTimeModule);
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
    }
    public static Map<String, Object> toMap(Object obj) {
        try {
            return objectMapper.convertValue(obj, new TypeReference<>() {
            });
        } catch (Exception e) {
            log.error("failed:", e);
            return Map.of();
        }
    }

    public static Map<String, Object> toMapIgnoreNull(Object obj) {
        try {
            return objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL)
                    .convertValue(obj, new TypeReference<>() {
                    });
        } catch (Exception e) {
            log.error("failed:", e);
            return Map.of();
        }
    }
}

测试代码

@Data
@EqualsAndHashCode(callSuper = false)
static class User {
    private String username;
    private String password;
}

public static void johnOut() {
    User john = new User();
    john.setUsername("John");
    Map<String, Object> johnMap = toMap(john);
    System.out.println("result 1: " + johnMap);
    Map<String, Object> JohnIgnoreNullMap = toMapIgnoreNull(john);
    System.out.println("result 2: " + JohnIgnoreNullMap);
}

private static void jimOut() {
    User jim = new User();
    jim.setUsername("jim");
    Map<String, Object> jimIgnoreNullMap = toMapIgnoreNull(jim);
    System.out.println("result 3: " + jimIgnoreNullMap);
    Map<String, Object> jimMap = toMap(jim);
    System.out.println("result 4: " + jimMap);
}

public static void main(String[] args) {
    johnOut();
    jimOut();
}

测试结果

  • 当先调用johnOut()时,输出:
result 1: {username=John, password=null}
result 2: {username=John, password=null}
result 3: {username=jim, password=null}
result 4: {username=jim, password=null}
  • 当先调用jimOut()时,输出:
result 3: {username=jim}
result 4: {username=jim}
result 1: {username=John}
result 2: {username=John}

原因分析

问题出在静态单例的ObjectMapper实例上:

  • objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL)是修改全局配置的操作,调用后这个配置会一直生效,后续所有使用该ObjectMapper的操作都会遵循这个规则。
  • 比如先调用toMapIgnoreNull,会把全局配置改成忽略Null值,之后再调用toMap时,ObjectMapper已经是忽略Null的状态,自然不会保留Null值;反之先调用toMap,初始配置是保留Null,直到toMapIgnoreNull修改配置后,后续所有方法都用新配置。

解决方法

有几种可行的方案,推荐使用第二种(兼顾性能和线程安全):

方案1:每次调用时创建新的ObjectMapper实例

在每个方法内部创建独立的ObjectMapper,避免全局配置被修改:

public static Map<String, Object> toMap(Object obj) {
    try {
        ObjectMapper mapper = new ObjectMapper();
        JavaTimeModule javaTimeModule = new JavaTimeModule();
        javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        mapper.registerModule(javaTimeModule);
        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        return mapper.convertValue(obj, new TypeReference<>() {});
    } catch (Exception e) {
        log.error("failed:", e);
        return Map.of();
    }
}

public static Map<String, Object> toMapIgnoreNull(Object obj) {
    try {
        ObjectMapper mapper = new ObjectMapper();
        JavaTimeModule javaTimeModule = new JavaTimeModule();
        javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        mapper.registerModule(javaTimeModule);
        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        return mapper.convertValue(obj, new TypeReference<>() {});
    } catch (Exception e) {
        log.error("failed:", e);
        return Map.of();
    }
}

缺点:每次创建ObjectMapper会有一定性能开销,适合调用频率不高的场景。

方案2:基于全局单例创建副本修改配置

利用ObjectMapper的copy()方法创建一个副本,修改副本的配置而不影响原实例:

public static Map<String, Object> toMap(Object obj) {
    try {
        // 使用原实例的配置,不做修改
        return objectMapper.convertValue(obj, new TypeReference<>() {});
    } catch (Exception e) {
        log.error("failed:", e);
        return Map.of();
    }
}

public static Map<String, Object> toMapIgnoreNull(Object obj) {
    try {
        // 创建副本并修改配置,不影响原单例
        ObjectMapper mapper = objectMapper.copy();
        mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        return mapper.convertValue(obj, new TypeReference<>() {});
    } catch (Exception e) {
        log.error("failed:", e);
        return Map.of();
    }
}

优点:复用原实例的模块和基础配置,性能开销小,同时保证原实例配置不被修改,线程安全。

方案3:使用ObjectWriter处理不同配置

通过ObjectWriter来实现不同的序列化规则,不修改原ObjectMapper的配置:

public static Map<String, Object> toMap(Object obj) {
    try {
        return objectMapper.convertValue(obj, new TypeReference<>() {});
    } catch (Exception e) {
        log.error("failed:", e);
        return Map.of();
    }
}

public static Map<String, Object> toMapIgnoreNull(Object obj) {
    try {
        ObjectWriter writer = objectMapper.writer().with(JsonInclude.Include.NON_NULL);
        // 先序列化为JSON字符串再转回Map,或者直接用writer的convertValue
        return writer.convertValue(obj, new TypeReference<>() {});
    } catch (Exception e) {
        log.error("failed:", e);
        return Map.of();
    }
}

优点:同样不修改原实例配置,ObjectWriter是线程安全的,适合频繁调用的场景。

内容的提问来源于stack exchange,提问作者尚振璞

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 21:59:55