使用Kryo框架序列化Java Collections.synchronizedMap反序列化异常问题
解决Kryo序列化Collections.synchronizedMap时反序列化的NullPointerException问题
你碰到的这个NPE问题其实很典型——Kryo默认的序列化逻辑对java.util.Collections$SynchronizedMap这种包装类的处理不够到位。虽然你已经注册了这个类,但Kryo只是简单实例化了它,却没有初始化它内部那个用来实际存储数据的私有map字段,所以反序列化后的实例本质上是个空壳,调用任何方法都会因为内部map为null抛出异常。
修复方案:自定义序列化器
要解决这个问题,我们需要为Collections$SynchronizedMap写一个自定义的Kryo序列化器,手动处理内部map的读写逻辑:
- 首先创建自定义序列化器类:
import com.esotericsoftware.kryo.Kryo; import com.esotericsoftware.kryo.Serializer; import com.esotericsoftware.kryo.io.Input; import com.esotericsoftware.kryo.io.Output; import java.util.Collections; import java.util.Map; import java.lang.reflect.Field; public class SynchronizedMapSerializer extends Serializer<Map> { @Override public void write(Kryo kryo, Output output, Map map) { try { // 通过反射获取SynchronizedMap内部的私有map字段 Field innerMapField = Collections.SynchronizedMap.class.getDeclaredField("m"); innerMapField.setAccessible(true); Map innerMap = (Map) innerMapField.get(map); // 序列化内部实际存储数据的map kryo.writeClassAndObject(output, innerMap); } catch (NoSuchFieldException | IllegalAccessException e) { throw new RuntimeException("Failed to serialize SynchronizedMap", e); } } @Override public Map read(Kryo kryo, Input input, Class<? extends Map> type) { // 先反序列化内部的map Map innerMap = (Map) kryo.readClassAndObject(input); // 重新包装成线程安全的SynchronizedMap return Collections.synchronizedMap(innerMap); } }
- 修改你的Kryo注册代码,将自定义序列化器与
Collections$SynchronizedMap绑定:
Serializer SERIALIZER = Serializer.using(KryoNamespace.builder() .register(KryoNamespaces.BASIC) // 注册类并关联自定义序列化器 .register(Class.forName("java.util.Collections$SynchronizedMap"), new SynchronizedMapSerializer()) .build()); // 测试代码保持不变 Map<String, String> map = Collections.synchronizedMap(new HashMap<String, String>()); map.put("test", "123"); byte[] encoded = SERIALIZER.encode(map); Map<String, String> decoded = SERIALIZER.decode(encoded); // 现在调用put等方法不会再抛出NPE了 decoded.put("newKey", "newValue");
为什么这个方案有效?
- 序列化阶段:我们通过反射拿到
SynchronizedMap内部的实际存储map,只序列化这个核心数据,避免了Kryo默认逻辑无法处理私有字段的问题。 - 反序列化阶段:我们先恢复内部的普通map,再用
Collections.synchronizedMap()重新包装成线程安全的实例,既保留了原有的线程安全特性,又保证了数据完整初始化。
内容的提问来源于stack exchange,提问作者Alexander Richter




