今天在使用spring boot+spring-data-redis 做緩存,在獲取緩存的時候出現了:java.lang.ClassCastException: com.xxx.www.xxx.entity.Sign cannot be cast to com.xxx.www.xxx.entity.Sign
的異常,記錄一下解決思路以及方案。
首先,出現這個問題,因爲類的全名和路徑都一樣,那麼根據java判斷兩個類是否是同一個類的2個原則:全名和類加載器,所以這個問題肯定是由類加載器不同引起的。所以先來跟一下代碼。
classloader.png
發現我們的class是由RestartClassLoader
加載的,並不是我熟悉的 AppClassLoader
加載的,於是上網搜索了一下,這個 RestartClassLoader 是 spring-boot-starter-devel 的加載器,目的是用於開發的時候,修改了類之後項目的快速重啓和重載,我的項目中確實也有配置了這個。
pom.png
然後繼續跟蹤代碼,來到 CacheAspectSupport
cacheAspectSupport.png
執行到這一步的時候,緩存的內容已經獲取到,現在來看一下這個返回值的類加載器是哪一個,
cacheresult.png
此時,可以看到,返回的值的類加載器爲: AppClassLoader
,這就導致了最後進行類型轉換的時候出現了我們前面提到的異常信息。
解決方案
- 首先肯定是最簡單的方案了嘛,直接移除掉pom裏面的
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<!-- <scope>runtime</scope>-->
<optional>true</optional>
</dependency>
cache 就能正常運行了。
但是,可能是我的強迫症犯了,覺得這樣會影響我的開發體驗,現在這個社會,用戶體驗最重要,我也是個開發的用戶,當然也想要體驗,肯定不能這樣就算了,於是,擼了一下源代碼,找到了第二種解決方案。
- 既然他們是不同的classloader ,那麼,我們能否讓他們的classloader變成一致?答案是當然的了。
接下來我就按照我當時解決的思路去一步一步的跟蹤。
首先我是通過CacheInterceptor
這個類作爲入口,斷點。1.png
中間跳過一些代碼,這些代碼都是可以直接跟着走就行,最終我們到了RedisCache
這個類中來了image.png
下面這一段代碼就是我們獲取並且deserialize爲對象。
@Nullable
protected Object deserializeCacheValue(byte[] value) {
if (isAllowNullValues() && ObjectUtils.nullSafeEquals(value, BINARY_NULL_VALUE)) {
return NullValue.INSTANCE;
}
return cacheConfig.getValueSerializationPair().read(ByteBuffer.wrap(value));
}
在這裏我們發現
image.png
我們的cacheConfig 是通過 AppClassLoader去加載的,所以可想而知,它在進行反序列化的時候,會使用AppClassLoader 去加載我們的業務對象的類,因此也就出現了前面的類型轉換錯誤。
繼續看cacheConfig 跟進去看一看這個類是什麼結構,在RedisCacheConfiguration.java 文件中發現這樣一個方法:
public static RedisCacheConfiguration defaultCacheConfig(@Nullable ClassLoader classLoader) {
DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
registerDefaultConverters(conversionService);
return new RedisCacheConfiguration(Duration.ZERO, true, true, CacheKeyPrefix.simple(),
SerializationPair.fromSerializer(RedisSerializer.string()),
SerializationPair.fromSerializer(RedisSerializer.java(classLoader)), conversionService);
}
可以傳入一個classLoader,那我們就有了解決辦了發,只要在構建RedisCacheConfiguration的時候傳入和線程一樣的ClassLoader不就可以了。
於是把我們的cacheManager的配置改成如下:
@Bean
public RedisCacheManager redisCacheManager(LettuceConnectionFactory lettuceConnectionFactory){
return RedisCacheManager.builder(lettuceConnectionFactory)
.cacheDefaults(RedisCacheConfiguration.defaultCacheConfig(Thread.currentThread().getContextClassLoader()))
.build();
}
重啓一下,大功告成。
使用FastJson 序列化value的配置:
@Bean
public RedisCacheManager redisCacheManager(LettuceConnectionFactory lettuceConnectionFactory){
RedisSerializationContext.SerializationPair<Object> objectSerializationPair = RedisSerializationContext.SerializationPair.fromSerializer(new GenericFastJsonRedisSerializer());
return RedisCacheManager.builder(lettuceConnectionFactory)
.cacheDefaults(
RedisCacheConfiguration.defaultCacheConfig(Thread.currentThread().getContextClassLoader())
.serializeValuesWith(objectSerializationPair)
)
.build();
}
3人點贊
作者:絕對零
鏈接:https://www.jianshu.com/p/5b98a61afd28
來源:簡書
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。