關於Spring Cache 緩存攔截器( CacheInterceptor)

Spring Cache 緩存攔截器( CacheInterceptor)

打開Spring Cache的核心緩存攔截器CacheInterceptor,可以看到具體實現:

public class CacheInterceptor extends CacheAspectSupport implements MethodInterceptor, Serializable {
	@Override
	public Object invoke(final MethodInvocation invocation) throws Throwable {
		Method method = invocation.getMethod();
		CacheOperationInvoker aopAllianceInvoker = new CacheOperationInvoker() {
			@Override
			public Object invoke() {
				try {
					return invocation.proceed();
				}
				catch (Throwable ex) {
					throw new ThrowableWrapper(ex);
				}
			}
		};
		try {
			return execute(aopAllianceInvoker, invocation.getThis(), method, invocation.getArguments());
		}
		catch (CacheOperationInvoker.ThrowableWrapper th) {
			throw th.getOriginal();
		}
	}
}

CacheInterceptor默認實現瞭Spring aop的MethodInterceptor接口,MethodInterceptor的功能是做方法攔截。攔截的方法都會調用invoke方法,在invoke方法裡面主要緩存邏輯是在execute方法裡面,該方法是繼承瞭父類CacheAspectSupport。

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);
			//獲取執行方法上所有的緩存操作集合。如果有緩存操作則執行到execute(...),如果沒有就執行invoker.invoke()直接調用執行方法瞭
			Collection<CacheOperation> operations = getCacheOperationSource().getCacheOperations(method, targetClass);
			if (!CollectionUtils.isEmpty(operations)) {
				return execute(invoker, method, new CacheOperationContexts(operations, method, args, target, targetClass));
			}
		}
		return invoker.invoke();
	}

集合Collection operations中存放瞭所有的緩存操作CachePutOperation、CacheableOperation、CacheEvictOperation

spring cache常用的三種緩存操作

  • @CachePut:執行方法後,將方法返回結果存放到緩存中。不管有沒有緩存過,執行方法都會執行,並緩存返回結果(unless可以否決進行緩存)。(當然,這裡說的緩存都要滿足condition條件)
  • @Cacheable:如果沒有緩存過,獲取執行方法的返回結果;如果緩存過,則直接從緩存中獲取,不再執行方法。
  • @CacheEvict:如果設置瞭beforeIntercepte則在方法執行前進行緩存刪除操作,如果沒有,則在執行方法調用完後進行緩存刪除操作。
private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
		// Special handling of synchronized invocation
		if (contexts.isSynchronized()) {
			CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next();
			if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) {
				Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT);
				Cache cache = context.getCaches().iterator().next();
				try {
					return wrapCacheValue(method, cache.get(key, new Callable<Object>() {
						@Override
						public Object call() throws Exception {
							return unwrapReturnValue(invokeOperation(invoker));
						}
					}));
				}
				catch (Cache.ValueRetrievalException ex) {
					// The invoker wraps any Throwable in a ThrowableWrapper instance so we
					// can just make sure that one bubbles up the stack.
					throw (CacheOperationInvoker.ThrowableWrapper) ex.getCause();
				}
			}
			else {
				// No caching required, only call the underlying method
				return invokeOperation(invoker);
			}
		}
		// 處理beforeIntercepte=true的緩存刪除操作
		processCacheEvicts(contexts.get(CacheEvictOperation.class), true,
				CacheOperationExpressionEvaluator.NO_RESULT);
		// 從緩存中查找,是否有匹配@Cacheable的緩存數據
		Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));
		// 如果@Cacheable沒有被緩存,那麼就需要將數據緩存起來,這裡將@Cacheable操作收集成CachePutRequest集合,以便後續做@CachePut緩存數據存放。
		List<CachePutRequest> cachePutRequests = new LinkedList<CachePutRequest>();
		if (cacheHit == null) {
			collectPutRequests(contexts.get(CacheableOperation.class),
					CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);
		}
		Object cacheValue;
		Object returnValue;
		//如果沒有@CachePut操作,就使用@Cacheable獲取的結果(可能也沒有@Cableable,所以result可能為空)。
		if (cacheHit != null && cachePutRequests.isEmpty() && !hasCachePut(contexts)) {
			//如果沒有@CachePut操作,並且cacheHit不為空,說明命中緩存瞭,直接返回緩存結果
			cacheValue = cacheHit.get();
			returnValue = wrapCacheValue(method, cacheValue);
		}
		else {
			// 否則執行具體方法內容,返回緩存的結果
			returnValue = invokeOperation(invoker);
			cacheValue = unwrapReturnValue(returnValue);
		}
		// Collect any explicit @CachePuts
		collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);
		// Process any collected put requests, either from @CachePut or a @Cacheable miss
		for (CachePutRequest cachePutRequest : cachePutRequests) {
			cachePutRequest.apply(cacheValue);
		}
		// Process any late evictions
		processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);
		return returnValue;
	}
	//根據key從緩存中查找,返回的結果是ValueWrapper,它是返回結果的包裝器
	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;
	}
	private Cache.ValueWrapper findInCaches(CacheOperationContext context, Object key) {
		for (Cache cache : context.getCaches()) {
			Cache.ValueWrapper wrapper = doGet(cache, key);
			if (wrapper != null) {
				if (logger.isTraceEnabled()) {
					logger.trace("Cache entry for key '" + key + "' found in cache '" + cache.getName() + "'");
				}
				return wrapper;
			}
		}
		return null;
	}

具體整個流程是這樣的

在這裡插入圖片描述

CacheInterceptor.java

項目中基本上都需要使用到Cache的功能, 但是Spring提供的Cacheable並不能很好的滿足我們的需求, 所以這裡自己借助Spring思想完成自己的業務邏輯.

定義Cacheable註解

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Cacheable { 
    RedisKey value(); 
    String key();
}

定義Rediskey.java

public enum RedisKeyEnum { 
    TEST_CACHE("test:", 24, TimeUnit.HOURS, "Test");
 
    /**
     * 緩存Key的前綴
     */
    private String keyPrefix;
 
    /**
     * 過期時間
     */
    private long timeout;
 
    /**
     * 過期時間單位
     */
    private TimeUnit timeUnit;
 
    /**
     * 描述
     */
    private String desc; 
    private static final String REDIS_KEY_DEFUALT_SEPARATOR = ":"; 
    RedisKey(String keyPrefix, long timeout, TimeUnit timeUnit, String desc){
        this.keyPrefix = keyPrefix;
        this.timeout = timeout;
        this.timeUnit = timeUnit;
        this.desc = desc;
    }
 
    public long getTimeout() {
        return timeout;
    }
 
    public TimeUnit getTimeUnit() {
        return timeUnit;
    }
 
    public String getDesc() {
        return desc;
    }
 
    /**
     * 獲取完整的緩存Key
     * @param keys
     * @return
     */
    public String getKey(String... keys) {
        if(keys == null || keys.length <= 0){
            return this.keyPrefix;
        }
        String redisKey = keyPrefix;
        for (int i = 0, length = keys.length; i < length; i++) {
            String key = keys[i];
            redisKey += key;
            if (i < length - 1) {
                redisKey += REDIS_KEY_DEFUALT_SEPARATOR;
            }
        }
        return redisKey;
    }
}

Cache.java

public interface Cache<K, V> { 
    /**
     * 返回緩存名稱
     * @return
     */
    String getName();
 
    /**
     * 添加一個緩存實例
     *
     * @param key
     * @param value
     */
    V put(K key, V value);
 
    /**
     * 添加一個可過期的緩存實例
     * @param key
     * @param value
     * @param expire
     * @param timeUnit
     * @return
     */
    V put(K key, V value, long expire, TimeUnit timeUnit);
 
    /**
     * 返回緩存數據
     *
     * @param key
     * @return
     */
    V get(K key);
 
    /**
     * 刪除一個緩存實例, 並返回緩存數據
     *
     * @param key
     * @return
     */
    void remove(K key);
 
    /**
     * 獲取所有的緩存key
     * @return
     */
    Set<K> keys();
 
    /**
     * 獲取所有的緩存key
     * @return
     */
    Set<K> keys(K pattern);
 
    /**
     * 獲取所有的緩存數據
     * @return
     */
    Collection<V> values();
 
    /**
     * 清空所有緩存
     */
    void clear();
}

RedisCache.java

public class RedisCache<K, V> implements Cache<K, V> { 
    public static final String DEFAULT_CACHE_NAME =  RedisCache.class.getName() + "_CACHE_NAME"; 
    private RedisTemplate<K, V> redisTemplate; 
    private ValueOperations<K, V> valueOperations; 
    public RedisCache(RedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
        this.valueOperations = redisTemplate.opsForValue();
        DataType dataType = redisTemplate.type("a");
    }
 
    @Override
    public String getName() {
        return DEFAULT_CACHE_NAME;
    }
 
    @Override
    public V put(K key, V value) {
        valueOperations.set(key, value);
        return value;
    }
 
    @Override
    public V put(K key, V value, long expire, TimeUnit timeUnit) {
        valueOperations.set(key, value, expire, timeUnit);
        return value;
    }
 
    @Override
    public V get(K key) {
        return valueOperations.get(key);
    }
 
    @Override
    public void remove(K key) {
//        V value = valueOperations.get(key);
        redisTemplate.delete(key);
    }
 
    @Override
    public Set<K> keys() {
        return null;
    }
 
    @Override
    public Set<K> keys(K pattern) {
        return redisTemplate.keys(pattern);
    }
 
    @Override
    public Collection<V> values() {
        return null;
    }
 
    @Override
    public void clear() { 
    }
}

CacheManager.java

public interface CacheManager { 
    /**
     * 獲取緩存
     * @return
     */
    Cache getCache(String name);
 
    /**
     * 獲取所有的緩存名稱
     */
    Collection<String> getCacheNames(); 
}

AbstractCacheManager.java

public abstract class AbstractCacheManager implements CacheManager, InitializingBean, DisposableBean { 
    private static final Logger logger = LoggerFactory.getLogger(AbstractCacheManager.class); 
    private final Map<String, Cache> cacheMap = new ConcurrentHashMap<>(16); 
    private volatile Set<String> cacheNames = Collections.emptySet(); 
    private static final String DEFAULT_CACHE_NAME_SUFFIX = "_CACHE_NAME";
 
    @Override
    public void afterPropertiesSet() throws Exception {
        initlalizingCache();
    }
 
    private void initlalizingCache(){
        Collection<? extends Cache> caches = loadCaches();
        synchronized (this.cacheMap) {
            this.cacheNames = Collections.emptySet();
            this.cacheMap.clear();
            Set<String> cacheNames = new LinkedHashSet<String>(caches.size());
            for (Cache cache : caches) {
                String name = cache.getName();
                if(StringUtils.isEmpty(name)){
                    name = cache.getClass().getName() + DEFAULT_CACHE_NAME_SUFFIX;
                }
                this.cacheMap.put(name, cache);
                cacheNames.add(name);
            }
            this.cacheNames = Collections.unmodifiableSet(cacheNames);
        }
    }
 
    @Override
    public Cache getCache(String name) {
        Cache cache = cacheMap.get(name);
        if(cache != null){
            return cache;
        }
        return null;
    }
 
    protected abstract Collection<? extends Cache> loadCaches(); 
    @Override
    public Collection<String> getCacheNames() {
        return this.cacheNames;
    }
 
    @Override
    public void destroy() throws Exception {
        cacheMap.clear();
    }
}

RedisCacheManager.java

public class RedisCacheManager extends AbstractCacheManager { 
    private RedisTemplate redisTemplate; 
    public RedisCacheManager(RedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    } 
 
    @Override
    protected Collection<? extends Cache> loadCaches() {
        Collection<Cache<String, Object>> caches = new ArrayList<>();
        RedisCache<String, Object> redisCache = new RedisCache<>(redisTemplate);
        caches.add(redisCache);
        return caches;
    }
}

實現CacheInterceptor.java

/**
 * 緩存數據過濾器, 緩存到redis數據中的數據是ServiceResult.getDateMap()數據
 * 使用: 在service方法上添加com.chinaredstar.urms.annotations.Cacheable註解, 並指定RedisKeyEunm和cache key, cache key支持Spel表達式
 * 以下情況不緩存數據:
 *  1: 返回狀態為fasle時, 不緩存數據
 *  2: 返回dataMap為空時, 不緩存數據
 *  3: 返回數據結構不是ServiceReslut實例時, 不緩存數據
 *
 * 當緩存問題時, 不影響正常業務, 但所有的請求都會打到DB上, 對DB有很大的沖擊
 */
public class CacheInterceptor implements MethodInterceptor { 
    private static final Logger logger = LoggerFactory.getLogger(CacheInterceptor.class); 
    private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); 
    private CacheManager cacheManager; 
    public void setCacheManager(CacheManager cacheManager) {
        this.cacheManager = cacheManager;
    }
 
    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        Method method = methodInvocation.getMethod();
        Object[] args = methodInvocation.getArguments();
        Cacheable cacheable = method.getAnnotation(Cacheable.class);
        if (cacheable == null) {
            return methodInvocation.proceed();
        }
        String key = parseCacheKey(method, args, cacheable.key());
        logger.info(">>>>>>>> -- 獲取緩存key : {}", key);
        if(StringUtils.isEmpty(key)){
            return methodInvocation.proceed();
        }
        RedisKey redisKey = cacheable.value();
        Cache cache = cacheManager.getCache(RedisCache.DEFAULT_CACHE_NAME);
        Object value = null;
        try{
            value = cache.get(redisKey.getKey(key));
        } catch (Exception e){
            logger.info(">>>>>>>> -- 從緩存中獲取數據異常 : {}", ExceptionUtil.exceptionStackTrace(e));
        }
        if (value != null) {
            logger.info(">>>>>>>> -- 從緩存中獲取數據 : {}", JsonUtil.toJson(value));
            return ServiceResult.newInstance(true, value);
        }
        value = methodInvocation.proceed();
        logger.info(">>>>>>>> -- 從接口中獲取數據 : {}", JsonUtil.toJson(value));
        if ( value != null && value instanceof ServiceResult ) {
            ServiceResult result = (ServiceResult) value;
            if(!result.isSuccess() || result.getDataMap() == null){
                return value;
            }
            try{
                cache.put(redisKey.getKey(key), result.getDataMap(), redisKey.getTimeout(), redisKey.getTimeUnit());
            } catch (Exception e){
                logger.info(">>>>>>>> -- 將數據放入緩存異常 : {}", ExceptionUtil.exceptionStackTrace(e));
            }
        }
        return value;
    }
 
    /**
     * 使用SpeL解析緩存key
     * @param method
     * @param args
     * @param expressionString
     * @return
     */
    private String parseCacheKey(Method method, Object[] args, String expressionString) {
        String[] parameterNames = parameterNameDiscoverer.getParameterNames(method);
        EvaluationContext context = new StandardEvaluationContext();
        if (parameterNames != null && parameterNames.length > 0
                && args != null && args.length > 0
                && args.length == parameterNames.length ) {
            for (int i = 0, length = parameterNames.length; i < length; i++) {
                context.setVariable(parameterNames[i], args[i]);
            }
        }
        ExpressionParser parser = new SpelExpressionParser();
        Expression expression = parser.parseExpression(expressionString);
        return (String) expression.getValue(context);
    }
}

配置Spring.xml

    <bean id="redisCacheManager" class="com.package.cache.RedisCacheManager">
        <constructor-arg ref="cacheRedisTemplate" />
    </bean>  
    <bean id="cacheInterceptor" class="com.package.interceptor.CacheInterceptor" p:cacheManager-ref="redisCacheManager"/>
 
    <!-- 方法攔截器 MethodInterceptor -->
    <aop:config proxy-target-class="true">
        <aop:pointcut id="cacheInterceptorPointcut" expression="execution(* com.package..*(..))
                                                                and @annotation(com.package.annotations.Cacheable)"/>
        <aop:advisor advice-ref="cacheInterceptor" pointcut-ref="cacheInterceptorPointcut" order="2" />
    </aop:config>

測試使用

@Cacheable(value = RedisKey.TEST_CACHE, key = "#code + ':' + #user.id")
public ServiceResult<String> test(String code, User user){ 
    return new ServiceResult("success");
}

說明

Cacheable其中的參數key拼接的規則支持Spring SpeL表達式。其規則和Spring Cacheable使用方法一致。

以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。

推薦閱讀: