问题背景
一天用户反馈一个功能查不出内容详情了,接口返回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;
}
核心根因定位
- 技术栈冲突:系统使用
net.sf.json解析外部接口返回值,同时使用Jackson作为 SpringMVC 默认的 HTTP 消息转换器(MappingJackson2HttpMessageConverter)做接口响应序列化; - NULL 对象差异:
net.sf.json库会将 JSON 中的null封装为JSONNull对象 (而非 Java 原生 null); - 序列化崩溃:Jackson 默认序列化器不识别
JSONNull,调用其isEmpty()方法时直接抛出JSONException: Object is null; - 异常无法捕获:该异常发生在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 完美兼容。
改造步骤:
- 移除
net.sf.json依赖; - 使用
com.alibaba.fastjson.JSONObject解析第三方接口返回值; - 无需修改序列化逻辑,代码零侵入、稳定性最高。
// 旧代码(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 对象,杜绝第三方库对象泄漏。