@Cacheable注解的实现原理以及其在SpringBoot1.X和2.X中的一些差异

原由

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

  1. 首先要使 @Cacheable 注解生效, 咱们须要在启动类上加上 @EnableCaching 注解函数

  2. 咱们来看一下 @EnableCaching 注解作了什么. 主要是这个注解上加了ui

    @Import(CachingConfigurationSelector.class) 
    复制代码
  3. 接着看 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

  1. 再来看 ProxyCachingConfiguration 作了什么事情
@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

  1. 接着看 AnnotationCacheOperationSource,看它的两个构造函数和一个属性
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

  1. 咱们再简单看一下 SpringCacheAnnotationParser 这个类,这个类会把带 caching 注解的bean放到Collection ops 这个集合里.
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 注解了是否是...

  1. 咱们回过头来再看一下一个很重要的Bean:CacheInterceptor,它继承于 CacheAspectSupport ,全部带@Cacheable 注解的方法都会被其 execute 方法拦截处理

自此,关于@Cacheable 注解相关的Bean的初始化工做终于完成了.... 是否是要晕了....

最后简单总结一下:

  • 由于 @EnableCaching 注解加上了 @Import(CachingConfigurationSelector.class)

  • CachingConfigurationSelector 会使 ProxyCachingConfiguration 初始化

  • ProxyCachingConfiguration 是关键,它初始化了三个bean:

    • CacheOperationSource (保存了全部带 @Cacheable 注解的Bean信息)
    • CacheInterceptor (此类继承自 CacheAspectSupport,全部带@Cacheable 注解的方法都会被其 execute 方法拦截处理)
    • BeanFactoryCacheOperationSourceAdvisor (前两个bean为这个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();
        };
    }
复制代码

为何2.X没问题呢

答案就是在cache.get(key)的时候cache不是RedisCache了,而是TransactionAwareCacheDecorator ,get的时候调用的是 AbstractValueAdaptingCache的get方法

相关文章
相关标签/搜索