Spring Cache --- @Cacheable/@CachePut/@CacheEvict注解的原理深度剖析和使用【享學(xué)Spring】

前言

上篇文章介紹了@EnableCaching瓣距,用它來開啟Spring對緩存注解的支持。本篇文章將繼續(xù)分析Spring Cache,并且講解的是我們最為關(guān)心的:緩存注解實(shí)操方面的原理支持和使用。

開發(fā)過程中因注解的優(yōu)雅、使用簡單使得這種方式廣泛被大家所接受和使用猴誊,本文將按照先原理,再實(shí)操的步驟侮措,一步步解惑Spring緩存注解的原理

緩存注解

關(guān)于Spring的緩存注解懈叹,一共有如下5個(gè):

  1. @Cacheable:緩存
// @since 3.1  可以標(biāo)注在方法上、類上  下同
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Cacheable {
    // 緩存名稱  可以寫多個(gè)~
    @AliasFor("cacheNames")
    String[] value() default {};
    @AliasFor("value")
    String[] cacheNames() default {};

    // 支持寫SpEL分扎,切可以使用#root
    String key() default "";
    // Mutually exclusive:它和key屬性互相排斥澄成。請只使用一個(gè)
    String keyGenerator() default "";

    String cacheManager() default "";
    String cacheResolver() default "";

    // SpEL,可以使用#root畏吓。  只有true時(shí)墨状,才會作用在這個(gè)方法上
    String condition() default "";
    // 可以寫SpEL #root,并且可以使用#result拿到方法返回值~~~
    String unless() default "";

    // true:表示強(qiáng)制同步執(zhí)行菲饼。(若多個(gè)線程試圖為**同一個(gè)鍵**加載值肾砂,以同步的方式來進(jìn)行目標(biāo)方法的調(diào)用)
    // 同步的好處是:后一個(gè)線程會讀取到前一個(gè)緩存的緩存數(shù)據(jù),不用再查庫了~~~ 
    // 默認(rèn)是false宏悦,不開啟同步one by one的
    // @since 4.3  注意是sync而不是Async
    // 它的解析依賴于Spring4.3提供的Cache.get(Object key, Callable<T> valueLoader);方法
    boolean sync() default false;
}
  1. @CachePut:緩存更新
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Cacheable {

    @AliasFor("cacheNames")
    String[] value() default {};
    @AliasFor("value")
    String[] cacheNames() default {};

    // 注意:它和上面區(qū)別是镐确。此處key它還能使用#result
    String key() default "";
    String keyGenerator() default "";

    String cacheManager() default "";
    String cacheResolver() default "";

    String condition() default "";
    String unless() default "";
}
  1. @CacheEvict:緩存刪除
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Cacheable {

    @AliasFor("cacheNames")
    String[] value() default {};
    @AliasFor("value")
    String[] cacheNames() default {};

    // 它也能使用#result
    String key() default "";
    String keyGenerator() default "";

    String cacheManager() default "";
    String cacheResolver() default "";
    String condition() default "";

    // 是否把上面cacheNames指定的所有的緩存都清除掉,默認(rèn)false
    boolean allEntries() default false;
    // 是否讓清理緩存動作在目標(biāo)方法之前執(zhí)行饼煞,默認(rèn)是false(在目標(biāo)方法之后執(zhí)行)
    // 注意:若在之后執(zhí)行的話源葫,目標(biāo)方法一旦拋出異常了,那緩存就清理不掉了~~~~
    boolean beforeInvocation() default false;

}
  1. @Caching:用于處理復(fù)雜的緩存情況砖瞧。比如用戶既要根據(jù)id緩存一份息堂,也要根據(jù)電話緩存一份,還要根據(jù)電子郵箱緩存一份,就可以使用它
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Caching {
    Cacheable[] cacheable() default {};
    CachePut[] put() default {};
    CacheEvict[] evict() default {};
}
  1. @CacheConfig:可以在類級別上標(biāo)注一些共用的緩存屬性荣堰。(所有方法共享床未,@since 4.1)
// @since 4.1 出現(xiàn)得還是比較晚的
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CacheConfig {
    String[] cacheNames() default {};
    String keyGenerator() default "";
    String cacheManager() default "";
    String cacheResolver() default "";
}

原理分析

先閱讀:【小家Spring】玩轉(zhuǎn)Spring Cache — @Cacheable/@CachePut/@CacheEvict緩存注解相關(guān)基礎(chǔ)類打點(diǎn) 再讀本文,效果會像德芙一般絲滑~

從上篇文章中已經(jīng)知道了@EnableCaching主要向容器注入了三個(gè)Bean:CacheOperationSource振坚、BeanFactoryCacheOperationSourceAdvisor即硼、CacheInterceptor。他們是讓注解生效的核心類屡拨。

CacheOperationSource

它代表緩存操作源,已經(jīng)分析過褥实。

BeanFactoryCacheOperationSourceAdvisor

從名字就能看出它是一個(gè)增強(qiáng)器Advisor呀狼,并且還和BeanFactory有關(guān)。

@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;
    }

從上配置知道损离,這個(gè)增強(qiáng)器的切面Advice是CacheInterceptor哥艇,并且持有CacheOperationSource的引用。

public class BeanFactoryCacheOperationSourceAdvisor extends AbstractBeanFactoryPointcutAdvisor {
    @Nullable
    private CacheOperationSource cacheOperationSource;

    // 切面Pointcut
    private final CacheOperationSourcePointcut pointcut = new CacheOperationSourcePointcut() {
        @Override
        @Nullable
        protected CacheOperationSource getCacheOperationSource() {
            return cacheOperationSource;
        }
    };

    public void setCacheOperationSource(CacheOperationSource cacheOperationSource) {
        this.cacheOperationSource = cacheOperationSource;
    }

    // 注意:此處你可以自定義一個(gè)ClassFilter僻澎,過濾掉你想忽略的類
    public void setClassFilter(ClassFilter classFilter) {
        this.pointcut.setClassFilter(classFilter);
    }

    @Override
    public Pointcut getPointcut() {
        return this.pointcut;
    }
}

Advisor的實(shí)現(xiàn)非常的簡單貌踏,切點(diǎn)是CacheOperationSourcePointcut,核心邏輯都依托于緩存屬性源窟勃。所以還沒有看這塊的祖乳,此處再一次推薦:【小家Spring】玩轉(zhuǎn)Spring Cache — @Cacheable/@CachePut/@CacheEvict緩存注解相關(guān)基礎(chǔ)類打點(diǎn)

CacheInterceptor

緩存攔截器。先說明一點(diǎn)秉氧,它的實(shí)現(xiàn)模式幾乎和TransactionInterceptor一毛一樣眷昆。所以我又想建議一句了,有空先看看它吧:【小家Spring】源碼分析Spring的事務(wù)攔截器:TransactionInterceptor和事務(wù)管理器:PlatformTransactionManager

同樣汁咏,CacheInterceptor是緩存真正執(zhí)行的核心亚斋,處理邏輯還是稍顯復(fù)雜的。

// @since 3.1  它是個(gè)MethodInterceptor環(huán)繞增強(qiáng)器~~~
public class CacheInterceptor extends CacheAspectSupport implements MethodInterceptor, Serializable {

    @Override
    @Nullable
    public Object invoke(final MethodInvocation invocation) throws Throwable {
        Method method = invocation.getMethod();

        // 采用函數(shù)的形式攘滩,最終把此函數(shù)傳交給父類的execute()去執(zhí)行
        // 但是很顯然帅刊,最終**執(zhí)行目標(biāo)方法**的是invocation.proceed();它

        //這里就是對執(zhí)行方法調(diào)用的一次封裝,主要是為了處理對異常的包裝漂问。
        CacheOperationInvoker aopAllianceInvoker = () -> {
            try {
                return invocation.proceed();
            }
            catch (Throwable ex) {
                throw new CacheOperationInvoker.ThrowableWrapper(ex);
            }
        };

        try {
            // //真正地去處理緩存操作的執(zhí)行赖瞒,很顯然這是父類的方法,所以我們要到父類CacheAspectSupport中去看看级解。
            return execute(aopAllianceInvoker, invocation.getThis(), method, invocation.getArguments());
        } catch (CacheOperationInvoker.ThrowableWrapper th) {
            throw th.getOriginal();
        }
    }

}

這個(gè)類本身的實(shí)現(xiàn)很少冒黑,主要邏輯都在他的抽象父類:CacheAspectSupport

CacheAspectSupport

它類似于TransactionAspectSupport,父類實(shí)現(xiàn)了所有的核心邏輯

// @since 3.1  它相較于TransactionAspectSupport額外實(shí)現(xiàn)了SmartInitializingSingleton接口
// SmartInitializingSingleton應(yīng)該也不會陌生勤哗。它在初始化完所有的單例Bean后會執(zhí)行這個(gè)接口的`afterSingletonsInstantiated()`方法
// 比如我們熟悉的ScheduledAnnotationBeanPostProcessor抡爹、EventListenerMethodProcessor都是這么來處理的

// 另外還需要注意,它還繼承自AbstractCacheInvoker:主要對異常情況用CacheErrorHandler處理
public abstract class CacheAspectSupport extends AbstractCacheInvoker implements BeanFactoryAware, InitializingBean, SmartInitializingSingleton {

    // CacheOperationCacheKey:緩存的key  CacheOperationMetadata就是持有一些基礎(chǔ)屬性的性息
    // 這個(gè)緩存挺大芒划,相當(dāng)于每一個(gè)類冬竟、方法都有氣對應(yīng)的**緩存屬性元數(shù)據(jù)**

    private final Map<CacheOperationCacheKey, CacheOperationMetadata> metadataCache = new ConcurrentHashMap<>(1024);
    // 解析一些condition欧穴、key、unless等可以寫el表達(dá)式的處理器~~~
    // 之前講過的熟悉的有:EventExpressionEvaluator
    private final CacheOperationExpressionEvaluator evaluator = new CacheOperationExpressionEvaluator();
    // 屬性源泵殴,默認(rèn)情況下是基于注解的`AnnotationCacheOperationSource`
    @Nullable
    private CacheOperationSource cacheOperationSource;
    // 看到了吧  key生成器默認(rèn)使用的SimpleKeyGenerator
    // 注意SingletonSupplier是Spring5.1的新類涮帘,實(shí)現(xiàn)了接口java.util.function.Supplier  主要是對null值進(jìn)行了容錯(cuò)
    private SingletonSupplier<KeyGenerator> keyGenerator = SingletonSupplier.of(SimpleKeyGenerator::new);

    @Nullable
    private SingletonSupplier<CacheResolver> cacheResolver;
    @Nullable
    private BeanFactory beanFactory;
    private boolean initialized = false;

    // @since 5.1
    public void configure(@Nullable Supplier<CacheErrorHandler> errorHandler, @Nullable Supplier<KeyGenerator> keyGenerator,@Nullable Supplier<CacheResolver> cacheResolver, @Nullable Supplier<CacheManager> cacheManager) {
        // 第二個(gè)參數(shù)都是默認(rèn)值,若調(diào)用者沒傳的話
        this.errorHandler = new SingletonSupplier<>(errorHandler, SimpleCacheErrorHandler::new);
        this.keyGenerator = new SingletonSupplier<>(keyGenerator, SimpleKeyGenerator::new);
        this.cacheResolver = new SingletonSupplier<>(cacheResolver, () -> SimpleCacheResolver.of(SupplierUtils.resolve(cacheManager)));
    }

    // 此處:若傳入了多個(gè)cacheOperationSources笑诅,那最終使用的就是CompositeCacheOperationSource包裝起來
    // 所以發(fā)現(xiàn)调缨,Spring是支持我們多種 緩存屬性源的
    public void setCacheOperationSources(CacheOperationSource... cacheOperationSources) {
        Assert.notEmpty(cacheOperationSources, "At least 1 CacheOperationSource needs to be specified");
        this.cacheOperationSource = (cacheOperationSources.length > 1 ? new CompositeCacheOperationSource(cacheOperationSources) : cacheOperationSources[0]);
    }
    // @since 5.1 單數(shù)形式的設(shè)置
    public void setCacheOperationSource(@Nullable CacheOperationSource cacheOperationSource) {
        this.cacheOperationSource = cacheOperationSource;
    }

    ... // 省略各種get/set方法~~~

    // CacheOperationSource必須不為null,因?yàn)橐磺幸劳杏谒?    @Override
    public void afterPropertiesSet() {
        Assert.state(getCacheOperationSource() != null, "The 'cacheOperationSources' property is required: " + "If there are no cacheable methods, then don't use a cache aspect.");
    }

    // 這個(gè)來自于接口:SmartInitializingSingleton  在實(shí)例化完所有單例Bean后調(diào)用
    @Override
    public void afterSingletonsInstantiated() {
        // 若沒有給這個(gè)切面手動設(shè)置cacheResolver  那就去拿CacheManager吧
        // 這就是為何我們只需要把CacheManager配進(jìn)容器里即可  就自動會設(shè)置在切面里了
        if (getCacheResolver() == null) {
            // Lazily initialize cache resolver via default cache manager...
            Assert.state(this.beanFactory != null, "CacheResolver or BeanFactory must be set on cache aspect");
            try {
                // 請注意:這個(gè)方法實(shí)際上是把CacheManager包裝成了一個(gè)SimpleCacheResolver
                // 所以最終還是給SimpleCacheResolver賦值
                setCacheManager(this.beanFactory.getBean(CacheManager.class));
            } ...
        }
        this.initialized = true;
    }

    // 主要為了輸出日志吆你,子類可復(fù)寫
    protected String methodIdentification(Method method, Class<?> targetClass) {
        Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
        return ClassUtils.getQualifiedMethodName(specificMethod);
    }

    // 從這里也能看出弦叶,至少要指定一個(gè)Cache才行(也就是cacheNames)
    protected Collection<? extends Cache> getCaches(CacheOperationInvocationContext<CacheOperation> context, CacheResolver cacheResolver) {

        Collection<? extends Cache> caches = cacheResolver.resolveCaches(context);
        if (caches.isEmpty()) {
            throw new IllegalStateException("No cache could be resolved for '" +
                    context.getOperation() + "' using resolver '" + cacheResolver +
                    "'. At least one cache should be provided per cache operation.");
        }
        return caches;
    }

    // 這個(gè)根據(jù)CacheOperation 這部分還是比較重要的
    protected CacheOperationMetadata getCacheOperationMetadata(CacheOperation operation, Method method, Class<?> targetClass) {
        CacheOperationCacheKey cacheKey = new CacheOperationCacheKey(operation, method, targetClass);
        CacheOperationMetadata metadata = this.metadataCache.get(cacheKey);

        if (metadata == null) {
            // 1、指定了KeyGenerator就去拿這個(gè)Bean(沒有就報(bào)錯(cuò)妇多,所以key不要寫錯(cuò)了)
            // 沒有指定就用默認(rèn)的
            KeyGenerator operationKeyGenerator;
            if (StringUtils.hasText(operation.getKeyGenerator())) {
                operationKeyGenerator = getBean(operation.getKeyGenerator(), KeyGenerator.class);
            } else {
                operationKeyGenerator = getKeyGenerator();
            }

            // 1伤哺、自己指定的CacheResolver
            // 2、再看指定的的CacheManager,包裝成一個(gè)SimpleCacheResolver
            // 3者祖、
            CacheResolver operationCacheResolver;
            if (StringUtils.hasText(operation.getCacheResolver())) {
                operationCacheResolver = getBean(operation.getCacheResolver(), CacheResolver.class);
            } else if (StringUtils.hasText(operation.getCacheManager())) {
                CacheManager cacheManager = getBean(operation.getCacheManager(), CacheManager.class);
                operationCacheResolver = new SimpleCacheResolver(cacheManager);
            } else { //最終都沒配置的話立莉,取本切面默認(rèn)的
                operationCacheResolver = getCacheResolver();
                Assert.state(operationCacheResolver != null, "No CacheResolver/CacheManager set");
            }

            // 封裝成Metadata
            metadata = new CacheOperationMetadata(operation, method, targetClass, operationKeyGenerator, operationCacheResolver);
            this.metadataCache.put(cacheKey, metadata);
        }
        return metadata;
    }

    // qualifiedBeanOfType的意思是,@Bean類上面標(biāo)注@Qualifier注解也生效
    protected <T> T getBean(String beanName, Class<T> expectedType) {
        if (this.beanFactory == null) {
            throw new IllegalStateException(
                    "BeanFactory must be set on cache aspect for " + expectedType.getSimpleName() + " retrieval");
        }
        return BeanFactoryAnnotationUtils.qualifiedBeanOfType(this.beanFactory, expectedType, beanName);
    }

    // 請Meta數(shù)據(jù)的緩存
    protected void clearMetadataCache() {
        this.metadataCache.clear();
        this.evaluator.clear();
    }

    // 父類最為核心的方法七问,真正執(zhí)行目標(biāo)方法 + 緩存操作
    @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)
        // 如果已經(jīng)表示初始化過了(有CacheManager蜓耻,CacheResolver了),執(zhí)行這里
        if (this.initialized) {
            // getTargetClass拿到原始Class  解剖代理(N層都能解開)
            Class<?> targetClass = getTargetClass(target);
            CacheOperationSource cacheOperationSource = getCacheOperationSource();

            if (cacheOperationSource != null) {
                // 簡單的說就是拿到該方法上所有的CacheOperation緩存操作械巡,最終一個(gè)一個(gè)的執(zhí)行~~~~
                Collection<CacheOperation> operations = cacheOperationSource.getCacheOperations(method, targetClass);
                if (!CollectionUtils.isEmpty(operations)) {

                    // CacheOperationContexts是非常重要的一個(gè)私有內(nèi)部類
                    // 注意它是復(fù)數(shù)哦~不是CacheOperationContext單數(shù)  所以它就像持有多個(gè)注解上下文一樣  一個(gè)個(gè)執(zhí)行吧
                    // 所以我建議先看看此類的描述媒熊,再繼續(xù)往下看~~~
                    return execute(invoker, method, new CacheOperationContexts(operations, method, args, target, targetClass));
                }
            }
        }

        // 若還沒初始化  直接執(zhí)行目標(biāo)方法即可
        return invoker.invoke();
    }

    @Nullable
    private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
        // Special handling of synchronized invocation
        // 如果是需要同步執(zhí)行的話,這塊還是
        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, () -> 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);
            }
        }

        // sync=false的情況坟比,走這里~~~

        // Process any early evictions  beforeInvocation=true的會在此處最先執(zhí)行~~~

        // 最先處理@CacheEvict注解~~~真正執(zhí)行的方法請參見:performCacheEvict
        // context.getCaches()拿出所有的caches芦鳍,看看是執(zhí)行cache.evict(key);方法還是cache.clear();而已
        // 需要注意的的是context.isConditionPassing(result); condition條件此處生效,并且可以使用#result
        // context.generateKey(result)也能使用#result
        // @CacheEvict沒有unless屬性
        processCacheEvicts(contexts.get(CacheEvictOperation.class), true, CacheOperationExpressionEvaluator.NO_RESULT);

        // 執(zhí)行@Cacheable  看看緩存是否能夠命中
        Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));

        // Collect puts from any @Cacheable miss, if no cached item is found
        List<CachePutRequest> cachePutRequests = new LinkedList<>();
        // 如果緩存沒有命中葛账,那就準(zhǔn)備一個(gè)cachePutRequest
        // 因?yàn)锧Cacheable首次進(jìn)來肯定命中不了柠衅,最終肯定是需要執(zhí)行一次put操作的~~~這樣下次進(jìn)來就能命中了呀
        if (cacheHit == null) {
            collectPutRequests(contexts.get(CacheableOperation.class), CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);
        }

        Object cacheValue;
        Object returnValue;

        // 如果緩存命中了,并且并且沒有@CachePut的話籍琳,也就直接返回了~~
        if (cacheHit != null && !hasCachePut(contexts)) {
            // If there are no put requests, just use the cache hit
            cacheValue = cacheHit.get();
            // wrapCacheValue主要是支持到了Optional
            returnValue = wrapCacheValue(method, cacheValue);
        } else { //到此處菲宴,目標(biāo)方法就肯定是需要執(zhí)行了的~~~~~
            // Invoke the method if we don't have a cache hit
            // 啥都不說,先invokeOperation執(zhí)行目標(biāo)方法趋急,拿到方法的的返回值  后續(xù)在處理put啥的
            returnValue = invokeOperation(invoker);
            cacheValue = unwrapReturnValue(returnValue);
        }

        // Collect any explicit @CachePuts   explicit:明確的
        collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);

        // Process any collected put requests, either from @CachePut or a @Cacheable miss
        for (CachePutRequest cachePutRequest : cachePutRequests) {
            // 注意:此處unless啥的生效~~~~
            // 最終執(zhí)行cache.put(key, result);方法
            cachePutRequest.apply(cacheValue);
        }

        // Process any late evictions beforeInvocation=true的會在此處最先執(zhí)行~~~  beforeInvocation=false的會在此處最后執(zhí)行~~~
        // 所以中途若拋出異常喝峦,此部分就不會執(zhí)行了~~~~
        processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);
        return returnValue;
    }

    // 緩存屬性的上下文們。每個(gè)方法可以對應(yīng)多個(gè)上下文~~~
    private class CacheOperationContexts {

        // 因?yàn)榉椒ㄉ峡梢詷?biāo)注多個(gè)注解 
        // 需要注意的是它的key是Class呜达,而CacheOperation的子類也就那三個(gè)哥們而已~
        private final MultiValueMap<Class<? extends CacheOperation>, CacheOperationContext> contexts;
        // 是否要求同步執(zhí)行谣蠢,默認(rèn)值是false
        private final boolean sync;

        public CacheOperationContexts(Collection<? extends CacheOperation> operations, Method method, Object[] args, Object target, Class<?> targetClass) {

            this.contexts = new LinkedMultiValueMap<>(operations.size());
            for (CacheOperation op : operations) {
                this.contexts.add(op.getClass(), getOperationContext(op, method, args, target, targetClass));
            }

            // sync這個(gè)屬性雖然不怎么使用,但determineSyncFlag這個(gè)方法可以看一下
            this.sync = determineSyncFlag(method);
        }

        public Collection<CacheOperationContext> get(Class<? extends CacheOperation> operationClass) {
            Collection<CacheOperationContext> result = this.contexts.get(operationClass);
            return (result != null ? result : Collections.emptyList());
        }
        public boolean isSynchronized() {
            return this.sync;
        }

        // 因?yàn)橹挥蠤Cacheable有sync屬性,所以只需要看CacheableOperation即可
        private boolean determineSyncFlag(Method method) {
            List<CacheOperationContext> cacheOperationContexts = this.contexts.get(CacheableOperation.class);
            if (cacheOperationContexts == null) {  // no @Cacheable operation at all
                return false;
            }

            boolean syncEnabled = false;
            // 單反只要有一個(gè)@Cacheable的sync=true了眉踱,那就為true  并且下面還有檢查邏輯
            for (CacheOperationContext cacheOperationContext : cacheOperationContexts) {
                if (((CacheableOperation) cacheOperationContext.getOperation()).isSync()) {
                    syncEnabled = true;
                    break;
                }
            }

            // 執(zhí)行sync=true的檢查邏輯
            if (syncEnabled) {
                // 人話解釋:sync=true時(shí)候挤忙,不能還有其它的緩存操作 也就是說@Cacheable(sync=true)的時(shí)候只能單獨(dú)使用
                if (this.contexts.size() > 1) {
                    throw new IllegalStateException("@Cacheable(sync=true) cannot be combined with other cache operations on '" + method + "'");
                }
                // 人話解釋:@Cacheable(sync=true)時(shí),多個(gè)@Cacheable也是不允許的
                if (cacheOperationContexts.size() > 1) {
                    throw new IllegalStateException("Only one @Cacheable(sync=true) entry is allowed on '" + method + "'");
                }

                // 拿到唯一的一個(gè)@Cacheable
                CacheOperationContext cacheOperationContext = cacheOperationContexts.iterator().next();
                CacheableOperation operation = (CacheableOperation) cacheOperationContext.getOperation();

                // 人話解釋:@Cacheable(sync=true)時(shí)谈喳,cacheName只能使用一個(gè)
                if (cacheOperationContext.getCaches().size() > 1) {
                    throw new IllegalStateException("@Cacheable(sync=true) only allows a single cache on '" + operation + "'");
                }
                // 人話解釋:sync=true時(shí)册烈,unless屬性是不支持的~~~并且是不能寫的
                if (StringUtils.hasText(operation.getUnless())) {
                    throw new IllegalStateException("@Cacheable(sync=true) does not support unless attribute on '" + operation + "'");
                }
                return true; // 只有校驗(yàn)都通過后,才返回true
            }
            return false;
        }
    }
    ...
}

以上婿禽,攔截器實(shí)現(xiàn)了Spring Cache處理注解緩存的執(zhí)行的核心步驟赏僧,個(gè)人建議上述代碼可多讀幾遍,其義自見扭倾。

處理緩存注解的步驟總結(jié)

Spring Cache是Spring框架的核心模塊之一次哈,不可謂不重要。用了好幾篇文章專門來講解使用吆录、分析原理。下面按照正常的思路琼牧,我把Spring處理的步驟總結(jié)如下:

  1. CacheOperation封裝了@CachePut恢筝、@Cacheable@CacheEvict(下稱三大緩存注解)的屬性信息巨坊,以便于攔截的時(shí)候能直接操作此對象來執(zhí)行邏輯撬槽。 1. 解析三大注解到CacheOperation的過程是由CacheAnnotationParser完成的
  2. CacheAnnotationSource代表緩存屬性源,非常非常重要的一個(gè)概念趾撵。它提供接口方法來獲取目標(biāo)方法的CacheOperation集合侄柔。由上可知,這個(gè)具體工作是委托給CacheAnnotationParser去完成的
  3. BeanFactoryCacheOperationSourceAdvisor它代表增強(qiáng)器占调,至于需要增強(qiáng)哪些類呢暂题??究珊?就是看有沒有存在CacheOperation屬性的方法
  4. CacheInterceptor實(shí)現(xiàn)了MethodInterceptor接口薪者,在Spring AOP中實(shí)現(xiàn)對執(zhí)行方法的攔截。在調(diào)用invoke執(zhí)行目標(biāo)方法前后剿涮,通過CacheAnnotationSource獲取到方法所有的緩存操作屬性言津,從而一個(gè)個(gè)的執(zhí)行
  5. 執(zhí)行的時(shí)候,每一個(gè)CacheOperation最后被封裝成了CacheOperationContext取试,而CacheOperationContext最終通過CacheResolver解析出緩存對象Cache(可能是多個(gè))
  6. 最后最后最后悬槽,CacheInterceptor調(diào)用其父類AbstractCacheInvoker執(zhí)行對應(yīng)的doPut / doGet / doEvict / doClear 等等。(可以處理執(zhí)行異常)

CacheProxyFactoryBean:手動實(shí)現(xiàn)Cache功能

其實(shí)ProxyFactoryBean的設(shè)計(jì)模式在Spring AOP中已經(jīng)非常不陌生了:【小家Spring】面向切面編程Spring AOP創(chuàng)建代理的方式:ProxyFactoryBean瞬浓、ProxyFactory初婆、AspectJProxyFactory(JDK Proxy和CGLIB)

如下截圖,Spring內(nèi)有非常多的xxxProxyFactoryBean的實(shí)現(xiàn):

如果說把@EnableCaching稱為自動模式的話,那使用CacheProxyFactoryBean就完全是手動檔烟逊。話不多說渣窜,此處給個(gè)使用Demo就收場了:

@Configuration
public class RootConfig {

    @Bean
    public CacheProxyFactoryBean cacheProxyFactoryBean() {
        CacheProxyFactoryBean proxyFactoryBean = new CacheProxyFactoryBean();
        // 使用AnnotationCacheOperationSource來識別三大注解緩存
        proxyFactoryBean.setCacheOperationSources(new AnnotationCacheOperationSource());

        // 設(shè)置需要代理的目標(biāo)類
        CacheDemoService cacheDemoService = new CacheDemoServiceImpl();
        proxyFactoryBean.setTarget(cacheDemoService);
        //proxyFactoryBean.setProxyInterfaces();

        // 設(shè)置個(gè)性化的一些東西
        CacheManager cacheManager = new ConcurrentMapCacheManager();
        proxyFactoryBean.setCacheManager(cacheManager);
        //proxyFactoryBean.setKeyGenerator();
        //proxyFactoryBean.setCacheResolver();

        return proxyFactoryBean;
    }

}

//@Service // 因?yàn)槭褂昧薈acheProxyFactoryBean手動額皮質(zhì),此處請不要再被掃描進(jìn)去宪躯,否則容器內(nèi)就出現(xiàn)兩個(gè)這樣的Bean了
public class CacheDemoServiceImpl implements CacheDemoService {

    @Cacheable(cacheNames = "demoCache", key = "#id")
    @Override
    public Object getFromDB(Integer id) {
        System.out.println("模擬去db查詢~~~" + id);
        return "hello cache...";
    }
}

測試:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {RootConfig.class, CacheConfig.class})
public class TestSpringBean {

    @Autowired
    private CacheDemoService cacheDemoService;

    @Test
    public void test1() {
        cacheDemoService.getFromDB(1);
        cacheDemoService.getFromDB(1);
    }

}

打印結(jié)果:

模擬去db查詢~~~1</pre>

只輸出一套日志:緩存生效

此示例中@EnableCaching可不是打開狀態(tài)哦乔宿,但我們依然能夠使用手動檔讓緩存生效。 使用手動檔访雪,我們可以很方便的使用NameMatchCacheOperationSource來根據(jù)方法名匹配~~~


緩存注解使用案例

關(guān)于緩存注解的常規(guī)使用案例详瑞,我覺得本文沒有必要介紹。 接下來主要講解一些特殊的使用:

若方法返回值為null臣缀,還會緩存嗎坝橡?

比如上例返回值改為null:

@Service
public class CacheDemoServiceImpl implements CacheDemoService {

    @Cacheable(cacheNames = "demoCache", key = "#id")
    @Override
    public Object getFromDB(Integer id) {
        System.out.println("模擬去db查詢~~~" + id);
        //return "hello cache...";
        return null;
    }
}

執(zhí)行單測:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {RootConfig.class, CacheConfig.class})
public class TestSpringBean {
    @Autowired
    private CacheDemoService cacheDemoService;
    @Autowired
    private CacheManager cacheManager;

    @Test
    public void test1() {
        cacheDemoService.getFromDB(1);
        cacheDemoService.getFromDB(1);

        System.out.println("----------驗(yàn)證緩存是否生效----------");
        Cache cache = cacheManager.getCache("demoCache");
        System.out.println(cache);
        System.out.println(cache.get(1, String.class));
    }
}

結(jié)果打印:

模擬去db查詢~~~1
----------驗(yàn)證緩存是否生效----------

org.springframework.cache.concurrent.ConcurrentMapCache@15f2eda3
null

結(jié)論是:默認(rèn)情況下精置,返回值是null也是會緩存的(第二次過來就不會再查詢了)计寇。通過一個(gè)斷點(diǎn)會更清晰:

[圖片上傳失敗...(image-cf500b-1640332873234)]

但假如修改緩存注解如下:

// 注意:unless 是非的意思
@Cacheable(cacheNames = "demoCache", key = "#id",unless = "#result == null")</pre>

運(yùn)行打印如下:

模擬去db查詢~~~1
模擬去db查詢~~~1
----------驗(yàn)證緩存是否生效----------

org.springframework.cache.concurrent.ConcurrentMapCache@6a282fdd
null

查了兩次DB,證明此時(shí)返回null就不會再緩存了脂倦,unless生效番宁。

倘若修改配置如下:

 @Bean
    public CacheManager cacheManager() {
        ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
        cacheManager.setAllowNullValues(false);
        return cacheManager;
    }

運(yùn)行則報(bào)錯(cuò):

java.lang.IllegalArgumentException: Cache 'demoCache' is configured to not allow null values but null was provided

一般情況下,不建議這么設(shè)置赖阻,因?yàn)橐话愣际窃试S緩存null值的蝶押。

@Cacheable注解sync=true的效果

在多線程環(huán)境下,某些操作可能使用相同參數(shù)同步調(diào)用(相同的key)火欧。默認(rèn)情況下棋电,緩存不鎖定任何資源,可能導(dǎo)致多次計(jì)算苇侵,而違反了緩存的目的赶盔。對于這些特定的情況,屬性 sync 可以指示底層將緩存鎖住榆浓,使只有一個(gè)線程可以進(jìn)入計(jì)算招刨,而其他線程堵塞,直到返回結(jié)果更新到緩存中(Spring4.3提供的)哀军。

下面給個(gè)例子直接看看效果就成:

@Cacheable(cacheNames = "demoCache", key = "#id")

測試Demo(多線程):

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {RootConfig.class, CacheConfig.class})
public class TestSpringBean {

    @Autowired
    private CacheDemoService cacheDemoService;

    @Test
    public void test1() throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                cacheDemoService.getFromDB(1);
            }).start();
        }

        // 保證main線程不要這么快結(jié)束(否則沒有日志結(jié)果的~~~)
        TimeUnit.SECONDS.sleep(10);

    }

}

打印結(jié)果:

模擬去db查詢~~~1
模擬去db查詢~~~1
模擬去db查詢~~~1
模擬去db查詢~~~1
模擬去db查詢~~~1</pre>

打印可能5次沉眶、可能6次不確定。但是若我們sync=true了呢杉适?

@Cacheable(cacheNames = "demoCache", key = "#id", sync = true)

再次運(yùn)行測試打印結(jié)果如下:

模擬去db查詢~~~1
永遠(yuǎn)只會打印一次谎倔。 所以在高并發(fā)環(huán)境下,我個(gè)人是十分建議開啟此同步開關(guān)的猿推,至于非高并發(fā)片习,無所謂啦~

注解可重復(fù)標(biāo)注嗎捌肴?

因?yàn)樵创a都分析了,所以此處看結(jié)論即可藕咏。

@CachePut(cacheNames = "demoCache", key = "#id") // 不同的注解可以標(biāo)注多個(gè)
    //@Cacheable(cacheNames = "demoCache", key = "#id") // 相同注解標(biāo)注兩個(gè)是不行的 因?yàn)樗⒉皇茾Repeatable的
    @Cacheable(cacheNames = "demoCache", key = "#id")
    @Override
    public Object getFromDB(Integer id) {
        System.out.println("模擬去db查詢~~~" + id);
        return "hello cache...";
    }

不同的注解可以標(biāo)注多個(gè)状知,且都能生效。若需要相同注解標(biāo)注多個(gè)等更復(fù)雜的場景孽查,推薦使用@Caching注解

如何給緩存注解設(shè)置專用的key生成器饥悴、緩存管理器等等

標(biāo)準(zhǔn)寫法是這樣的:

@EnableCaching
@Configuration
public class CacheConfig implements CachingConfigurer {
    @Override
    public CacheResolver cacheResolver() {
        return null;
    }
    @Override
    public KeyGenerator keyGenerator() {
        return null;
    }
    @Override
    public CacheErrorHandler errorHandler() {
        return null;
    }
}

實(shí)現(xiàn)對應(yīng)的方法,可以new一個(gè)對象返回盲再,也可以用容器內(nèi)的西设。

大多數(shù)情況下我們都不需要特別的指定緩存注解使用的管理器,因?yàn)樗约簳ト萜骼镎摇?但是答朋,但是贷揽,但是當(dāng)你使用了多套緩存時(shí),我還是建議顯示的指定的梦碗。

總結(jié)

本篇文章相對較長禽绪,因?yàn)閺脑硖幧疃确治隽?code>Spring Cache的執(zhí)行過程,希望能幫助到大家做到心里有數(shù)洪规,從而更加健康的書寫代碼和擴(kuò)展功能印屁。

關(guān)于緩存注解這塊,我相信很多人都有一個(gè)痛點(diǎn):注解并不支持設(shè)置過期時(shí)間淹冰。其實(shí)我想說,如果你看了上篇文章就知道巨柒,這個(gè)Spring它做不了樱拴,因?yàn)樗]有對Expire進(jìn)行抽象。 但是Spring做不了不代表我們自己做不了洋满,因此有興趣的同學(xué)可以在此基礎(chǔ)上晶乔,擴(kuò)展出可以自定義超時(shí)時(shí)間的能力~~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市牺勾,隨后出現(xiàn)的幾起案子正罢,更是在濱河造成了極大的恐慌,老刑警劉巖驻民,帶你破解...
    沈念sama閱讀 221,888評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件翻具,死亡現(xiàn)場離奇詭異,居然都是意外死亡回还,警方通過查閱死者的電腦和手機(jī)裆泳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,677評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來柠硕,“玉大人工禾,你說我怎么就攤上這事运提。” “怎么了闻葵?”我有些...
    開封第一講書人閱讀 168,386評論 0 360
  • 文/不壞的土叔 我叫張陵民泵,是天一觀的道長。 經(jīng)常有香客問我槽畔,道長栈妆,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,726評論 1 297
  • 正文 為了忘掉前任竟痰,我火速辦了婚禮签钩,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘坏快。我一直安慰自己铅檩,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,729評論 6 397
  • 文/花漫 我一把揭開白布莽鸿。 她就那樣靜靜地躺著昧旨,像睡著了一般。 火紅的嫁衣襯著肌膚如雪祥得。 梳的紋絲不亂的頭發(fā)上兔沃,一...
    開封第一講書人閱讀 52,337評論 1 310
  • 那天,我揣著相機(jī)與錄音级及,去河邊找鬼乒疏。 笑死,一個(gè)胖子當(dāng)著我的面吹牛饮焦,可吹牛的內(nèi)容都是我干的怕吴。 我是一名探鬼主播,決...
    沈念sama閱讀 40,902評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼县踢,長吁一口氣:“原來是場噩夢啊……” “哼转绷!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起硼啤,我...
    開封第一講書人閱讀 39,807評論 0 276
  • 序言:老撾萬榮一對情侶失蹤议经,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后谴返,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體煞肾,經(jīng)...
    沈念sama閱讀 46,349評論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,439評論 3 340
  • 正文 我和宋清朗相戀三年嗓袱,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了扯旷。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,567評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡索抓,死狀恐怖钧忽,靈堂內(nèi)的尸體忽然破棺而出毯炮,到底是詐尸還是另有隱情,我是刑警寧澤耸黑,帶...
    沈念sama閱讀 36,242評論 5 350
  • 正文 年R本政府宣布桃煎,位于F島的核電站,受9級特大地震影響大刊,放射性物質(zhì)發(fā)生泄漏为迈。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,933評論 3 334
  • 文/蒙蒙 一缺菌、第九天 我趴在偏房一處隱蔽的房頂上張望葫辐。 院中可真熱鬧,春花似錦伴郁、人聲如沸耿战。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,420評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽剂陡。三九已至,卻和暖如春狐胎,著一層夾襖步出監(jiān)牢的瞬間鸭栖,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,531評論 1 272
  • 我被黑心中介騙來泰國打工握巢, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留晕鹊,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,995評論 3 377
  • 正文 我出身青樓暴浦,卻偏偏與公主長得像溅话,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子肉渴,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,585評論 2 359

推薦閱讀更多精彩內(nèi)容