fastjson2 icon indicating copy to clipboard operation
fastjson2 copied to clipboard

[BUG] 空List反序列化后向其中添加元素时报java.lang.UnsupportedOperationException异常

Open dcrpp opened this issue 1 year ago • 10 comments

问题描述

ObjectReaderImplList中239行使用了Collections.emptyList(),导致空List反序列化后向其中添加元素时报java.lang.UnsupportedOperationException异常

环境信息

  • 版本信息:[Fastjson2 2.0.40]

重现步骤

参见以下bug复现代码

public class App {

    public static void main(String[] args) {
        List<Person> sourceList = new ArrayList<>();
//        sourceList.add(new Person("zhangsan"));
        Map<String, Object> source = new HashMap<>();
        source.put("data", sourceList);

        String json = JSON.toJSONString(source);

        System.out.println(json);

        JSONObject target = JSON.parseObject(json);
        Type type = getType();
        List<Person> targetList = target.getObject("data", type);

        System.out.println(targetList.getClass().getName());

        targetList.add(new Person("lisi"));

        System.out.println(JSON.toJSONString(targetList));
    }

    public static Type getType() {
        try {
            App app = new App();
            Method method = app.getClass().getMethod("test");
            return method.getGenericReturnType();
        } catch (Throwable e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

    public List<Person> test() {
        return new ArrayList<>();
    }

    public static class Person {

        private String name;

        public Person() {

        }

        public Person(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

    }

}

期待的正确结果

空List反序列化后可以正常向其中添加元素。

全面排查并仔细评估所有使用了Collections.emptyList()和Collections.emptySet()的地方,有类似问题的地方建议一并修改。

相关日志输出

以上bug复现代码日志输出如下

{"data":[]}
java.util.Collections$EmptyList
Exception in thread "main" java.lang.UnsupportedOperationException
	at java.base/java.util.AbstractList.add(AbstractList.java:153)
	at java.base/java.util.AbstractList.add(AbstractList.java:111)
	at com.dcr.demo.App.main(App.java:34)

附加信息

我使用fastjson2做Openfeign接口的序列化及反序列化,当Openfeign接口方法返回值不为空时,向list中添加元素不报异常。当返回空List后,再向list中添加元素时会报UnsupportedOperationException异常。

dcrpp avatar Sep 05 '23 17:09 dcrpp

https://oss.sonatype.org/content/repositories/snapshots/com/alibaba/fastjson2/fastjson2/2.0.41-SNAPSHOT/ 问题已修复,请用2.0.41-SNAPSHOT帮忙验证

wenshao avatar Sep 09 '23 11:09 wenshao

https://github.com/alibaba/fastjson2/releases/tag/2.0.41 问题已修复,请用新版本

wenshao avatar Oct 06 '23 02:10 wenshao

@wenshao 此问题在 2.0.42以及以后版本又出现了

iwangjie avatar Jan 05 '24 03:01 iwangjie

从42-45版本都有这个问题

cycle2zhou avatar Jan 15 '24 10:01 cycle2zhou

@cycle2zhou 新版本的异常信息能提供下么?

wenshao avatar Jan 27 '24 16:01 wenshao

@cycle2zhou 新版本的异常信息能提供下么?

温少,这是我们升级dubbo-3.2.10后遇到的异常调用栈。 @wenshao

{"timestamp":"2024-01-26T14:02:04.917",
"message":" [DUBBO] Decode rpc result failed: org.apache.dubbo.common.serialize.SerializationException: java.lang.UnsupportedOperationException, dubbo version: 3.2.10, current host: 192.168.108.128, error code: 4-20. This may be caused by , go to https://dubbo.apache.org/faq/4/20 to find instructions. ","thread":"DubboServerHandler-192.168.108.128:8504-thread-392","logger":"org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcResult","level":"WARN","trace_id":"21c6f0fc84474d55a2e32f6cd7ddbc13.70.17062485467755809","exception":""}

java.io.IOException: org.apache.dubbo.common.serialize.SerializationException: java.lang.UnsupportedOperationException
  at org.apache.dubbo.common.serialize.DefaultSerializationExceptionWrapper.handleToIOException(DefaultSerializationExceptionWrapper.java:353)
  at org.apache.dubbo.common.serialize.DefaultSerializationExceptionWrapper.access$000(DefaultSerializationExceptionWrapper.java:27)
  at org.apache.dubbo.common.serialize.DefaultSerializationExceptionWrapper$ProxyObjectInput.readObject(DefaultSerializationExceptionWrapper.java:172)
  at org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcResult.handleValue(DecodeableRpcResult.java:176)
  at org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcResult.decode(DecodeableRpcResult.java:110)
  at org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcResult.decode(DecodeableRpcResult.java:153)
  at org.apache.dubbo.remoting.transport.DecodeHandler.decode(DecodeHandler.java:61)
  at org.apache.dubbo.remoting.transport.DecodeHandler.received(DecodeHandler.java:49)
  at org.apache.dubbo.remoting.transport.dispatcher.ChannelEventRunnable.run(ChannelEventRunnable.java:64)
  at org.apache.dubbo.common.threadpool.ThreadlessExecutor$RunnableWrapper.run(ThreadlessExecutor.java:151)
  at org.apache.dubbo.common.threadpool.ThreadlessExecutor.waitAndDrain(ThreadlessExecutor.java:77)
  at org.apache.dubbo.rpc.AsyncRpcResult.get(AsyncRpcResult.java:219)
  at org.apache.dubbo.rpc.protocol.AbstractInvoker.waitForResultIfSync(AbstractInvoker.java:292)
  at org.apache.dubbo.rpc.protocol.AbstractInvoker.invoke(AbstractInvoker.java:194)
  at org.apache.dubbo.rpc.listener.ListenerInvokerWrapper.invoke(ListenerInvokerWrapper.java:71)
  at io.seata.integration.dubbo.ApacheDubboTransactionPropagationFilter.invoke(ApacheDubboTransactionPropagationFilter.java:69)
  at com.leoao.starter.dubbo.generic.GenericFilterWrapper.invoke(GenericFilterWrapper.java:67)
  at org.apache.dubbo.rpc.cluster.filter.FilterChainBuilder$CopyOfFilterChainNode.invoke(FilterChainBuilder.java:349)
  at com.alibaba.dubbo.rpc.Invoker$CompatibleInvoker.invoke(Invoker.java:77)
  at io.seata.integration.dubbo.alibaba.AlibabaDubboTransactionPropagationFilter.invoke(AlibabaDubboTransactionPropagationFilter.java:45)
  at com.alibaba.dubbo.rpc.Filter.invoke(Filter.java:34)
  at com.leoao.starter.dubbo.generic.GenericFilterWrapper.invoke(GenericFilterWrapper.java:67)
  at org.apache.dubbo.rpc.cluster.filter.FilterChainBuilder$CopyOfFilterChainNode.invoke(FilterChainBuilder.java:349)
  at org.apache.dubbo.rpc.filter.RpcExceptionFilter.invoke(RpcExceptionFilter.java:40)
  at com.leoao.starter.dubbo.generic.GenericFilterWrapper.invoke(GenericFilterWrapper.java:67)
  at org.apache.dubbo.rpc.cluster.filter.FilterChainBuilder$CopyOfFilterChainNode.invoke(FilterChainBuilder.java:349)
  at org.apache.dubbo.rpc.filter.ActiveLimitFilter.invoke(ActiveLimitFilter.java:85)
  at com.leoao.starter.dubbo.generic.GenericFilterWrapper.invoke(GenericFilterWrapper.java:67)
  at org.apache.dubbo.rpc.cluster.filter.FilterChainBuilder$CopyOfFilterChainNode.invoke(FilterChainBuilder.java:349)
  at org.apache.dubbo.rpc.cluster.filter.FilterChainBuilder$CallbackRegistrationInvoker.invoke(FilterChainBuilder.java:197)
  at org.apache.dubbo.rpc.protocol.ReferenceCountInvokerWrapper.invoke(ReferenceCountInvokerWrapper.java:106)
  at org.apache.dubbo.rpc.cluster.support.AbstractClusterInvoker.invokeWithContext(AbstractClusterInvoker.java:412)
  at org.apache.dubbo.rpc.cluster.support.FailoverClusterInvoker.doInvoke(FailoverClusterInvoker.java:82)
  at org.apache.dubbo.rpc.cluster.support.AbstractClusterInvoker.invoke(AbstractClusterInvoker.java:366)
  at org.apache.dubbo.rpc.cluster.router.RouterSnapshotFilter.invoke(RouterSnapshotFilter.java:46)
  at org.apache.dubbo.rpc.cluster.filter.FilterChainBuilder$CopyOfFilterChainNode.invoke(FilterChainBuilder.java:349)
  at ...
Caused by: org.apache.dubbo.common.serialize.SerializationException: java.lang.UnsupportedOperationException
  ... 150 common frames omitted
Caused by: java.lang.UnsupportedOperationException: null
  at java.util.AbstractList.add(AbstractList.java:148)
  at java.util.AbstractList.add(AbstractList.java:108)
  at com.alibaba.fastjson2.reader.ORG_20_5_Page.readJSONBObject(Unknown Source)
  at com.alibaba.fastjson2.JSONB.parseObject(JSONB.java:540)
  at org.apache.dubbo.common.serialize.fastjson2.FastJson2ObjectInput.readObject(FastJson2ObjectInput.java:171)
  at org.apache.dubbo.common.serialize.DefaultSerializationExceptionWrapper$ProxyObjectInput.readObject(DefaultSerializationExceptionWrapper.java:170)
  ... 147 common frames omitted

bert82503 avatar Jan 29 '24 01:01 bert82503

Page的类结构可以提供下么?

wenshao avatar Jan 29 '24 09:01 wenshao

Page的类结构可以提供下么?

温少,这个问题调整后已修复 @wenshao

先前使用private List<T> data = Collections.emptyList();,这个问题调整后已修复。

package basic.model;

import lombok.Data;
import org.springframework.util.CollectionUtils;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * 分页模型
 */
@Data
public class Page<T> implements Serializable {
    private static final long serialVersionUID = 201612008194101999L;

    /**
     * 页面元素
     */
    // 有问题
    private List<T> data = Collections.emptyList();
    // 修复正常
//    private List<T> data = new ArrayList<>();

    /**
     * 当前页码
     */
    private Integer pageIndex = 0;

    /**
     * 每页展示数
     */
    private Integer pageSize = 0;

    /**
     * 总页码
     */
    private Integer totalPage = 0;

    /**
     * 总数
     */
    private Long total = 0L;

    /**
     * 构建
     */
    public static <T> Page<T> of (List<T> data, Integer pageIndex, Integer pageSize, Long total) {
        Page<T> page = new Page<>();
        page.setData(data);
        page.setPageIndex(pageIndex);
        page.setPageSize(pageSize);
        page.setTotal(total);
        page.setTotalPage(total, pageSize);
        return page;
    }

    @Deprecated
    public void setData(List<T> items) {
        if (CollectionUtils.isEmpty(items)) {
            return;
        }
        this.data = items.stream().filter(Objects::nonNull).collect(Collectors.toList());
    }

    /**
     * 设置总页数
     *
     * @param total 总数
     * @param pageSize 分页大小
     */
    @Deprecated
    public void setTotalPage (Long total, Integer pageSize) {
        this.total = total;
        this.pageSize = pageSize;
        if (total == null || total == 0) {
            return;
        }

        if (pageSize == null || pageSize == 0) {
            return;
        }

        int totalPage;
        if (0 == total % pageSize)
            totalPage = (int) (total / pageSize);
        else
            totalPage = (int) (total / pageSize + 1);
        if(0==totalPage)
            totalPage = 1;
        this.totalPage = totalPage;
    }
}

bert82503 avatar Jan 29 '24 10:01 bert82503

问题无法重现,能够调试看下序列化之前这个Page里面的data是什么类型么?或者调试反序列化报错UnsupportedOperationException时当前List的类型

wenshao avatar Jan 29 '24 10:01 wenshao

问题无法重现,能够调试看下序列化之前这个Page里面的data是什么类型么?或者调试反序列化报错UnsupportedOperationException时当前List的类型

温少,data是java.util.Collections.EmptyList类型 @wenshao

package basic.model;

import lombok.Data;
import org.springframework.util.CollectionUtils;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * 分页模型
 */
@Data
public class Page<T> implements Serializable {
    private static final long serialVersionUID = 201612008194101999L;

    /**
     * 页面元素
     */
    // 有问题
    private List<T> data = Collections.emptyList();
    // 修复正常
//    private List<T> data = new ArrayList<>();

    /**
     * 当前页码
     */
    private Integer pageIndex = 0;

    /**
     * 每页展示数
     */
    private Integer pageSize = 0;

    /**
     * 总页码
     */
    private Integer totalPage = 0;

    /**
     * 总数
     */
    private Long total = 0L;

    /**
     * 构建
     */
    public static <T> Page<T> of (List<T> data, Integer pageIndex, Integer pageSize, Long total) {
        Page<T> page = new Page<>();
        page.setData(data);
        page.setPageIndex(pageIndex);
        page.setPageSize(pageSize);
        page.setTotal(total);
        page.setTotalPage(total, pageSize);
        return page;
    }

    @Deprecated
    public void setData(List<T> items) {
        if (CollectionUtils.isEmpty(items)) {
            return;
        }
        this.data = items.stream().filter(Objects::nonNull).collect(Collectors.toList());
    }

    /**
     * 设置总页数
     *
     * @param total 总数
     * @param pageSize 分页大小
     */
    @Deprecated
    public void setTotalPage (Long total, Integer pageSize) {
        this.total = total;
        this.pageSize = pageSize;
        if (total == null || total == 0) {
            return;
        }

        if (pageSize == null || pageSize == 0) {
            return;
        }

        int totalPage;
        if (0 == total % pageSize)
            totalPage = (int) (total / pageSize);
        else
            totalPage = (int) (total / pageSize + 1);
        if(0==totalPage)
            totalPage = 1;
        this.totalPage = totalPage;
    }
}

Page类只有JDK生成的默认构造函数,其data字段值会初始化为Collections.emptyList()。 提供者端是通过静态方法Page.of(...)构造Page对象,会返回ArrayList<>()的多条数据; 消费者端,FastJson2通过默认构造函数创建Page对象,data字段只能使用默认值Collections.emptyList(),其实现类是jdk-8的EmptyList<E> extends AbstractList<E>,EmptyList未覆盖add方法,AbstractList定义了add(int index, E element)

image image

bert82503 avatar Jan 31 '24 03:01 bert82503

这个不好解决呢

wenshao avatar Feb 23 '24 21:02 wenshao

这个不好解决呢

温少,这个我们通过初始化其他类型,升级解决了。是我们使用的问题,不是框架层面的。不用解决哈

bert82503 avatar Feb 24 '24 16:02 bert82503

https://github.com/alibaba/fastjson2/releases/tag/2.0.48 请用新版本

wenshao avatar Mar 25 '24 01:03 wenshao

@wenshao 使用了2.0.49,此问题还是会出现,会考虑优化这个问题吗?

Alleninggx avatar Jun 22 '24 08:06 Alleninggx

https://github.com/alibaba/fastjson2/releases/tag/2.0.52 问题已修复,请用新版本

wenshao avatar Jul 14 '24 14:07 wenshao