EsotericSoftware / kryo

Java binary serialization and cloning: fast, efficient, automatic
BSD 3-Clause "New" or "Revised" License
6.19k stars 823 forks source link

Sets.newConcurrentHashSet() Serialization exception #802

Closed fallsea closed 3 years ago

fallsea commented 3 years ago

Set<String> set = Sets.newConcurrentHashSet(); After creating a collection in this way, the following exception occurs in deserialization. How can I solve it?

Exception in thread "main" com.esotericsoftware.kryo.KryoException: java.lang.NullPointerException
Serialization trace:
authRoleDataMap (com.wueasy.base.session.Session)
    at com.esotericsoftware.kryo.serializers.ReflectField.read(ReflectField.java:147)
    at com.esotericsoftware.kryo.serializers.FieldSerializer.read(FieldSerializer.java:124)
    at com.esotericsoftware.kryo.Kryo.readObject(Kryo.java:710)
    at com.wueasy.base.util.SerializeHelper.deserializeKryo(SerializeHelper.java:185)
    at com.wueasy.base.util.SerializeHelper.main(SerializeHelper.java:222)
Caused by: java.lang.NullPointerException
    at java.util.Collections$SetFromMap.add(Collections.java:5468)
    at com.esotericsoftware.kryo.serializers.CollectionSerializer.read(CollectionSerializer.java:243)
    at com.esotericsoftware.kryo.serializers.CollectionSerializer.read(CollectionSerializer.java:44)
    at com.esotericsoftware.kryo.Kryo.readClassAndObject(Kryo.java:813)
    at com.esotericsoftware.kryo.serializers.MapSerializer.read(MapSerializer.java:237)
    at com.esotericsoftware.kryo.serializers.MapSerializer.read(MapSerializer.java:42)
    at com.esotericsoftware.kryo.Kryo.readObject(Kryo.java:732)
    at com.esotericsoftware.kryo.serializers.ReflectField.read(ReflectField.java:125)
    ... 4 more
theigl commented 3 years ago

@fallsea: Can you reproduce this issue in a testcase?

fallsea commented 3 years ago

@theigl

The following code is the test case,Sets.newConcurrentHashSet() Is the implementation of safe collection of guava

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.util.Set;

import org.objenesis.strategy.StdInstantiatorStrategy;

import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import com.google.common.collect.Sets;

public class Test {

    public static void main(String[] args) throws IOException {

        Kryo kryo = new Kryo();
        kryo.setReferences(false);
        kryo.setRegistrationRequired(false);
        kryo.setInstantiatorStrategy(new StdInstantiatorStrategy());

        Demo demo = new Demo();
        Set<String> set = Sets.newConcurrentHashSet();//guava tools
        set.add("demo");
        demo.setSet(set);

        //serialize
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        Output output = new Output(byteArrayOutputStream);
        kryo.writeObject(output, demo);
        output.close();
        byte[] data =  byteArrayOutputStream.toByteArray();
        byteArrayOutputStream.close();

        //deserialized
        Input input = new Input(data);
        Demo demo2= kryo.readObject(input, Demo.class);
        System.err.println(demo2.getSet());

    }

}

class Demo implements Serializable{

    private static final long serialVersionUID = 4196944830626095055L;

    private Set<String> set;

    public Set<String> getSet() {
        return set;
    }

    public void setSet(Set<String> set) {
        this.set = set;
    }

}
theigl commented 3 years ago

@fallsea: Thanks for the testcase!

The problem is that Kryo doesn't have a built-in serializer for Collections.newSetFromMap(). Implementing such a serializer is not trivial because the map underlying the set cannot be accessed without reflection and the concrete implementation may be JDK dependent.

See #221 and #646.

As a workaround you can fall back to the JavaSerializer for this class:

kryo.register(Collections.newSetFromMap(new HashMap<>()).getClass(), new JavaSerializer());
theigl commented 3 years ago

Alternatively, you can register a custom serializer for the class. Something like this should do the job, but it might not work on all JDKs:

public static class SetFromMapSerializer extends Serializer<Set> {

    private static final Class SOURCE_CLASS;
    private static final Field SOURCE_MAP_FIELD;
    private static final Field SOURCE_SET_FIELD;

    static {
        try {
            SOURCE_CLASS = Class.forName("java.util.Collections$SetFromMap");

            SOURCE_MAP_FIELD = SOURCE_CLASS.getDeclaredField("m");
            SOURCE_MAP_FIELD.setAccessible(true);

            SOURCE_SET_FIELD = SOURCE_CLASS.getDeclaredField("s");
            SOURCE_SET_FIELD.setAccessible(true);
        } catch (ClassNotFoundException | IllegalAccessError | NoSuchFieldException e) {
            throw new KryoException("Could not access java.util.Collections$SetFromMap.", e);
        }
    }

    public void write (Kryo kryo, Output output, Set object) {
        try {
            kryo.writeClassAndObject(output, SOURCE_MAP_FIELD.get(object));
        } catch (IllegalAccessException e) {
            throw new KryoException("Could not access source" +
                    " map field in java.util.Collections$SetFromMap.", e);
        }
    }

    public Set read (Kryo kryo, Input input, Class<? extends Set> type) {
        final Map map = (Map)kryo.readClassAndObject(input);
        final Set o = (Set)kryo.newInstance(SOURCE_CLASS);
        try {
            SOURCE_MAP_FIELD.set(o, map);
            SOURCE_SET_FIELD.set(o, map.keySet());
        } catch (IllegalAccessException e) {
            throw new KryoException("Could not access source" +
                " fields in java.util.Collections$SetFromMap.", e);
        }
        return o;
    }
}