引言

在使用 Spring AI Alibaba 构建对话系统时,你是否遇到过这样的错误?

java.lang.ClassCastException: class java.util.LinkedHashMap cannot be cast to
class org.springframework.ai.chat.messages.AbstractMessage

第一次调用正常,第二次调用就崩溃?明明序列化和反序列化用的是同一个 ObjectMapper,为什么还会出现类型转换问题?

本文将深入分析,带你一步步揭开这个"诡异"问题的真相。


一、问题现场

用户的配置

一位用户在使用 RedisSaver 进行对话历史持久化时,按照最佳实践配置了自定义的 ObjectMapper

// 1. 创建自定义 ObjectMapper
@Configuration
public class RedisConfig {

    @Bean
    public ObjectMapper customObjectMapper() {
        return Jackson2ObjectMapperBuilder.json()
            .modules(new JavaTimeModule())
            .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            .serializationInclusion(JsonInclude.Include.NON_NULL)
            .featuresToDisable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
            .build();
    }

    // 2. 使用自定义 ObjectMapper 配置 RedissonClient
    @Bean
    public RedissonClient redissonClient(ObjectMapper customObjectMapper) {
        Config config = new Config();
        config.setCodec(new JsonJacksonCodec(customObjectMapper));
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        return Redisson.create(config);
    }

    // 3. 创建 RedisSaver
    @Bean
    public RedisSaver redisSaver(RedissonClient redissonClient,
                                  ObjectMapper customObjectMapper) {
        SpringAIJacksonStateSerializer serializer =
            new SpringAIJacksonStateSerializer(OverAllState::new, customObjectMapper);

        return RedisSaver.builder()
            .redisson(redissonClient)
            .stateSerializer(serializer)
            .build();
    }
}

二、问题复现

// 第一次调用 - 正常
ReactAgent agent = ReactAgent.builder()
    .model(chatModel)
    .saver(redisSaver)
    .build();

RunnableConfig config = RunnableConfig.builder()
    .threadId("user-conversation-001")
    .build();

// ✅ 第一次成功
agent.stream(new UserMessage("1+1等于几?"), config).subscribe();

// ❌ 第二次失败!
agent.stream(new UserMessage("那2+2等于几?"), config).subscribe();

错误堆栈

java.lang.ClassCastException: class java.util.LinkedHashMap cannot be cast to
class org.springframework.ai.chat.messages.AbstractMessage
    at com.alibaba.cloud.ai.graph.agent.node.AgentLlmNode.lambda$buildNodeAction$0
    (AgentLlmNode.java:149)

症状总结

  • ✅ 第一次调用正常
  • ❌ 第二次调用抛出 ClassCastException
  • 🤔 错误信息:LinkedHashMap 无法转换为 AbstractMessage

三、完整的代码链路追踪

要理解这个问题,我们需要追踪完整的序列化/反序列化链路。

第一次调用:保存到 Redis

┌─────────────────────────────────────────────────────────────────┐
│ 第一次调用:ReactAgent.stream("1+1等于几?")                      │
└─────────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────────┐
│ Step 1: 应用层处理                                                │
│ - AgentLlmNode 调用 LLM                                          │
│ - 生成 AssistantMessage                                          │
│ - state.data = {                                                 │
│     "messages": [UserMessage, AssistantMessage]                  │
│   }                                                              │
└─────────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────────┐
│ Step 2: 【序列化层 1】SpringAIJacksonStateSerializer              │
│ 文件:JacksonStateSerializer.java:95-101                        │
│                                                                  │
│ public void writeData(Map<String, Object> data, ObjectOutput out) {│
│     String json = objectMapper.writeValueAsString(data);         │
│     Serializer.writeUTF(json, out);                              │
│ }                                                                │
│                                                                  │
│ 使用 customObjectMapper 序列化                                   │
│ ⚠️ 注意:虽然传入的是 customObjectMapper,但 Spring AI 在       │
│    构造函数中给它配置了 enableDefaultTyping()                     │
└─────────────────────────────────────────────────────────────────┘
                            ↓
                    生成的 JSON:
        ┌─────────────────────────────────────┐
        │ {                                    │
        │   "messages": [                      │
        │     {                                │
        │       "@class": "...UserMessage",   │← 有类型信息!
        │       "content": "1+1等于几?"       │
        │     },                               │
        │     {                                │
        │       "@class": "...AssistantMessage",│
        │       "content": "等于2"             │
        │     }                                │
        │   ]                                  │
        │ }                                    │
        └─────────────────────────────────────┘
                            ↓
                    ⚠️ 但是注意!
        数组本身没有 @class 字段(被过滤了)
        只有数组元素有 @class 字段
                            ↓
┌─────────────────────────────────────────────────────────────────┐
│ Step 3: 【序列化层 2】Java 对象序列化                              │
│ 文件:RedisSaver.java:91-102                                    │
│                                                                  │
│ private String serializeCheckpoints(List<Checkpoint> checkpoints) {│
│     ObjectOutputStream oos = ...;                                │
│     checkpointSerializer.write(checkpoint, oos);                 │
│     return Base64.getEncoder().encodeToString(bytes);            │
│ }                                                                │
│                                                                  │
│ 将 Checkpoint 对象(包含 JSON 字符串)序列化为字节数组            │
└─────────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────────┐
│ Step 4: Base64 编码                                              │
│ 字节数组 → Base64 字符串                                         │
└─────────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────────┐
│ Step 5: 存储到 Redis                                             │
│ 文件:RedisSaver.java:289                                        │
│                                                                  │
│ RBucket<String> bucket = redisson.getBucket(key);                │
│ bucket.set(base64String);  // ← 只存储字符串!                   │
│                                                                  │
│ ⚠️ 关键:RBucket<String> 意味着 Redis 只做字符串存储              │
│    不会使用 RedissonClient 的 JsonJacksonCodec!                 │
└─────────────────────────────────────────────────────────────────┘
                            ↓
                    ✅ 第一次调用成功

第二次调用:从 Redis 加载

┌─────────────────────────────────────────────────────────────────┐
│ 第二次调用:ReactAgent.stream("那2+2等于几?")                     │
│ 需要加载之前的对话历史                                             │
└─────────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────────┐
│ Step 1: 从 Redis 读取                                            │
│ 文件:RedisSaver.java:200-201                                    │
│                                                                  │
│ RBucket<String> bucket = redisson.getBucket(key);                │
│ String content = bucket.get();  // ← 只读取字符串                 │
│                                                                  │
│ 返回:Base64 字符串                                               │
│ ✅ Redis 的工作结束,没有反序列化操作                              │
└─────────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────────┐
│ Step 2: Base64 解码                                              │
│ Base64 字符串 → 字节数组                                         │
└─────────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────────┐
│ Step 3: 【反序列化层 1】Java 对象反序列化                          │
│ 文件:RedisSaver.java:104-118                                    │
│                                                                  │
│ ObjectInputStream ois = new ObjectInputStream(bais);             │
│ checkpoint = checkpointSerializer.read(ois);                     │
│                                                                  │
│ 返回:Checkpoint 对象(包含 JSON 字符串)                         │
│ ✅ 这一步正常                                                     │
└─────────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────────┐
│ Step 4: 【反序列化层 2】JSON 反序列化                              │
│ 文件:JacksonStateSerializer.java:104-108                       │
│                                                                  │
│ public Map<String, Object> readData(ObjectInput in) {            │
│     String json = Serializer.readUTF(in);                        │
│     return objectMapper.readValue(json,                          │
│         new TypeReference<Map<String, Object>>() {});            │
│ }                                                                │
│                                                                  │
│ 🔥 问题发生在这里!                                               │
└─────────────────────────────────────────────────────────────────┘
                            ↓
              Jackson 的反序列化决策过程:
        ┌─────────────────────────────────────┐
        │ 输入 JSON:                           │
        │ {                                    │
        │   "messages": [                      │← 数组本身没有 @class
        │     {"@class": "UserMessage", ...},  │← 元素有 @class
        │     {"@class": "AssistantMessage", ...}│
        │   ]                                  │
        │ }                                    │
        │                                      │
        │ 目标类型:Map<String, Object>         │
        │           ↑              ↑           │
        │           键类型        值是 Object   │
        └─────────────────────────────────────┘
                            ↓
        ┌─────────────────────────────────────┐
        │ Jackson 的推理:                     │
        │                                      │
        │ 1. "messages" 的值是一个 JSON 数组    │
        │ 2. 数组本身没有类型信息(@class)      │
        │ 3. 目标类型声明为 Object(太宽泛)     │
        │ 4. 虽然数组元素有 @class...          │
        │ 5. 但是!因为:                       │
        │    - 父容器(数组)无类型信息          │
        │    - 目标类型是 Object                │
        │    - Jackson 无法确定如何处理         │
        │                                      │
        │ 🔥 决定:使用 LinkedHashMap           │
        │    表示 JSON 对象                     │
        └─────────────────────────────────────┘
                            ↓
                    反序列化结果:
        ┌─────────────────────────────────────┐
        │ Map<String, Object> state = {         │
        │   "messages": [                       │
        │     LinkedHashMap {                   │← ❌ 不是 UserMessage!
        │       "content": "1+1等于几?"        │
        │     },                                │
        │     LinkedHashMap {                   │← ❌ 不是 AssistantMessage!
        │       "content": "等于2"              │
        │     }                                 │
        │   ]                                   │
        │ }                                     │
        └─────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────────┐
│ Step 5: 应用层使用数据                                            │
│ 文件:AgentLlmNode.java:149                                      │
│                                                                  │
│ if (state.value("messages").isEmpty()) {                         │
│     // ...                                                       │
│ } else {                                                         │
│     // 🔥 类型转换发生在这里                                      │
│     messages = (List<Message>) state.value("messages").get();    │
│     //          ↑              ↑                                 │
│     //     期望的类型        实际是 ArrayList<LinkedHashMap>      │
│ }                                                                │
└─────────────────────────────────────────────────────────────────┘
                            ↓
        ┌─────────────────────────────────────┐
        │ 为什么强制转换能通过编译?            │
        │                                      │
        │ Object obj = state.value("messages").get();│
        │ // 实际类型:ArrayList<LinkedHashMap>│
        │                                      │
        │ List<Message> messages = (List<Message>) obj;│
        │ // Java 泛型擦除:只检查 List        │
        │ // ✅ 编译通过!运行时也不会立即报错  │
        │                                      │
        │ Message firstMsg = messages.get(0);  │
        │ // 返回 LinkedHashMap                │
        │ // 尝试当作 Message 使用...          │
        │                                      │
        │ 🔥 ClassCastException!              │
        └─────────────────────────────────────┘

四、问题根源深度分析

为什么同一个 ObjectMapper 会产生不同结果?

这是最让人困惑的地方:序列化和反序列化都用同一个 ObjectMapper,为什么还会出问题?

原因1:useForType() 过滤了集合类型

当用户传入 customObjectMapper 时,SpringAIJacksonStateSerializer 的构造函数会修改它:

// SpringAIJacksonStateSerializer.java:66-97
public SpringAIJacksonStateSerializer(
    AgentStateFactory<OverAllState> stateFactory,
    ObjectMapper objectMapper) {

    super(stateFactory, objectMapper);

    // 配置类型解析器
    ObjectMapper.DefaultTypeResolverBuilder typeResolver =
        new ObjectMapper.DefaultTypeResolverBuilder(...) {

        @Override
        public boolean useForType(JavaType t) {
            // 🔥 关键:过滤掉集合类型!
            if (t.isCollectionLikeType() ||
                t.isTypeOrSubTypeOf(Collection.class)) {
                return false;  // ← List 不会有 @class 字段
            }

            // 过滤掉 Map 类型
            if (t.isTypeOrSubTypeOf(Map.class) || t.isMapLikeType()) {
                return false;
            }

            return super.useForType(t);
        }
    };

    objectMapper.setDefaultTyping(typeResolver);
}

结果

  • ✅ 普通对象(如 UserMessage)会有 @class 字段
  • ❌ 集合类型(如 List)不会有 @class 字段
  • ❌ Map 类型不会有 @class 字段
原因2:TypeReference<Map<String, Object>> 的影响

反序列化时使用的目标类型:

// JacksonStateSerializer.java:106
return objectMapper.readValue(json,
    new TypeReference<Map<String, Object>>() {});
    //                          ↑
    //                     值类型是 Object

Jackson 的推理过程

  1. 看到 JSON 数组:[{...}, {...}]
  2. 数组本身没有 @class 字段(被 useForType 过滤了)
  3. 目标类型是 Object(太宽泛,可以是任何类型)
  4. 虽然数组元素有 @class 字段
  5. 但因为父容器无类型信息 + 目标类型宽泛
  6. Jackson 不确定如何处理
  7. 默认使用 LinkedHashMap 表示 JSON 对象
原因3:序列化和反序列化的不对称性
序列化时:
    List<Message> → JSON 数组
    元素有 @class:[{"@class":"UserMessage",...}]
    ✅ 看起来正常

反序列化时:
    JSON 数组 → TypeReference<Map<String, Object>>
    目标类型是 Object
    数组本身无类型信息
    Jackson 无法推断出应该是 List<Message>
    ❌ 使用 LinkedHashMap 作为后备方案

这是用户的问题还是框架的问题?

🤔 争议点分析

观点A:用户配置不当(用户的问题)

理由

  1. 用户不应该传入自定义的 ObjectMapper
  2. SpringAIJacksonStateSerializer 已经提供了默认配置
  3. 如果使用默认配置,问题不会发生

代码示例

// ❌ 错误做法
SpringAIJacksonStateSerializer serializer =
    new SpringAIJacksonStateSerializer(OverAllState::new, customObjectMapper);

// ✅ 正确做法
SpringAIJacksonStateSerializer serializer =
    new SpringAIJacksonStateSerializer(OverAllState::new);
    // 使用默认 ObjectMapper
观点B:框架设计缺陷(框架的问题)

理由

  1. API 设计误导:既然提供了接受 ObjectMapper 的构造函数,就应该正确工作
  2. 文档不足:没有明确说明传入自定义 ObjectMapper 的风险
  3. useForType 过滤策略有问题:过滤掉集合类型导致信息丢失
  4. 反序列化策略脆弱:依赖 TypeReference<Map<String, Object>> 过于宽泛

✅ 结论:这是一个框架设计问题

虽然用户可以通过不传入自定义 ObjectMapper 来避免,但本质上是框架的问题:

1. API 契约被违反
// 框架提供了这个构造函数
public SpringAIJacksonStateSerializer(
    AgentStateFactory<OverAllState> stateFactory,
    ObjectMapper objectMapper) {
    // ...
}

用户合理期望

  • ✅ 传入的 ObjectMapper 应该被正确使用
  • ✅ 序列化和反序列化应该是对称的
  • ✅ 不应该产生类型转换异常

实际情况

  • ❌ 传入的 ObjectMapper 被框架修改
  • ❌ 修改后的配置导致反序列化失败
  • ❌ 没有任何警告或文档说明
2. useForType 的设计缺陷

为什么要过滤掉集合类型?框架开发者的考虑可能是:

  • 避免 JSON 过于臃肿(集合的 @class 信息很长)
  • 认为集合的元素有类型信息就够了

但这导致了:

  • ❌ 反序列化时缺少关键信息
  • ❌ Jackson 无法正确重建对象图
  • ❌ 必须使用精确的 TypeReference
3. 缺少防御性编程

框架应该:

  1. 验证配置:检测用户传入的 ObjectMapper 是否兼容
  2. 明确文档:说明哪些配置是必需的
  3. 提供诊断:在反序列化失败时给出有用的错误信息
  4. 提供辅助方法:让用户可以安全地自定义配置

五、解决方案

方案1:用户侧 - 不传入自定义 ObjectMapper(临时方案)

@Bean
public RedisSaver redisSaver(RedissonClient redissonClient) {
    // ✅ 使用默认配置
    SpringAIJacksonStateSerializer serializer =
        new SpringAIJacksonStateSerializer(OverAllState::new);

    return RedisSaver.builder()
        .redisson(redissonClient)
        .stateSerializer(serializer)
        .build();
}

优点

  • ✅ 立即解决问题
  • ✅ 不需要等待框架更新

缺点

  • ❌ 失去了自定义配置的能力
  • ❌ 不能添加自定义的 Jackson 模块

方案2:用户侧 - 正确配置自定义 ObjectMapper(推荐)

如果确实需要自定义配置,可以这样做:

@Bean
public RedisSaver redisSaver(RedissonClient redissonClient) {
    // 1. 先创建默认的 serializer
    SpringAIJacksonStateSerializer serializer =
        new SpringAIJacksonStateSerializer(OverAllState::new);

    // 2. 获取它内部已经配置好的 ObjectMapper
    ObjectMapper mapper = ((JacksonStateSerializer) serializer).objectMapper();

    // 3. 在此基础上添加自定义模块
    mapper.registerModule(new JavaTimeModule());
    mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);

    return RedisSaver.builder()
        .redisson(redissonClient)
        .stateSerializer(serializer)
        .build();
}

优点

  • ✅ 保留了类型信息处理
  • ✅ 可以添加自定义配置
  • ✅ 不会出现 LinkedHashMap 问题

方案3:框架侧 - 改进 useForType 策略

框架应该修改 useForType 的过滤逻辑:

@Override
public boolean useForType(JavaType t) {
    // ❌ 当前实现:过滤掉所有集合
    if (t.isCollectionLikeType()) {
        return false;
    }

    // ✅ 改进方案:保留包含消息的集合的类型信息
    if (t.isCollectionLikeType()) {
        // 检查集合的元素类型
        JavaType contentType = t.getContentType();
        if (contentType != null &&
            contentType.isTypeOrSubTypeOf(Message.class)) {
            return true;  // ← 为消息集合保留类型信息
        }
        return false;
    }

    return super.useForType(t);
}

方案4:框架侧 - 使用更精确的 TypeReference

反序列化时使用更精确的类型:

// ❌ 当前实现
public Map<String, Object> readData(ObjectInput in) {
    String json = Serializer.readUTF(in);
    return objectMapper.readValue(json,
        new TypeReference<Map<String, Object>>() {});
        //                          ↑ 太宽泛
}

// ✅ 改进方案:根据 JSON 内容动态选择类型
public Map<String, Object> readData(ObjectInput in) {
    String json = Serializer.readUTF(in);

    // 如果 JSON 包含 messages 字段,使用更精确的类型
    JsonNode root = objectMapper.readTree(json);
    if (root.has("messages")) {
        // 使用支持消息类型的自定义反序列化器
        return objectMapper.readValue(json, MESSAGE_AWARE_TYPE_REF);
    }

    return objectMapper.readValue(json, DEFAULT_TYPE_REF);
}

方案5:框架侧 - 提供配置验证

在构造函数中验证传入的 ObjectMapper

public SpringAIJacksonStateSerializer(
    AgentStateFactory<OverAllState> stateFactory,
    ObjectMapper objectMapper) {

    super(stateFactory, objectMapper);

    // ✅ 验证 ObjectMapper 是否正确配置
    if (!isObjectMapperCompatible(objectMapper)) {
        throw new IllegalArgumentException(
            "The provided ObjectMapper is not compatible with " +
            "SpringAIJacksonStateSerializer. Please use the default " +
            "constructor or ensure your ObjectMapper has proper type " +
            "handling configured.");
    }

    // ... 继续配置
}

private boolean isObjectMapperCompatible(ObjectMapper mapper) {
    // 测试序列化/反序列化一个包含消息的状态
    try {
        Map<String, Object> testState = Map.of(
            "messages", List.of(new UserMessage("test"))
        );
        String json = mapper.writeValueAsString(testState);
        Map<String, Object> result = mapper.readValue(json,
            new TypeReference<Map<String, Object>>() {});

        Object messages = result.get("messages");
        if (messages instanceof List) {
            List<?> list = (List<?>) messages;
            if (!list.isEmpty() && list.get(0) instanceof Message) {
                return true;  // ✅ 配置正确
            }
        }
    } catch (Exception e) {
        // 序列化失败
    }
    return false;  // ❌ 配置不兼容
}

测试验证

我在测试代码中成功复现了这个问题:

@Test
void testDirectRedisDeserialization_WithCustomObjectMapper() {
    System.out.println("演示 LinkedHashMap 问题");

    // 创建自定义 ObjectMapper(模拟用户配置)
    ObjectMapper customMapper = Jackson2ObjectMapperBuilder.json()
        .modules(new JavaTimeModule())
        .build();

    // 使用自定义 RedissonClient
    Config config = new Config();
    config.setCodec(new JsonJacksonCodec(customMapper));
    RedissonClient customClient = Redisson.create(config);

    // 存储消息
    List<Object> messages = new ArrayList<>();
    Map<String, Object> userMsg = new LinkedHashMap<>();
    userMsg.put("content", "User question");
    userMsg.put("messageType", "USER");
    messages.add(userMsg);

    RBucket<Object> bucket = customClient.getBucket("test-messages");
    bucket.set(messages);
    System.out.println("✅ Stored to Redis");

    // 读取消息
    Object retrieved = bucket.get();
    System.out.println("Retrieved type: " + retrieved.getClass());

    if (retrieved instanceof List) {
        List<?> list = (List<?>) retrieved;
        Object first = list.get(0);
        System.out.println("First element type: " + first.getClass().getName());

        if (first instanceof LinkedHashMap) {
            System.out.println("✅ Successfully reproduced issue #3980!");
            System.out.println("   Messages were deserialized as LinkedHashMap");

            // 尝试类型转换
            try {
                AbstractMessage msg = (AbstractMessage) first;
                fail("Should have thrown ClassCastException");
            } catch (ClassCastException e) {
                System.out.println("❌ ClassCastException occurred:");
                System.out.println("   " + e.getMessage());
            }
        }
    }

    bucket.delete();
}

输出

演示 LinkedHashMap 问题
✅ Stored to Redis
Retrieved type: class java.util.ArrayList
First element type: java.util.LinkedHashMap
✅ Successfully reproduced issue #3980!
   Messages were deserialized as LinkedHashMap
❌ ClassCastException occurred:
   class java.util.LinkedHashMap cannot be cast to
   class org.springframework.ai.chat.messages.AbstractMessage

六、经验总结

1. 序列化框架的复杂性

这个问题揭示了序列化框架的几个微妙之处:

  • 类型擦除:Java 泛型在运行时被擦除,导致类型信息丢失
  • 多态序列化:需要显式的类型标记(如 @class
  • 配置依赖:序列化和反序列化必须使用兼容的配置
  • 默认行为:Jackson 在缺少类型信息时的后备策略

2. 框架设计原则

这个案例也展示了好的框架应该遵循的原则:

✅ 应该做的:
  1. 明确的 API 契约

    • 如果提供了参数,就应该正确处理
    • 不应该暗中修改用户的配置
  2. 防御性编程

    • 验证输入参数
    • 提供有用的错误信息
    • 在问题发生前就捕获
  3. 完善的文档

    • 说明哪些配置是必需的
    • 警告潜在的陷阱
    • 提供最佳实践示例
  4. 向后兼容

    • 不破坏现有代码
    • 提供迁移路径
    • 保持 API 稳定
❌ 不应该做的:
  1. 暗中修改用户配置

    • 用户传入的对象被框架修改
    • 没有明确说明
  2. 依赖隐式约定

    • 期望用户"知道"正确的配置
    • 没有验证机制
  3. 宽泛的类型定义

    • TypeReference<Map<String, Object>>
    • 丢失了重要的类型信息

3. 对开发者的建议

如果你正在使用 Spring AI Alibaba:

  1. 优先使用默认配置

    // ✅ 推荐
    new SpringAIJacksonStateSerializer(OverAllState::new);
    
    // ⚠️ 谨慎使用
    new SpringAIJacksonStateSerializer(OverAllState::new, customMapper);
    
  2. 需要自定义时,基于默认配置扩展

    SpringAIJacksonStateSerializer serializer =
        new SpringAIJacksonStateSerializer(OverAllState::new);
    ObjectMapper mapper = serializer.objectMapper();
    mapper.registerModule(myCustomModule);
    
  3. 测试序列化/反序列化

    @Test
    void testSerializationRoundTrip() {
        Map<String, Object> original = ...;
        String json = mapper.writeValueAsString(original);
        Map<String, Object> deserialized =
            mapper.readValue(json, typeRef);
    
        // 验证类型正确
        Object messages = deserialized.get("messages");
        assertTrue(messages instanceof List);
        List<?> list = (List<?>) messages;
        assertTrue(list.get(0) instanceof Message);
    }
    

4. 对框架开发者的建议

如果你正在开发类似的框架:

  1. API 设计

    • 不提供可能出错的选项
    • 或者提供但要严格验证
    • 考虑使用 Builder 模式限制配置
  2. 错误处理

    • 在问题发生时提供清晰的错误信息
    • 说明问题原因和解决方法
    • 考虑添加诊断工具
  3. 文档

    • 说明每个参数的含义和约束
    • 提供正反示例
    • 解释底层机制
  4. 测试

    • 覆盖各种配置组合
    • 测试边缘情况
    • 包含反例测试(应该失败的情况)

七、结语

本文介绍了一个经典的序列化框架问题,它揭示了:

  1. 问题本质useForType 过滤掉集合类型 + TypeReference<Map<String, Object>> 过于宽泛 = LinkedHashMap
  2. 责任归属:虽然用户可以避免,但本质上是框架设计问题
  3. 解决方案:用户侧使用默认配置,框架侧改进 API 设计

希望这篇文章能帮助你理解这个问题的来龙去脉。如果你在使用 Spring AI Alibaba 时遇到类似问题,现在你知道该怎么办了!


附录:关键代码位置索引

组件 文件 行号 说明
序列化 JacksonStateSerializer.java 95-101 writeData()
反序列化 JacksonStateSerializer.java 104-108 readData()
类型过滤 SpringAIJacksonStateSerializer.java 72-75 useForType()
Redis 存储 RedisSaver.java 289 bucket.set()
Redis 读取 RedisSaver.java 200-201 bucket.get()
类型转换 AgentLlmNode.java 149 强制转换位置
测试验证 RedisSaverIssue3980Test.java 264-360 问题复现
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐