背景
为了在日志中把出入参打印出来,以便验证链路和排查问题,在日志中将入参用fastjson格式化成字符串输出,结果遇到了NPE。
问题复现
示例代码
public static void main(String[] args) {
OrganizationId orgId = new OrganizationId();
NodeName name = new NodeName("test");
Node node = new Node();
node.setName(name);
node.setOrganizationId(orgId);
System.out.println(JSONObject.toJSONString(node));
}
错误提示
发现是OrganizationId对象里的方法报空指针了,赶紧看一眼这个类:
public class OrganizationId {
private String id;
public Long getIdToLong() {
return Long.valueOf(this.id);
}
}
怎么会运行到 getIdToLong 方法呢?
问题排查
对 JSONObject.toJSONString 方法进行反复 debug 之后,终于发现了原因,以下是具体路径:
public static String toJSONString(Object object,
SerializeConfig config,
SerializeFilter[] filters,
String dateFormat,
int defaultFeatures,
SerializerFeature... features) {
SerializeWriter out = new SerializeWriter(null, defaultFeatures, features);
try {
JSONSerializer serializer = new JSONSerializer(out, config);
if (dateFormat != null && dateFormat.length() != 0) {
serializer.setDateFormat(dateFormat);
serializer.config(SerializerFeature.WriteDateUseDateFormat, true);
}
if (filters != null) {
for (SerializeFilter filter : filters) {
serializer.addFilter(filter);
}
}
serializer.write(object);
return out.toString();
} finally {
out.close();
}
}
往下到 serializer.write 方法:
public final void write(Object object) {
if (object == null) {
out.writeNull();
return;
}
Class<?> clazz = object.getClass();
ObjectSerializer writer = getObjectWriter(clazz);
try {
writer.write(this, object, null, null, 0);
} catch (IOException e) {
throw new JSONException(e.getMessage(), e);
}
}
再到 getObjectWriter,注意入参create传了true:
public ObjectSerializer getObjectWriter(Class<?> clazz) {
return getObjectWriter(clazz, true);
}
在 getObjectWriter 的核心具体实现中,走到了自定义对象序列化的流程:
// ......
if (create) {
writer = createJavaBeanSerializer(clazz);
put(clazz, writer);
}
createJavaBeanSerializer 往下到 TypeUtils.buildBeanInfo:
public final ObjectSerializer createJavaBeanSerializer(Class<?> clazz) {
SerializeBeanInfo beanInfo = TypeUtils.buildBeanInfo(clazz, null, propertyNamingStrategy, fieldBased);
if (beanInfo.fields.length == 0 && Iterable.class.isAssignableFrom(clazz)) {
return MiscCodec.instance;
}
return createJavaBeanSerializer(beanInfo);
}
在 buildBeanInfo 中,由于入参 fieldBased 是false,会走到 computeGetters 的逻辑:
List<FieldInfo> fieldInfoList = fieldBased
? computeGettersWithFieldBase(beanType, aliasMap, false, propertyNamingStrategy) //
: computeGetters(beanType, jsonType, aliasMap, fieldCacheMap, false, propertyNamingStrategy);
看到 computeGetters 的名字,感觉八成是这里了,发现里面有一段逻辑是扫描以 get 开头的方法名,把方法后缀变成一个属性,后续在获取对应属性时,会去运行对应的 getter 方法:
if(methodName.startsWith("get")){
// 省略...
// 从方法名中解析出属性名
propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
}
从上面这段代码可以获取到 propertyName 的值为 idToLong,并且对应的 fieldInfo 是 getIdToLong 方法。
到这里基本水落石出了,原来是fastjson序列化是扫描以 “get”(还有“is”) 开头的方法,并且从该方法名中提取属性,如果对应的方法中存在问题,那么这里就可能遇到对应的异常,就像本文遇到的NPE。
解决方案
1、 业务逻辑中处理:保证 node 对象中的 orgId 不为空,避免NPE。
2、日志打印中处理:不序列化整个对象,只打出关键信息,避开可能为空的字段。
3、 在调用JSON.toJSONString的时候,加上SerializerFeature.IgnoreNonFieldGetter参数,忽略掉所有没有对应成员变量(Field)的getter函数,可以正常序列化。
JSONObject.toJSONString(node, SerializerFeature.IgnoreNonFieldGetter)
4、 通过在函数上 getXxx() 增加@JSONField(serialize = false)注解,也能达到同样的效果。
@JSONField(serialize = false)
public Long getIdToLong() {
return Long.valueOf(this.id);
}
computeGetters 中消费注解的代码:
JSONField annotation = method.getAnnotation(JSONField.class);
// ...
if(annotation != null){
if(!annotation.serialize()){
continue;
}
// ...
if(methodName.startsWith("get")){
// ...
总结
fastjson 将对象转为 string 时,会把以“get”开头的方法认为是属性的 getter,把 getXXX 方法后面的 XXX 变成一个属性,并通过 getXXX 方法去获取,如果get方法内存在异常逻辑,就可能报错。可以尽量避免使用JSON打日志。
附录
1、阿里巴巴开发规约
2、默认根据get方法进行序列化,根据java bean的定义,通过反射来获取,javaBean定义见:什么是JavaBean、bean?