Spring AI + Redis 聊天记忆反序列化异常解决方案
Spring AI + Redis 聊天记忆反序列化异常解决方案
1. 问题背景与现象
在使用 Spring AI 的 MessageWindowChatMemory 将大模型对话上下文(Chat Memory)持久化到 Redis 时,项目抛出极其冗长的异常日志。
主要错误日志 1(核心异常):
org.springframework.data.redis.serializer.SerializationException: Could not read JSON:Cannot construct instance of org.springframework.ai.chat.messages.UserMessage (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)
主要错误日志 2(连锁反应):
org.springframework.http.converter.HttpMessageNotWritableException: No converter for [class top.justq.blogadmin.common.Result] with preset Content-Type 'text/plain'
2. 根因分析
2.1 为什么无法反序列化?
在使用 GenericJackson2JsonRedisSerializer 将对象存入 Redis 时,序列化(对象转 JSON)过程非常顺利。但当尝试从 Redis 读取历史对话记录时,Jackson 抛出了异常。
这是因为 Spring AI 的消息体实体类(如 UserMessage、AssistantMessage、SystemMessage)在设计上为了保证对象的不可变性,没有提供公开的无参构造函数,也没有标注 Jackson 所需的 @JsonCreator 注解。
默认配置下的 Jackson 在反序列化时,遵循“先调用无参构造创建空壳对象,再通过 Setter 赋值”的逻辑。面对没有无参构造且参数名称无法直接推断的第三方源码类,Jackson 无法实例化对象,从而导致服务崩溃。
2.2 为什么产生二次报错?
当上述的反序列化异常发生后,全局异常处理器(GlobalExceptionHandler)捕获了该异常,并尝试返回自定义的统一响应对象 Result。由于当前 AI 聊天接口可能使用的是流式响应(SSE)或被推断为纯文本类型(text/plain),Spring MVC 找不到能将 Result 对象转为纯文本的转换器,导致了二次报错,前端无法获取到有效的错误提示。
3. 完整解决方案
要彻底解决该问题,不能依赖原生 JDK 序列化(因为这些类未实现 Serializable 接口),必须通过自定义 Jackson 反序列化器来接管解析过程。
第一步:编写自定义反序列化器
针对 Spring AI 常用的三种消息类型,分别编写反序列化规则。
1. UserMessageDeserializer.java
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import org.springframework.ai.chat.messages.UserMessage;
import java.io.IOException;
public class UserMessageDeserializer extends JsonDeserializer<UserMessage> {
@Override
public UserMessage deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
JsonNode node = p.getCodec().readTree(p);
String text = node.has("text") && !node.get("text").isNull() ? node.get("text").asText() : "";
return new UserMessage(text);
}
}2. AssistantMessageDeserializer.java
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import org.springframework.ai.chat.messages.AssistantMessage;
import java.io.IOException;
public class AssistantMessageDeserializer extends JsonDeserializer<AssistantMessage> {
@Override
public AssistantMessage deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
JsonNode node = p.getCodec().readTree(p);
String text = node.has("text") && !node.get("text").isNull() ? node.get("text").asText() : "";
return new AssistantMessage(text);
}
}3. SystemMessageDeserializer.java
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import org.springframework.ai.chat.messages.SystemMessage;
import java.io.IOException;
public class SystemMessageDeserializer extends JsonDeserializer<SystemMessage> {
@Override
public SystemMessage deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
JsonNode node = p.getCodec().readTree(p);
String text = node.has("text") && !node.get("text").isNull() ? node.get("text").asText() : "";
return new SystemMessage(text);
}
}第二步:配置 RedisTemplate 并注册模块
在 Redis 配置类中,创建自定义的 ObjectMapper,激活多态类型识别,并将上述三个反序列化器注册进去。
RedisConfig.java
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import java.net.UnknownHostException;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
// 1. 创建自定义 ObjectMapper
ObjectMapper objectMapper = new ObjectMapper();
// 2. 激活默认类型识别(必须配置,否则无法反序列化带有多种子类的 List 集合)
objectMapper.activateDefaultTyping(
objectMapper.getPolymorphicTypeValidator(),
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.PROPERTY
);
// 3. 注册自定义的 Spring AI 消息反序列化模块
SimpleModule aiMessageModule = new SimpleModule();
aiMessageModule.addDeserializer(UserMessage.class, new UserMessageDeserializer());
aiMessageModule.addDeserializer(AssistantMessage.class, new AssistantMessageDeserializer());
aiMessageModule.addDeserializer(SystemMessage.class, new SystemMessageDeserializer());
objectMapper.registerModule(aiMessageModule);
// 4. 将调教好的 ObjectMapper 传入序列化器
GenericJackson2JsonRedisSerializer jsonRedisSerializer =
new GenericJackson2JsonRedisSerializer(objectMapper);
// 5. 设置 Key 与 Value 的序列化方式
template.setKeySerializer(RedisSerializer.string());
template.setHashKeySerializer(RedisSerializer.string());
template.setValueSerializer(jsonRedisSerializer);
template.setHashValueSerializer(jsonRedisSerializer);
return template;
}
}