【Java】使用fastjson进行序列化时出现空指针异常问题研究
最近在使用fastjson的JSONObject.toJSONString()
方法将bean对象转为字符串的时候报如下错误:
com.alibaba.fastjson.JSONException: write javaBean error, fastjson version 1.2.58, class com.sun.proxy.$Proxy395, fieldName : 0
at com.alibaba.fastjson.serializer.JavaBeanSerializer.write(JavaBeanSerializer.java:523)
at com.alibaba.fastjson.serializer.JavaBeanSerializer.write(JavaBeanSerializer.java:160)
at com.alibaba.fastjson.serializer.JSONSerializer.writeWithFieldName(JSONSerializer.java:333)
at com.alibaba.fastjson.serializer.ASMSerializer_5_Xtsaxx.write(Unknown Source)
at com.alibaba.fastjson.serializer.JSONSerializer.write(JSONSerializer.java:285)
at com.alibaba.fastjson.JSON.toJSONString(JSON.java:740)
at com.alibaba.fastjson.JSON.toJSONString(JSON.java:678)
at com.alibaba.fastjson.JSON.toJSONString(JSON.java:643)
// ...省略一些业务代码
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:749)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
at org.springframework.aop.interceptor.AsyncExecutionInterceptor.lambda$invoke$0(AsyncExecutionInterceptor.java:115)
at java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:266)
at java.util.concurrent.FutureTask.run(FutureTask.java)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
Caused by: java.lang.NullPointerException: null
at com.sun.proxy.$Proxy395.isBooleanValid(Unknown Source)
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.alibaba.fastjson.util.FieldInfo.get(FieldInfo.java:491)
at com.alibaba.fastjson.serializer.FieldSerializer.getPropertyValueDirect(FieldSerializer.java:149)
at com.alibaba.fastjson.serializer.JavaBeanSerializer.write(JavaBeanSerializer.java:291)
... 21 common frames omitted
根据错误信息定位到JavaBeanSerializer的291行代码,调用了getPropertyValueDirect
方法获取属性值,如果出现异常,会进入到catch中的处理逻辑:
(1)如果设置了忽略Getter没有对应字段的情况(SerializerFeature.IgnoreErrorGetter
),属性值置为NULL;
(2)如果没有设置忽略Getter没有对应字段的情况,向上抛出异常,问题基本就出现在这里了,从错误信息中可以看出调用的isBooleanValid方法抛出的,那么猜测应该是该方法没有对应的字段造成的;
public class JavaBeanSerializer extends SerializeFilterable implements ObjectSerializer {
protected void write(JSONSerializer serializer, //
Object object, //
Object fieldName, //
Type fieldType, //
int features,
boolean unwrapped
) throws IOException {
SerializeWriter out = serializer.out;
// ....
try {
if (notApply) {
propertyValue = null;
} else {
try {
// 这里是291行代码
// 获取属性值,这里的实现是执行GETTER方法获取属性值
propertyValue = fieldSerializer.getPropertyValueDirect(object);
} catch (InvocationTargetException ex) {
errorFieldSerializer = fieldSerializer;
// 如果设置忽略Getter没有对应字段的情况
if (out.isEnabled(SerializerFeature.IgnoreErrorGetter)) {
propertyValue = null; // 值置为NULL
} else {
throw ex; // 否则抛出异常
}
}
}
} catch (Exception e) {
String errorMessage = "write javaBean error, fastjson version " + JSON.VERSION;
// 这里是第523行代码,向上抛出异常
throw new JSONException(errorMessage, cause);
} finally {
serializer.context = parent;
}
}
}
进入到FieldSerializer的getPropertyValueDirect
方法,可以看到调用了FieldInfo
的get方法获取属性值,get方法中如果method(也就是GETTER方法)不为空,就通过反射执行方法来获取返回值:
public class FieldSerializer implements Comparable<FieldSerializer> {
public final Method method;
public Object getPropertyValueDirect(Object object) throws InvocationTargetException, IllegalAccessException {
// fieldInfo是具体的字段,这里调用对应的方法,从对象object中获取字段的值
Object fieldValue = fieldInfo.get(object);
if (persistenceXToMany && !TypeUtils.isHibernateInitialized(fieldValue)) {
return null;
}
return fieldValue;
}
}
public class FieldInfo implements Comparable<FieldInfo> {
public Object get(Object javaObject) throws IllegalAccessException, InvocationTargetException {
// 如果method不为空,执行方法获取返回值
return method != null
? method.invoke(javaObject) // 通过反射执行方法
: field.get(javaObject);
}
}
对于出现错误的原因现在已经比较清楚,根据GETTER方法找不到对应的字段导致的,解决方式也比较简单,在序列化的时候通过SerializerFeature.IgnoreErrorGetter
设置忽略即可:
JSONObject.toJSONString(bean, SerializerFeature.IgnoreErrorGetter);
虽然解决问题比较简单,但我们还是来模拟一下,看下在什么情况下会出现这种错误:
(1)首先定义了一个User接口,接口中定义了是否合法以及用户名的get/set方法,当然还定义了一个isBooleanValid,问题就出现这个方法上,它同样用来返回是否合法,只不过返回值是bollean类型,由于是公司其他团队封装的一些公用组件,猜测添加isBooleanValid
方法可能是为了处理某种情况吧:
/**
* User接口
*/
public interface IUser {
public Integer getValid();
public void setValid(Integer nValid);
public String getName();
public void setName(String cName);
default boolean isBooleanValid() {
Integer valid = getValid();
return valid != null && valid == 1;
}
}
(2)创建User的具体实现类,假如有一个管理员类型的用户,它添加了valid和name成员变量,并实现了接口中的get/set方法,这里可以看到并没有booleanValid字段:
public class AdminUser implements IUser {
private Integer valid;
private String name;
@Override
public Integer getValid() {
return valid;
}
@Override
public void setValid(Integer nValid) {
this.valid = nValid;
}
@Override
public String getName() {
return name;
}
@Override
public void setName(String cName) {
this.name = cName;
}
}
(3)创建DTO,前端提交的参数中包含用户的信息:
public class SubmitDTO {
private IUser user;
public IUser getUser() {
return user;
}
public void setUser(IUser user) {
this.user = user;
}
}
(4)测试
情况一(正常序列化)
手动创建AdminUser,并设置相应的信息,调用JSONObject.toJSONString
方法将对象转为JSON字符串:
public class Test {
public static void main(String[] args) {
AdminUser user = new AdminUser();
user.setName("admin");
user.setValid(1);
SubmitDTO submitDTO = new SubmitDTO();
submitDTO.setUser(user);
// 情况一
System.out.println(JSONObject.toJSONString(submitDTO));
}
}
输出:
{"user":{"booleanValid":true,"name":"admin","valid":1}}
对于这种情况,已经明确知道IUser的具体实现类为AdminUser:
情况二(出现异常)
由于参数是前端传入的JSON字符串,后端在接收参数的时候将字符串解析为对应的实体对象,这里我们直接用字符串模拟:
public class Test {
public static void main(String[] args) {
String submitStr = "{\"user\":{\"booleanValid\":true,\"name\":\"admin\",\"valid\":1}}";
SubmitDTO submitDTO1 = JSON.parseObject(submitStr, SubmitDTO.class);
System.out.println(JSONObject.toJSONString(submitDTO1));
}
}
通过parseObject方式解析后的对象,对于接口是通过动态代理创建对应的实现类,此时调用JSONObject.toJSONString
就会出现上面的错误:
Exception in thread "main" com.alibaba.fastjson.JSONException: write javaBean error, fastjson version 1.2.58, class com.sun.proxy.$Proxy0, fieldName : user
at com.alibaba.fastjson.serializer.JavaBeanSerializer.write(JavaBeanSerializer.java:544)
at com.alibaba.fastjson.serializer.JavaBeanSerializer.write(JavaBeanSerializer.java:154)
at com.alibaba.fastjson.serializer.JSONSerializer.writeWithFieldName(JSONSerializer.java:360)
at com.alibaba.fastjson.serializer.ASMSerializer_1_SubmitDTO.write(Unknown Source)
at com.alibaba.fastjson.serializer.JSONSerializer.write(JSONSerializer.java:312)
at com.alibaba.fastjson.JSON.toJSONString(JSON.java:793)
at com.alibaba.fastjson.JSON.toJSONString(JSON.java:731)
at com.alibaba.fastjson.JSON.toJSONString(JSON.java:688)
at Test.main(Test.java:49)
Caused by: java.lang.NullPointerException
at com.sun.proxy.$Proxy0.isBooleanValid(Unknown Source)
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.alibaba.fastjson.util.FieldInfo.get(FieldInfo.java:571)
at com.alibaba.fastjson.serializer.FieldSerializer.getPropertyValueDirect(FieldSerializer.java:143)
at com.alibaba.fastjson.serializer.JavaBeanSerializer.write(JavaBeanSerializer.java:287)
... 8 more
当然解决方式上面已经提到过,设置SerializerFeature.IgnoreErrorGetter
即可:
public class Test {
public static void main(String[] args) {
String submitStr = "{\"user\":{\"booleanValid\":true,\"name\":\"admin\",\"valid\":1}}";
SubmitDTO submitDTO1 = JSON.parseObject(submitStr, SubmitDTO.class);
System.out.println(JSONObject.toJSONString(submitDTO1, SerializerFeature.IgnoreErrorGetter));
}
}
输出:
{"user":{"name":"admin","valid":1}}
不过对于使用动态代理生成实现类的方式,在执行GETTER方法时,如果没有对应的字段会报错的问题有待研究。