在使用 @Cacheable 註解的時候報了個異常java
java.lang.ClassCastException: org.springframework.cache.interceptor.SimpleKey cannot be cast to java.lang.String
複製代碼
先說明一下,我用的 Springboot 版本是1.X.且CacheManager爲RedisCacheManager. 下面提出的解決方法也是基於這個配置的.redis
若是Springboot2.X或者使用的是CaffeineCacheManager等其餘CacheManager則不會有這個報錯,至於爲何下面分析.spring
Redis配置就不放上來了,百度一下就OK. 直接貼上使用的代碼,很是簡單app
@Cacheable(value = "testCacheable")
public String testCacheable() {
return "testCacheable";
}
複製代碼
至於這個問題, 若是要完全搞明白的話須要理解@Cacheable註解背後實現的原理, 我這裏粗略說一下.ide
首先要使 @Cacheable 註解生效, 咱們須要在啓動類上加上 @EnableCaching 註解函數
咱們來看一下 @EnableCaching 註解作了什麼. 主要是這個註解上加了ui
@Import(CachingConfigurationSelector.class)
複製代碼
接着看 CachingConfigurationSelector 的 selectImports 方法. selectImports簡單字面來理解就是選擇要導入的Bean(即實例化進Spring容器的Bean)this
@Override
public String[] selectImports(AdviceMode adviceMode) {
switch (adviceMode) {
case PROXY:
return getProxyImports();
case ASPECTJ:
return getAspectJImports();
default:
return null;
}
}
private String[] getProxyImports() {
List<String> result = new ArrayList<>(3);
result.add(AutoProxyRegistrar.class.getName());
result.add(ProxyCachingConfiguration.class.getName());
if (jsr107Present && jcacheImplPresent) {
result.add(PROXY_JCACHE_CONFIGURATION_CLASS);
}
return StringUtils.toStringArray(result);
}
複製代碼
由於Spring的代理模式爲PROXY, 因此咱們直接看 getProxyImports 方法.lua
其中咱們能夠看到在 getProxyImports 方法中有一行代碼spa
result.add(ProxyCachingConfiguration.class.getName());
複製代碼
能夠看出 CachingConfigurationSelector 最終初始化了 ProxyCachingConfiguration 這個Bean
@Bean(name = CacheManagementConfigUtils.CACHE_ADVISOR_BEAN_NAME)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public BeanFactoryCacheOperationSourceAdvisor cacheAdvisor() {
BeanFactoryCacheOperationSourceAdvisor advisor = new BeanFactoryCacheOperationSourceAdvisor();
advisor.setCacheOperationSource(cacheOperationSource());
advisor.setAdvice(cacheInterceptor());
if (this.enableCaching != null) {
advisor.setOrder(this.enableCaching.<Integer>getNumber("order"));
}
return advisor;
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public CacheOperationSource cacheOperationSource() {
return new AnnotationCacheOperationSource();
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public CacheInterceptor cacheInterceptor() {
CacheInterceptor interceptor = new CacheInterceptor();
interceptor.configure(this.errorHandler, this.keyGenerator, this.cacheResolver, this.cacheManager);
interceptor.setCacheOperationSource(cacheOperationSource());
return interceptor;
}
複製代碼
咱們能夠看到裏面定義了三個bean,分別是:
BeanFactoryCacheOperationSourceAdvisor (真正的大佬)
CacheOperationSource (保存了全部帶 @Cacheable 註解的Bean信息)
CacheInterceptor (此類繼承自 CacheAspectSupport,是全部@Cacheable 註解的切面基礎類)
複製代碼
可是後兩個Bean都是爲了 BeanFactoryCacheOperationSourceAdvisor 這個Bean服務的.點開此類的源碼能夠發現這個類是繼承於 AbstractBeanFactoryPointcutAdvisor 這個類,看類名就知道這個類是AOP相關的.而咱們的@Cacheable 註解也是基於AOP來實現的.
咱們再看 CacheOperationSource,其實是註冊了 AnnotationCacheOperationSource 這個Bean
private final Set<CacheAnnotationParser> annotationParsers;
public AnnotationCacheOperationSource() {
this(true);
}
public AnnotationCacheOperationSource(boolean publicMethodsOnly) {
this.publicMethodsOnly = publicMethodsOnly;
this.annotationParsers = Collections.singleton(new SpringCacheAnnotationParser());
}
複製代碼
簡單介紹一下,CacheAnnotationParser 是用於解析已知 caching 註解的策略接口,就是 caching 註解會被 CacheAnnotationParser 的具體實現類來處理.咱們能夠看到此處的 CacheAnnotationParser 實際爲它的惟一實現類 SpringCacheAnnotationParser
private static final Set<Class<? extends Annotation>> CACHE_OPERATION_ANNOTATIONS = new LinkedHashSet<>(8);
static {
CACHE_OPERATION_ANNOTATIONS.add(Cacheable.class);
CACHE_OPERATION_ANNOTATIONS.add(CacheEvict.class);
CACHE_OPERATION_ANNOTATIONS.add(CachePut.class);
CACHE_OPERATION_ANNOTATIONS.add(Caching.class);
}
複製代碼
咱們能夠看到咱們熟悉的 Cacheable 註解了是否是...
自此,關於@Cacheable 註解相關的Bean的初始化工做終於完成了.... 是否是要暈了....
最後簡單總結一下:
由於 @EnableCaching 註解加上了 @Import(CachingConfigurationSelector.class)
CachingConfigurationSelector 會使 ProxyCachingConfiguration 初始化
ProxyCachingConfiguration 是關鍵,它初始化了三個bean:
其實若是理解了@Cacheable 註解相關的Bean的初始化的話,那麼@Cacheable 註解運行時是怎麼工做的就很好理解了
前面分析可知, 全部帶@Cacheable 註解的方法都會被 CacheAspectSupport 的 execute 方法攔截處理,那麼咱們就來看一下 CacheAspectSupport 的廬山真面目吧.主要看他的 execute 方法就行.
@Nullable
protected Object execute(CacheOperationInvoker invoker, Object target, Method method, Object[] args) {
// Check whether aspect is enabled (to cope with cases where the AJ is pulled in automatically)
if (this.initialized) {
Class<?> targetClass = getTargetClass(target);
CacheOperationSource cacheOperationSource = getCacheOperationSource();
if (cacheOperationSource != null) {
Collection<CacheOperation> operations = cacheOperationSource.getCacheOperations(method, targetClass);
if (!CollectionUtils.isEmpty(operations)) {
return execute(invoker, method,
new CacheOperationContexts(operations, method, args, target, targetClass));
}
}
}
return invoker.invoke();
}
複製代碼
首先經過 getCacheOperationSource 方法拿到 CacheOperationSource,前面分析可知 CacheOperationSource 保存了帶@Cacheable 註解的Bean信息.最後調用的是execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts)方法,CacheOperationContexts是CacheAspectSupport的一個內部類,主要是對 CacheOperationSource 的一些包裝
咱們來看一下這個execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) 方法最關鍵的一步:
Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));
複製代碼
看一下 findCachedItem 方法
@Nullable
private Cache.ValueWrapper findCachedItem(Collection<CacheOperationContext> contexts) {
Object result = CacheOperationExpressionEvaluator.NO_RESULT;
for (CacheOperationContext context : contexts) {
if (isConditionPassing(context, result)) {
Object key = generateKey(context, result);
Cache.ValueWrapper cached = findInCaches(context, key);
if (cached != null) {
return cached;
}
else {
if (logger.isTraceEnabled()) {
logger.trace("No cache entry for key '" + key + "' in cache(s) " + context.getCacheNames());
}
}
}
}
return null;
}
複製代碼
就是生成key和拿value了,重點看下findInCaches 方法,這個方法最終執行的是 doGet 方法
protected Cache.ValueWrapper doGet(Cache cache, Object key) {
try {
return cache.get(key);
}
catch (RuntimeException ex) {
getErrorHandler().handleCacheGetError(ex, cache, key);
return null; // If the exception is handled, return a cache miss
}
}
複製代碼
關鍵的一行代碼: return cache.get(key);
這裏的Cache是一個接口,真正傳進來時會是具體的實現類,能夠是 RedisCache,CaffeineCache等等,而後調用對應的get方法
這裏主要講一下爲何 Springboot 版本是1.X.且 RedisCache 會報錯
咱們先看一下doGet方法裏面的key是什麼
Object key = generateKey(context, result);
private Object generateKey(CacheOperationContext context, Object result) {
Object key = context.generateKey(result);
if (key == null) {
throw new IllegalArgumentException("Null key returned for cache operation (maybe you are " +
"using named params on classes without debug info?) " + context.metadata.operation);
}
if (logger.isTraceEnabled()) {
logger.trace("Computed cache key '" + key + "' for operation " + context.metadata.operation);
}
return key;
}
protected Object generateKey(Object result) {
if (StringUtils.hasText(this.metadata.operation.getKey())) {
EvaluationContext evaluationContext = createEvaluationContext(result);
return evaluator.key(this.metadata.operation.getKey(), this.methodCacheKey, evaluationContext);
}
return this.metadata.keyGenerator.generate(this.target, this.metadata.method, this.args);
}
@Override
public Object generate(Object target, Method method, Object... params) {
return generateKey(params);
}
public static Object generateKey(Object... params) {
if (params.length == 0) {
return SimpleKey.EMPTY;
}
if (params.length == 1) {
Object param = params[0];
if (param != null && !param.getClass().isArray()) {
return param;
}
}
return new SimpleKey(params);
}
複製代碼
這裏可見當咱們方法參數爲空的時候返回的Key是一個SimpleKey 對象
咱們來看一下 RedisCache 的get 方法,先貼代碼
@Override
public ValueWrapper get(Object key) {
return get(getRedisCacheKey(key));
}
複製代碼
public RedisCacheElement get(final RedisCacheKey cacheKey) {
Assert.notNull(cacheKey, "CacheKey must not be null!");
Boolean exists = (Boolean) redisOperations.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
return connection.exists(cacheKey.getKeyBytes());
}
});
if (!exists) {
return null;
}
byte[] bytes = doLookup(cacheKey);
// safeguard if key gets deleted between EXISTS and GET calls.
if (bytes == null) {
return null;
}
return new RedisCacheElement(cacheKey, fromStoreValue(deserialize(bytes)));
}
複製代碼
public byte[] getKeyBytes() {
byte[] rawKey = serializeKeyElement();
if (!hasPrefix()) {
return rawKey;
}
byte[] prefixedKey = Arrays.copyOf(prefix, prefix.length + rawKey.length);
System.arraycopy(rawKey, 0, prefixedKey, prefix.length, rawKey.length);
return prefixedKey;
}
複製代碼
@SuppressWarnings("unchecked")
private byte[] serializeKeyElement() {
if (serializer == null && keyElement instanceof byte[]) {
return (byte[]) keyElement;
}
return serializer.serialize(keyElement);
}
複製代碼
public byte[] serialize(String string) {
return (string == null ? null : string.getBytes(charset));
}
複製代碼
分析一下調用鏈:
get(Object key)-->get(final RedisCacheKey cacheKey)-->getKeyBytes()-->serializeKeyElement()-->serializer.serialize(keyElement)
咱們看最後一步serializer.serialize(keyElement)調用的是StringRedisSerializer的serialize方法,SimpleKey 轉String報錯...
終於真相大白了....
咱們知道是由於 generateKey 生成的key爲一個SimpleKey 對象而不是String,轉型報錯,因此咱們可不能夠重寫 generateKey 方法呢, 答案是能夠的.
代碼以下:
@Override
@Bean
public KeyGenerator keyGenerator() {
return (target, method, objects) -> {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getName());
sb.append(method.getName());
for (Object obj : objects) {
sb.append(obj.toString());
}
return sb.toString();
};
}
複製代碼
答案就是在cache.get(key)的時候cache不是RedisCache了,而是TransactionAwareCacheDecorator ,get的時候調用的是 AbstractValueAdaptingCache的get方法