Spring @Cacheable为何不将注解方法返回类型传递给反序列化器?
这确实是个很有意思的问题——从你给出的Redis缓存配置和代码来看,GenericJackson2JsonRedisSerializer处理Kotlin类时需要依赖缓存条目中的@class类型信息,但如果Spring能把@Cacheable注解方法的返回类型传递给反序列化器,确实能带来减少内存占用和简化反序列化逻辑的好处。先结合你的代码场景再明确下问题:
代码场景复现
1. Redis缓存配置(@Configuration)
@Bean("cacheManager1hour") fun cacheManager1hour(@Qualifier("cacheConfig") cacheConfiguration: RedisCacheConfiguration, redisConnectionFactory: RedisConnectionFactory): CacheManager { cacheConfiguration.entryTtl(Duration.ofSeconds(60 * 60)) return RedisCacheManager.builder(redisConnectionFactory) .cacheDefaults(cacheConfiguration) .build() } @Bean("cacheConfig") fun cacheConfig(objectMapper:ObjectMapper): RedisCacheConfiguration { return RedisCacheConfiguration.defaultCacheConfig() .computePrefixWith { cacheName -> "yaya:$cacheName:" } .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(GenericJackson2JsonRedisSerializer())) }
2. 缓存使用示例(@RestController)
@Cacheable(value = "book", key = "#root.methodName", cacheManager = "cacheManager1hour") fun getBook(): Book { return Book() } class Book { var asdasd:String? = "TEST" var expires_in = 123 }
3. 理论上的优化点(Spring源码片段)
你提到的Spring缓存拦截器和RedisCache的代码片段,确实显示出有机会在缓存读取环节传递方法返回类型:
// org.springframework.cache.interceptor.CacheAspectSupport @Nullable private Cache.ValueWrapper findInCaches(CacheOperationContext context, Object key) { for (Cache cache : context.getCaches()) { // --> 理论上可以将context.method.returnType传递给doGet Cache.ValueWrapper wrapper = doGet(cache, key); if (wrapper != null) { if (logger.isTraceEnabled()) { logger.trace("Cache entry for key '" + key + "' found in cache '" + cache.getName() + "'"); } return wrapper; } } return null; } // org.springframework.data.redis.cache.RedisCache @Override protected Object lookup(Object key) { // --> 如果能拿到反序列化目标类型,就可以直接传给Jackson byte[] value = cacheWriter.get(name, createAndConvertCacheKey(key)); if (value == null) { return null; } return deserializeCacheValue(value); }
核心原因分析
从Spring缓存抽象的整体设计角度来看,不传递方法返回类型主要有以下几个关键原因:
1. 缓存抽象的通用性原则
Spring的缓存抽象是与具体缓存实现解耦的,它不仅支持Redis,还兼容Caffeine、Ehcache、Guava Cache等多种缓存方案。如果为Redis专门添加传递返回类型的逻辑,会打破这种通用性,让抽象层和具体实现产生强耦合,违背了Spring“抽象统一、实现多样”的设计理念。
2. 缓存条目的复用性要求
缓存的核心价值之一是复用——同一个缓存key对应的value可能被多个不同返回类型的方法读取。比如,假设"book"缓存中的同一个key,既被返回Book的方法使用,也被返回BookDTO的方法读取。如果硬传递方法返回类型,会导致缓存复用失败,甚至出现反序列化类型不匹配的错误。
3. 动态类型与泛型场景的兼容性
在实际业务中,方法的返回类型可能是父类、接口或者泛型类型,而实际存储的是子类或具体泛型实例。比如:
@Cacheable(value = "item") fun getItem(): Item { // 父类 return BookItem() // 子类实例 }
如果仅传递编译时的Item类型给反序列化器,会导致子类特有的字段丢失或反序列化失败。Spring选择让序列化器携带@class类型信息,正是为了兼容这类动态类型场景。
4. 现有序列化逻辑的兼容性
Spring缓存抽象的默认设计是让序列化器自身处理类型信息(比如GenericJackson2JsonRedisSerializer的@class字段),这种逻辑已经被广泛使用。如果改为传递方法返回类型,会打破现有序列化器的工作机制,需要大量修改适配,同时也会导致老的缓存条目无法正常反序列化,带来兼容性问题。
5. 上下文信息的获取与存储成本
要在缓存读取环节拿到方法返回类型,要么在写入缓存时就把类型信息额外存储(增加缓存体积),要么在读取时传递整个缓存操作上下文(增加抽象层复杂度)。这两种方案都会牺牲Spring缓存抽象的“轻量、简洁”特性,不符合其设计初衷。
内容的提问来源于stack exchange,提问作者Sqluo

