在使用 @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方法