鱼皮超级智能体文件读写报错
Spring AI Kryo 序列化报错Encountered unregistered class ID 解决方案在开发 Spring AI 聊天记忆功能时采用 Kryo 实现消息的文件持久化存储运行过程中突然报出com.esotericsoftware.kryo.KryoException: Encountered unregistered class ID错误排查许久才发现核心问题特此记录解决方案帮各位开发者避坑。一、报错现象精准复现开发场景基于 Spring AI 实现聊天记忆功能自定义FileBasedChatMemory类使用 Kryo 序列化Message消息包含文本、图片等媒体类型并持久化到本地文件。报错核心信息com.esotericsoftware.kryo.KryoException: Encountered unregistered class ID: 104 Serialization trace: media (org.springframework.ai.chat.messages.UserMessage) at com.esotericsoftware.kryo.util.DefaultClassResolver.readClass(DefaultClassResolver.java:159) at com.esotericsoftware.kryo.Kryo.readClass(Kryo.java:758) at com.esotericsoftware.kryo.serializers.ReflectField.read(ReflectField.java:117) t com.esotericsoftware.kryo.serializers.FieldSerializer.read(FieldSerializer.java:130) t com.esotericsoftware.kryo.Kryo.readObjectOrNull(Kryo.java:854) t com.esotericsoftware.kryo.serializers.CollectionSerializer.read(CollectionSerializer.java:238) com.esotericsoftware.kryo.serializers.CollectionSerializer.read(CollectionSerializer.java:44) com.esotericsoftware.kryo.Kryo.readObject(Kryo.java:777) om.gaokaiyuan.yuaiagent.chatmemory.FileBasedChatMemory.getOrCreateConversation(FileBasedChatMemory.java:67) at c at at a a a补充说明注释掉单参数add方法如下后报错暂时消失但聊天记忆无法正常持久化本质是绕开了自定义的文件存储逻辑。// Override // public void add(String conversationId, Message message) { // saveConversation(conversationId, List.of(message)); // // ChatMemory.super.add(conversationId, message); // } // 注释后不报错但失去文件存储功能二、报错深层原因核心必懂很多开发者会误以为是代码逻辑错误比如消息追加逻辑但实际问题出在Kryo 序列化机制与 Spring AI 消息类型不兼容具体拆解为3点1. 核心矛盾Kryo 不支持 Spring AI Message 的媒体类型Spring AI 的UserMessage类聊天消息的核心类包含media字段用于存储图片、附件等媒体数据。而 Kryo 作为轻量级序列化框架默认不支持 Spring AI 内部封装的媒体类型未注册该类的序列化器导致序列化/反序列化时无法识别类 ID直接抛出异常。简单说Kryo 不认识 Spring AI 消息里的“图片/附件”字段一遇到就炸。2. 注释方法后不报错的“假象”我们自定义的FileBasedChatMemory实现了 Spring AI 的ChatMemory接口该接口有两个add方法// 接口方法1添加单条消息 void add(String conversationId, Message message); // 接口方法2添加多条消息 void add(String conversationId, ListMessage messages);当我们注释掉自定义的单参数add方法后Java 会自动调用接口的默认实现default void add(String conversationId, Message message) { add(conversationId, List.of(message)); }这个默认方法不会调用我们自定义的saveConversationKryo 序列化逻辑相当于“绕开了文件存储”自然不会触发 Kryo 报错但代价是聊天记忆无法持久化到文件重启服务后消息全部丢失。3. 补充误区不是“追加逻辑”的问题排查初期很容易误以为是“消息追加时覆盖了旧文件”但实际测试发现即使只发送一条带图片的消息首次序列化时就会报错——核心问题是“序列化本身失败”和追加逻辑无关。三、两种解决方案按需选择均保留文件存储核心思路要么让 Kryo 支持 Spring AI 媒体类型复杂不推荐要么替换序列化方式简单、稳定推荐。以下提供两种可直接落地的方案均保留文件存储功能。方案一替换序列化方式推荐JSON 序列化放弃 Kryo改用 Jackson 实现 JSON 序列化。JSON 支持所有 Spring AI 消息类型且文件可直接打开查看兼容性强、稳定性高是最推荐的方案。完整可运行代码直接替换原FileBasedChatMemorypackage com.gaokaiyuan.yuaiagent.chatmemory; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.ai.chat.messages.*; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.util.*; public class FileBasedChatMemory implements ChatMemory { private final String BASE_DIR; private final ObjectMapper objectMapper new ObjectMapper(); // 构造方法初始化存储目录自动创建不存在的目录 public FileBasedChatMemory(String dir) { this.BASE_DIR dir; File baseDir new File(dir); if (!baseDir.exists()) baseDir.mkdirs(); } // 单条消息追加正确逻辑支持媒体类型 Override public void add(String conversationId, Message message) { ListMessage messages get(conversationId); messages.add(message); save(conversationId, messages); } // 多条消息追加正确逻辑复用单条追加逻辑 Override public void add(String conversationId, ListMessage messages) { ListMessage existing get(conversationId); existing.addAll(messages); save(conversationId, existing); } // 读取指定对话的所有历史消息反序列化 Override public ListMessage get(String conversationId) { File file getFile(conversationId); if (!file.exists()) return new ArrayList(); try { // 读取JSON文件转成Map列表避免直接序列化Message报错 ListMapString, Object list objectMapper.readValue(file, List.class); ListMessage messages new ArrayList(); // 遍历Map还原成Spring AI Message对象 for (MapString, Object map : list) { MessageType type MessageType.valueOf((String) map.get(messageType)); String text (String) map.get(text); Object media map.get(media); Message msg; // 处理带媒体图片/附件的消息 if (media ! null) { msg new UserMessage(text, List.of(media)); } else { // 处理纯文本消息区分消息类型 msg switch (type) { case USER - new UserMessage(text); case ASSISTANT - new AssistantMessage(text); case SYSTEM - new SystemMessage(text); case TOOL - new ToolMessage(text, (ListString) map.get(toolCalls)); }; } messages.add(msg); } return messages; } catch (Exception e) { e.printStackTrace(); return new ArrayList(); } } // 清空指定对话的历史消息删除文件 Override public void clear(String conversationId) { try { Files.deleteIfExists(getFile(conversationId).toPath()); } catch (IOException e) { e.printStackTrace(); } } // 核心将消息列表序列化为JSON保存到文件 private void save(String conversationId, ListMessage messages) { ListMapString, Object list new ArrayList(); for (Message m : messages) { MapString, Objectgt; map new HashMap(); // 存储消息类型、文本内容 map.put(messageType, m.getMessageType().name()); map.put(text, m.getText()); // 单独处理媒体字段避免序列化失败 if (m instanceof UserMessage userMsg userMsg.getMedia() ! null !userMsg.getMedia().isEmpty()) { map.put(media, userMsg.getMedia().getFirst()); } list.add(map); } try { // 写入JSON文件格式化输出可直接打开查看 objectMapper.writerWithDefaultPrettyPrinter().writeValue(getFile(conversationId), list); } catch (IOException e) { e.printStackTrace(); } } // 获取对话对应的文件以conversationId为文件名后缀为json private File getFile(String conversationId) { return new File(BASE_DIR, conversationId .json); } }方案优势支持文本、图片等所有 Spring AI 消息类型不报错文件为 JSON 格式可直接打开查看、调试消息追加逻辑正确历史记录不会丢失完全兼容 Spring AI 的ChatMemory接口无需修改其他代码。方案二Kryo 注册自定义序列化器不推荐复杂易踩坑如果必须使用 Kryo比如追求极致序列化性能需要为 Spring AI 的UserMessage及其内部媒体类型注册自定义序列化器步骤如下仅作参考不推荐生产使用// 1. 初始化Kryo时注册自定义序列化器 static { kryo.setRegistrationRequired(false); kryo.setInstantiatorStrategy(new StdInstantiatorStrategy()); // 为UserMessage注册自定义序列化器 kryo.register(UserMessage.class, new UserMessageSerializer()); } // 2. 自定义UserMessage序列化器 public class UserMessageSerializer extends SerializerUserMessage { Override public void write(Kryo kryo, Output output, UserMessage object) { // 手动序列化UserMessage的字段text、media等 output.writeString(object.getText()); // 序列化media字段需根据实际类型处理 kryo.writeObject(output, object.getMedia()); } Override public UserMessage read(Kryo kryo, Input input, ClassUserMessage type) { // 手动反序列化字段还原UserMessage对象 String text input.readString(); ListObject media kryo.readObject(input, ArrayList.class); return new UserMessage(text, media); } }方案缺点需要为 Spring AI 所有相关消息类型如AssistantMessage注册序列化器工作量大Spring AI 版本更新后消息类结构可能变化序列化器需要同步修改维护成本高仍可能出现未知的序列化兼容问题稳定性不如 JSON 方案。四、总结与避坑建议1. 核心结论本次报错的本质是Kryo 不兼容 Spring AI 消息的媒体类型与消息追加逻辑无关注释单参数add方法不报错是因为绕开了自定义的文件存储并非真正解决问题。2. 避坑建议开发 Spring AI 聊天记忆功能如需文件持久化优先使用 JSON 序列化Jackson兼容性强、维护成本低避免使用 Kryo 序列化 Spring AI 相关对象除非明确不需要媒体类型仅纯文本消息遇到 Kryo 序列化报错优先排查“是否有未注册的自定义类型/第三方框架类型”而非业务逻辑。3. 最终推荐直接使用方案一JSON 序列化复制代码替换原类无需修改其他业务代码即可实现“文件存储 支持图片消息 不报错”的需求是最稳定、最高效的解决方案。如果在使用过程中遇到 JSON 序列化的细节问题如日期、特殊字段可留言补充后续将持续完善。