【Java】使用fastjson进行序列化时出现空指针异常问题研究

java,使用,fastjson,进行,序列,化时,出现,指针,异常,问题,研究 · 浏览次数 : 184

小编点评

总结内容如下: 1. 为了解决字段找不到的问题,可以使用动态代理生成实现类。 2. 在执行动态代理生成实现类时,如果字段没有对应的字段,会出现异常的问题。 3. 可以通过设置SerializerFeature.IgnoreErrorGetter解决字段找不到的问题。 4. 在使用动态代理生成实现类时,如果字段没有对应的字段,可能会出现异常的问题。 5. 可以通过设置SerializerFeature.IgnoreErrorGetter解决字段找不到的问题。

正文

最近在使用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方法时,如果没有对应的字段会报错的问题有待研究。

与【Java】使用fastjson进行序列化时出现空指针异常问题研究相似的内容:

【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 Caused by: java.lang.NullPointerException: null

Fastjson基础环境配置与Java基础概念

Preface 此篇系列文章将会从 Java 的基础语法开始,以 Fastjson 的各个反序列化漏洞分析为结尾,详细记录如何从一个具有基础面向对象编程但毫无 Java 基础的小白成长为了解 Fastjson 的各个漏洞并能够熟练利用的网络安全人员。 环境配置 我们使用 IDEA 作为开发的 IDE

[转帖]Java使用火焰图查看系统瓶颈

场景 一般情况下,我们会对现有系统进行压测等方式,来了解系统最大的吞吐量等等,通过这种方式得知系统在生产环境下可扛住的压力,如果我们想了解在压测的链路过程中,是哪些地方执行时间过长,影响了系统的吞吐量,可以使用火焰图的方式来观察。 工具 生成火焰图需要两个工具: 1. async-profiler:

[转帖]Java线程的5个使用技巧

https://cloud.tencent.com/developer/article/1179560?from=article.detail.1767994&areaSource=106000.6&traceId=akXSS578NgvCLH6Eiqbla Java线程有哪些不太为人所知的技巧与用

使用Java统计gitlab代码行数

一、背景: 需要对当前公司所有的项目进行代码行数的统计 二、 可实现方式 1.脚本:通过git脚本将所有的项目拉下来并然后通过进行代码行数的统计 样例: echo 创建项目对应的文件夹 mkdir 项目名称echo 切到创建的文件夹中 cd 项目名称echo 进行git初始化 git init ec

如何使用Java创建数据透视表并导出为PDF

摘要:本文由葡萄城技术团队原创并首发。转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具、解决方案和服务,赋能开发者。 前言 数据透视分析是一种强大的工具,可以帮助我们从大量数据中提取有用信息并进行深入分析。而在Java开发中,可以借助PivotTable,通过数据透视分析揭示数据中的隐藏

如何使用Java + React计算个人所得税?

**前言** 在报表数据处理中,Excel公式拥有强大而多样的功能,广泛应用于各个业务领域。无论是投资收益计算、财务报表编制还是保险收益估算,Excel公式都扮演着不可或缺的角色。传统的做法是直接依赖Excel来实现复杂的业务逻辑,并生成相应的Excel文件。因此只需在预设位置输入相应参数,Exce

SpringBoot 集成 Quartz + MySQL

Quartz 简单使用 Java SpringBoot 中,动态执行 bean 对象中的方法 源代码地址 => https://gitee.com/VipSoft/VipBoot/tree/develop/vipsoft-quartz 工作原理解读 只要配置好 DataSource Quartz 会

Java socket 获取gps定位

1.Java socket 获取gps定位的方法 在Java中使用Socket来直接获取GPS定位信息并不直接可行,因为GPS数据通常不是通过Socket通信来获取的。GPS数据通常由设备(如智能手机、GPS接收器)上的GPS硬件模块生成,并通过操作系统或专门的GPS软件库来访问。 然而,如果我们的

Java定时任务实现优惠码

在Java中实现定时任务来发放优惠码,我们可以使用多种方法,比如使用java.util.Timer类、ScheduledExecutorService接口,或者更高级的框架如Spring的@Scheduled注解。这里,我将以ScheduledExecutorService为例来展示如何实现这一功能