在Spring中使用`@Cacheable`注解时,若底层缓存实现(如Redis、Ehcache)采用二进制序列化(如JDK原生序列化或Kryo),而被缓存的返回对象未实现`Serializable`接口,将抛出`NotSerializableException`。典型场景包括:DTO类缺少`implements Serializable`、含非序列化字段(如`ThreadLocal`、`InputStream`)、或使用Lombok但未显式添加`@Serial`或`serialVersionUID`。该问题在本地堆缓存(如ConcurrentMapCache)中不暴露,易在接入分布式缓存后突然出现。根本原因在于序列化机制要求对象及其所有可访问非瞬态成员均需可序列化。解决方案包括:确保实体/DTO实现`Serializable`并定义`serialVersionUID`;配置JSON序列化(如RedisTemplate使用GenericJackson2JsonRedisSerializer)替代JDK序列化;或通过`@Cacheable(key = "...", unless = "#result == null")`配合自定义序列化策略规避。
1条回答 默认 最新
希芙Sif 2026-02-10 13:50关注```html一、现象层:缓存突然失效,抛出 NotSerializableException
当项目从本地开发(使用
ConcurrentMapCacheManager)迁移到生产环境(接入 Redis 或 Ehcache 二进制缓存)后,原本正常运行的@Cacheable方法在首次命中缓存写入时突然抛出:java.io.NotSerializableException: com.example.dto.UserDTO堆栈中高频出现
ObjectOutputStream.writeOrdinaryObject或Kryo.writeClassAndObject—— 这是典型的二进制序列化失败信号。该异常在本地不复现,极具迷惑性。二、机制层:@Cacheable 的序列化契约被底层 CacheManager 隐式继承
Spring 的
@Cacheable本身不执行序列化,但其抽象层Cache接口要求实现类具备「存储任意 Java 对象」能力。当使用如下缓存提供者时,隐式强依赖 JDK 序列化语义:- Redis:默认
JdkSerializationRedisSerializer(RedisTemplate未显式配置 serializer 时) - Ehcache 2.x:
DefaultElement默认启用 Java 序列化 - Kryo-based CacheManager(如某些自定义集成):要求所有类型注册或可反射序列化
根本约束在于:JVM 序列化要求对象及其所有非
transient、非static成员变量所属类型均实现Serializable。三、根因层:三类典型不可序列化场景深度剖析
场景类型 代码示例 为何失败 DTO 未实现 Serializable public class UserDTO { String name; }顶层类无序列化标识,JDK 拒绝序列化入口 含不可序列化字段 private ThreadLocal<String> context;ThreadLocal类未实现Serializable,且其内部状态无法安全跨 JVM 复制Lombok + 缺失 @Serial @Data @AllArgsConstructor public class OrderDTO { ... }Lombok 生成的 toString()/equals()不影响序列化,但若字段含InputStream等,且未用@Transient或transient修饰,则仍失败四、解决方案层:从防御到重构的三级应对策略
- 【基础防御】强制 Serializable 合规:
所有 DTO/VO/Entity 显式声明:
public class UserDTO implements Serializable { private static final long serialVersionUID = 1L; }
并对不可序列化字段添加transient或 Lombok@JsonIgnore/@Transient(需配合 JSON 序列化器) - 【架构升级】切换为 JSON 序列化(推荐):
配置RedisTemplate使用GenericJackson2JsonRedisSerializer:
@Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); template.setDefaultSerializer(new GenericJackson2JsonRedisSerializer()); return template; } - 【弹性兜底】运行时规避 + 自定义缓存逻辑:
结合 SpEL 控制缓存条件,并引入CacheResolver或CacheManager分支逻辑:
@Cacheable(value = "userCache", key = "#id", unless = "#result == null || #result.getClass().getDeclaredFields().anyMatch(f -> !f.getType().isAssignableFrom(Serializable.class))")
五、验证层:构建可落地的序列化健康检查流程
建议在 CI/CD 中嵌入静态检查与运行时断言:
graph TD A[编译期] --> B[Checkstyle / PMD 规则:检测未实现 Serializable 的 POJO] A --> C[Lombok @Serial 注解扫描] D[测试期] --> E[JUnit + AssertJ:new ObjectOutputStream(new ByteArrayOutputStream()).writeObject(dto)] D --> F[集成测试:启动 Redis CacheManager 后调用 @Cacheable 方法]同时,在核心 DTO 上增加单元测试断言:
```
assertThatCode(() -> new ObjectOutputStream(new ByteArrayOutputStream()).writeObject(dto)).doesNotThrowAnyException();本回答被题主选为最佳回答 , 对您是否有帮助呢?解决 无用评论 打赏 举报- Redis:默认