【线上问题处理】JSONNull导致的接口500

11 阅读5分钟

问题背景

一天用户反馈一个功能查不出内容详情了,接口返回500。

我们的系统在没有任何报错日志,仅仅在nginx日志中显示返回状态码500

排查异常的数据发现,这个接口并不是所有数据查询都会返回500,而是仅对自昨晚有过变更的数据会产生异常。而这正是我们所依赖的一个外部接口的系统昨晚系统上线的时间,联系上游系统回滚,回滚后,再新建或修改的数据,查询正常。

对比变更前后,查询接口返回的内容,是我们调用的一个外部接口新增了一个字段,字段返回内容是null。如下表:

{"oldItemHaveContent":"name","oldItemNoContent":""}{"oldItemHaveContent":"name","oldItemNoContent":"","newItem":null}

问题分析

新增一个null字段就会有如此大的魔力吗?

至此,我们开始分析自己的系统,我们这个接口的逻辑如下:

Contoller层:

@RequestMapping("getDetail.ajax")
@ResponseBody
public ApiResultDTO getDetail(Integer id) {
    logger.info("in getDetail id = "+id);
    ApiResultDTO resultDTO = new ApiResultDTO();
    try{
        DetailBean obj = service.getDetail(id);
        resultDTO.setCode(0);
        resultDTO.setSuccess(true);
        resultDTO.setObj(obj);
    } catch (Exception e){
        logger.error("error :" + e.getMessage(), e);
        resultDTO.setCode(-1);
        resultDTO.setSuccess(false);
        resultDTO.setMessage("error:" + e.getMessage());
    }
    return resultDTO;
}

核心根因定位

  1. 技术栈冲突:系统使用 net.sf.json 解析外部接口返回值,同时使用 Jackson 作为 SpringMVC 默认的 HTTP 消息转换器(MappingJackson2HttpMessageConverter)做接口响应序列化;
  2. NULL 对象差异net.sf.json 库会将 JSON 中的null封装为 JSONNull对象 (而非 Java 原生 null);
  3. 序列化崩溃:Jackson 默认序列化器不识别JSONNull,调用其isEmpty()方法时直接抛出JSONException: Object is null
  4. 异常无法捕获:该异常发生在Controller 方法执行完毕后、Spring 响应序列化阶段,脱离了 Controller 的 try-catch 范围,因此无业务日志、直接返回 500

关键知识点:JSON 库对null的处理差异

Jackson(Spring 默认)

默认行为:Java 对象为null,直接序列化为标准null;可通过@JsonInclude(JsonInclude.Include.NON_NULL)忽略 null 字段。

net.sf.json(问题根源)

默认行为:将null封装为 【JSONNull单例对象】,它不是 Java 原生 null,而是一个有类型的对象;调用JSONNull.isEmpty()会直接抛出异常,这是接口 500 的直接原因。

Fastjson(阿里巴巴)

默认行为:自动忽略 null 字段,不会封装特殊对象,与 Jackson 兼容性极佳。

复现验证代码

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
import net.sf.json.JSONNull;
import java.util.*;
import org.apache.log4j.Logger;
import net.sf.json.JSONObject;


public class Test {

    private static final Logger logger = Logger.getLogger(Test.class);

    public static void main(String args[]) throws Exception {
        Student student = new Student();
        student.setName("张三");

        ObjectMapper objectMapper = new ObjectMapper();
        String jsonString = objectMapper.writeValueAsString(student);
        logger.info("jsonString = " + jsonString);

        // 测试 net.sf.json.JSONObject
        JSONObject netJson = JSONObject.fromObject(jsonString);
        logger.info("netJson = " + netJson);

        if (netJson.get("cluster") == null) {
            logger.info("equal null");
        }
        if (Objects.isNull(netJson.get("cluster"))) {
            logger.info("is null");
        }
        if (netJson.get("cluster") instanceof JSONNull) {
            logger.info("is JSONNull");
        }
        try {
            String resultJson = objectMapper.writeValueAsString(netJson);
            logger.info("return netJson as JSON = " + resultJson);
        } catch (Exception e) {
            logger.error("Error converting netJson to JSON", e);
        }

        // 测试 com.alibaba.fastjson.JSONObject
        com.alibaba.fastjson.JSONObject alibabaJson = com.alibaba.fastjson.JSONObject.parseObject(jsonString);
        logger.info("alibabaJson = " + alibabaJson);
        if (alibabaJson.get("cluster") == null) {
            logger.info("equal null");
        }
        if (Objects.isNull(alibabaJson.get("cluster"))) {
            logger.info("is null");
        }
        if (alibabaJson.get("cluster") instanceof JSONNull) {
            logger.info("is JSONNull");
        }
        try {
            String resultJson = objectMapper.writeValueAsString(alibabaJson);
            logger.info("return netJson as JSON = " + resultJson);
        } catch (Exception e) {
            logger.error("Error converting netJson to JSON", e);
        }
    }

    @Data
    public static class Student {
        String name;
        String cluster;
        Integer age;
    }

}

输出:

2025-06-29_15:06:17:304	[INFO]Test.java	19	main	jsonString = {"name":"张三","cluster":null,"age":null}
2025-06-29_15:06:17:333	[INFO]Test.java	23	main	netJson = {"name":"张三","cluster":null,"age":null}
2025-06-29_15:06:17:333	[INFO]Test.java	32	main	is JSONNull
2025-06-29_15:06:17:338	[ERROR]Test.java	38	main	Error converting netJson to JSON
com.fasterxml.jackson.databind.JsonMappingException: Object is null (through reference chain: net.sf.json.JSONObject["cluster"]->net.sf.json.JSONNull["empty"])
	at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:390)
	at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:349)
	at com.fasterxml.jackson.databind.ser.std.StdSerializer.wrapAndThrow(StdSerializer.java:316)
	at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:778)
	at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:178)
	at com.fasterxml.jackson.databind.ser.std.MapSerializer.serializeFields(MapSerializer.java:808)
	at com.fasterxml.jackson.databind.ser.std.MapSerializer.serializeWithoutTypeInfo(MapSerializer.java:764)
	at com.fasterxml.jackson.databind.ser.std.MapSerializer.serialize(MapSerializer.java:720)
	at com.fasterxml.jackson.databind.ser.std.MapSerializer.serialize(MapSerializer.java:35)
	at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider._serialize(DefaultSerializerProvider.java:480)
	at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider.serializeValue(DefaultSerializerProvider.java:319)
	at com.fasterxml.jackson.databind.ObjectMapper._writeValueAndClose(ObjectMapper.java:4487)
	at com.fasterxml.jackson.databind.ObjectMapper.writeValueAsString(ObjectMapper.java:3742)
	at Test.main(Test.java:35)
Caused by: net.sf.json.JSONException: Object is null
	at net.sf.json.JSONNull.isEmpty(JSONNull.java:69)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:689)
	at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:770)
	... 10 more
2025-06-29_15:06:17:365	[INFO]Test.java	43	main	alibabaJson = {"name":"张三"}
2025-06-29_15:06:17:365	[INFO]Test.java	45	main	equal null
2025-06-29_15:06:17:365	[INFO]Test.java	48	main	is null
2025-06-29_15:06:17:366	[INFO]Test.java	55	main	return netJson as JSON = {"cluster":null,"name":"张三","age":null}

关键输出(验证根因)

[INFO] is JSONNull
[ERROR] Error converting netJson to JSON
Caused by: net.sf.json.JSONException: Object is null
        at net.sf.json.JSONNull.isEmpty(JSONNull.java:69)

解决办法

方案 1:替换 JSON 库(推荐,根治问题)

放弃net.sf.json,统一使用Fastjson解析外部接口,从根源消除JSONNull对象,与 Spring 的 Jackson 完美兼容。

改造步骤

  1. 移除net.sf.json依赖;
  2. 使用com.alibaba.fastjson.JSONObject解析第三方接口返回值;
  3. 无需修改序列化逻辑,代码零侵入、稳定性最高。
// 旧代码(net.sf.json)
JSONObject resp = JSONObject.fromObject(thirdResponse);

// 新代码(fastjson,推荐)
com.alibaba.fastjson.JSONObject resp = JSON.parseObject(thirdResponse);

方案 2:自定义 Jackson 序列化器(兼容改造)

保留net.sf.json,重写MappingJackson2HttpMessageConverter,自定义序列化器将JSONNull转为 Java null。

步骤 1:编写通用序列化器

import net.sf.json.JSONObject;
import net.sf.json.JSONNull;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

public class JSONObjectSerializer extends JsonSerializer<JSONObject> {
    @Override
    public void serialize(JSONObject value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        if (value == null || value.isNullObject()) {
            gen.writeNull();
            return;
        }
        // 遍历转换:JSONNull → Java null
        Map<String, Object> map = new HashMap<>();
        for (Object key : value.keySet()) {
            Object val = value.get(key);
            map.put(key.toString(), val instanceof JSONNull ? null : val);
        }
        gen.writeObject(map);
    }
}

步骤 2:全局注册序列化器

import com.fasterxml.jackson.databind.module.SimpleModule;
import net.sf.json.JSONObject;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;

@Configuration
public class JacksonConfig {
    @Bean
    public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        SimpleModule module = new SimpleModule();
        // 全局处理net.sf.json.JSONObject
        module.addSerializer(JSONObject.class, new JSONObjectSerializer());
        converter.getObjectMapper().registerModule(module);
        return converter;
    }
}

方案 3:字段注解(局部使用)

仅对包含JSONObject的字段单独指定序列化器,适合少量字段改造。

import com.fasterxml.jackson.databind.annotation.JsonSerialize;

// 在实体类字段上添加注解
@JsonSerialize(using = JSONObjectSerializer.class)
private JSONObject detailObj;

方案 4:Jackson 注解(辅助优化)

配合@JsonInclude忽略 null 值,减少无效字段输出,无法单独解决 500 问题,需配合方案 1/2 使用。

// 类上添加:序列化时忽略null字段
@JsonInclude(JsonInclude.Include.NON_NULL)
public class DetailBean {
    // 字段...
}

问题思考与优化建议

1. 状态码异常监控体系建设

  • 监控维度:Nginx 500 状态码告警、应用服务无日志 500 告警;
  • 监控目的:提前感知序列化 / 框架层异常,避免用户先于研发发现问题;
  • 落地方式:对接 ELK/Prometheus,配置 500 比例阈值告警。

2. 第三方接口兼容性防护

  • 契约校验:对接入的第三方接口做字段白名单校验,禁止未知字段 / 异常 null 值传入内部系统;
  • 数据隔离:第三方返回值不直接用于接口响应,必须转换为内部自定义 Java 对象,杜绝第三方库对象泄漏。