背景
为了在日志中把出入参打印出来,以便验证链路和排查问题,在日志中将入参用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?



















