@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方法

相關文章
相關標籤/搜索