Spring cache源碼分析

Spring cache是一個(gè)緩存API層油狂,封裝了對多種緩存的通用操作赶盔,可以借助注解方便地為程序添加緩存功能企锌。
常見的注解有@Cacheable、@CachePut于未、@CacheEvict撕攒,有沒有想過背后的原理是什么?樓主帶著疑問烘浦,閱讀完Spring cache的源碼后抖坪,做一個(gè)簡要總結(jié)。
先說結(jié)論闷叉,核心邏輯在CacheAspectSupport類擦俐,封裝了所有的緩存操作的主體邏輯,下面詳細(xì)介紹片习。

題外話:如何閱讀開源代碼捌肴?

有2種方法蹬叭,可以結(jié)合起來使用:

  • 靜態(tài)代碼閱讀:查找關(guān)鍵類藕咏、方法的usage之處,熟練使用find usages功能秽五,找到所有相關(guān)的類孽查、方法,靜態(tài)分析核心邏輯的執(zhí)行過程坦喘,一步步追根問底盲再,直至建立全貌
  • 運(yùn)行時(shí)debug:在關(guān)鍵方法上加上斷點(diǎn),并且寫一個(gè)單元測試調(diào)用類庫/框架瓣铣,熟練使用step into/step over/resume來動(dòng)態(tài)分析代碼的執(zhí)行過程

核心類圖

spring-cache.png

如圖所示答朋,可以分成以下幾類class:

  • Cache、CacheManager:Cache抽象了緩存的通用操作棠笑,如get梦碗、put,而CacheManager是Cache的集合,之所以需要多個(gè)Cache對象洪规,是因?yàn)樾枰喾N緩存失效時(shí)間印屁、緩存條目上限等
  • CacheInterceptor、CacheAspectSupport斩例、AbstractCacheInvoker:CacheInterceptor是一個(gè)AOP方法攔截器雄人,在方法前后做額外的邏輯,也即查詢緩存念赶、寫入緩存等础钠,它繼承了CacheAspectSupport(緩存操作的主體邏輯)、AbstractCacheInvoker(封裝了對Cache的讀寫)
  • CacheOperation叉谜、AnnotationCacheOperationSource珍坊、SpringCacheAnnotationParser:CacheOperation定義了緩存操作的緩存名字、緩存key正罢、緩存條件condition阵漏、CacheManager等,AnnotationCacheOperationSource是一個(gè)獲取緩存注解對應(yīng)CacheOperation的類翻具,而SpringCacheAnnotationParser是真正解析注解的類履怯,解析后會(huì)封裝成CacheOperation集合供AnnotationCacheOperationSource查找

源碼分析(帶注釋解釋)

下面對Spring cache源碼做分析,帶注釋解釋裆泳,只摘錄核心代碼片段叹洲。

1、解析注解

首先看看注解是如何解析的工禾。注解只是一個(gè)標(biāo)記运提,要讓它真正工作起來,需要對注解做解析操作闻葵,并且還要有對應(yīng)的實(shí)際邏輯民泵。

SpringCacheAnnotationParser:負(fù)責(zé)解析注解,返回CacheOperation集合

public class SpringCacheAnnotationParser implements CacheAnnotationParser, Serializable {

        // 解析類級別的緩存注解
    @Override
    public Collection<CacheOperation> parseCacheAnnotations(Class<?> type) {
        DefaultCacheConfig defaultConfig = getDefaultCacheConfig(type);
        return parseCacheAnnotations(defaultConfig, type);
    }

        // 解析方法級別的緩存注解
    @Override
    public Collection<CacheOperation> parseCacheAnnotations(Method method) {
        DefaultCacheConfig defaultConfig = getDefaultCacheConfig(method.getDeclaringClass());
        return parseCacheAnnotations(defaultConfig, method);
    }

        // 解析緩存注解
    private Collection<CacheOperation> parseCacheAnnotations(DefaultCacheConfig cachingConfig, AnnotatedElement ae) {
        Collection<CacheOperation> ops = null;

                // 解析@Cacheable注解
        Collection<Cacheable> cacheables = AnnotatedElementUtils.getAllMergedAnnotations(ae, Cacheable.class);
        if (!cacheables.isEmpty()) {
            ops = lazyInit(ops);
            for (Cacheable cacheable : cacheables) {
                ops.add(parseCacheableAnnotation(ae, cachingConfig, cacheable));
            }
        }

                // 解析@CacheEvict注解
        Collection<CacheEvict> evicts = AnnotatedElementUtils.getAllMergedAnnotations(ae, CacheEvict.class);
        if (!evicts.isEmpty()) {
            ops = lazyInit(ops);
            for (CacheEvict evict : evicts) {
                ops.add(parseEvictAnnotation(ae, cachingConfig, evict));
            }
        }

                // 解析@CachePut注解
        Collection<CachePut> puts = AnnotatedElementUtils.getAllMergedAnnotations(ae, CachePut.class);
        if (!puts.isEmpty()) {
            ops = lazyInit(ops);
            for (CachePut put : puts) {
                ops.add(parsePutAnnotation(ae, cachingConfig, put));
            }
        }

                // 解析@Caching注解
        Collection<Caching> cachings = AnnotatedElementUtils.getAllMergedAnnotations(ae, Caching.class);
        if (!cachings.isEmpty()) {
            ops = lazyInit(ops);
            for (Caching caching : cachings) {
                Collection<CacheOperation> cachingOps = parseCachingAnnotation(ae, cachingConfig, caching);
                if (cachingOps != null) {
                    ops.addAll(cachingOps);
                }
            }
        }

        return ops;
    }

AnnotationCacheOperationSource:調(diào)用SpringCacheAnnotationParser獲取注解對應(yīng)CacheOperation

public class AnnotationCacheOperationSource extends AbstractFallbackCacheOperationSource implements Serializable {

        // 查找類級別的CacheOperation列表
    @Override
    protected Collection<CacheOperation> findCacheOperations(final Class<?> clazz) {
        return determineCacheOperations(new CacheOperationProvider() {
            @Override
            public Collection<CacheOperation> getCacheOperations(CacheAnnotationParser parser) {
                return parser.parseCacheAnnotations(clazz);
            }
        });

    }

        // 查找方法級別的CacheOperation列表
    @Override
    protected Collection<CacheOperation> findCacheOperations(final Method method) {
        return determineCacheOperations(new CacheOperationProvider() {
            @Override
            public Collection<CacheOperation> getCacheOperations(CacheAnnotationParser parser) {
                return parser.parseCacheAnnotations(method);
            }
        });
    }

}

AbstractFallbackCacheOperationSource:AnnotationCacheOperationSource的父類槽畔,實(shí)現(xiàn)了獲取CacheOperation的通用邏輯

public abstract class AbstractFallbackCacheOperationSource implements CacheOperationSource {

    /**
     * Cache of CacheOperations, keyed by method on a specific target class.
     * <p>As this base class is not marked Serializable, the cache will be recreated
     * after serialization - provided that the concrete subclass is Serializable.
     */
    private final Map<Object, Collection<CacheOperation>> attributeCache =
            new ConcurrentHashMap<Object, Collection<CacheOperation>>(1024);


    // 根據(jù)Method栈妆、Class反射信息,獲取對應(yīng)的CacheOperation列表
    @Override
    public Collection<CacheOperation> getCacheOperations(Method method, Class<?> targetClass) {
        if (method.getDeclaringClass() == Object.class) {
            return null;
        }

        Object cacheKey = getCacheKey(method, targetClass);
        Collection<CacheOperation> cached = this.attributeCache.get(cacheKey);

                // 因解析反射信息較耗時(shí)厢钧,所以用map緩存鳞尔,避免重復(fù)計(jì)算
                // 如在map里已記錄,直接返回
        if (cached != null) {
            return (cached != NULL_CACHING_ATTRIBUTE ? cached : null);
        }
                // 否則做一次計(jì)算早直,然后寫入map
        else {
            Collection<CacheOperation> cacheOps = computeCacheOperations(method, targetClass);
            if (cacheOps != null) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Adding cacheable method '" + method.getName() + "' with attribute: " + cacheOps);
                }
                this.attributeCache.put(cacheKey, cacheOps);
            }
            else {
                this.attributeCache.put(cacheKey, NULL_CACHING_ATTRIBUTE);
            }
            return cacheOps;
        }
    }

        // 計(jì)算緩存操作列表寥假,優(yōu)先用target代理類的方法上的注解,如果不存在則其次用target代理類霞扬,再次用原始類的方法糕韧,最后用原始類
    private Collection<CacheOperation> computeCacheOperations(Method method, Class<?> targetClass) {
        // Don't allow no-public methods as required.
        if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
            return null;
        }

        // The method may be on an interface, but we need attributes from the target class.
        // If the target class is null, the method will be unchanged.
        Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
        // If we are dealing with method with generic parameters, find the original method.
        specificMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);

                // 調(diào)用findCacheOperations(由子類AnnotationCacheOperationSource實(shí)現(xiàn))拾给,最終通過SpringCacheAnnotationParser來解析
        // First try is the method in the target class.
        Collection<CacheOperation> opDef = findCacheOperations(specificMethod);
        if (opDef != null) {
            return opDef;
        }

        // Second try is the caching operation on the target class.
        opDef = findCacheOperations(specificMethod.getDeclaringClass());
        if (opDef != null && ClassUtils.isUserLevelMethod(method)) {
            return opDef;
        }

        if (specificMethod != method) {
            // Fallback is to look at the original method.
            opDef = findCacheOperations(method);
            if (opDef != null) {
                return opDef;
            }
            // Last fallback is the class of the original method.
            opDef = findCacheOperations(method.getDeclaringClass());
            if (opDef != null && ClassUtils.isUserLevelMethod(method)) {
                return opDef;
            }
        }

        return null;
    }

2、邏輯執(zhí)行

以@Cacheable背后的邏輯為例兔沃。預(yù)期是先查緩存蒋得,如果緩存命中了就直接使用緩存值,否則執(zhí)行業(yè)務(wù)邏輯乒疏,并把結(jié)果寫入緩存额衙。

ProxyCachingConfiguration:是一個(gè)配置類,用于生成CacheInterceptor類和CacheOperationSource類的Spring bean

CacheInterceptor:是一個(gè)AOP方法攔截器怕吴,它通過CacheOperationSource獲取第1步解析注解的CacheOperation結(jié)果(如緩存名字窍侧、緩存key、condition條件)转绷,本質(zhì)上是攔截原始方法的執(zhí)行伟件,在之前、之后增加邏輯

// 核心類议经,緩存攔截器
public class CacheInterceptor extends CacheAspectSupport implements MethodInterceptor, Serializable {

        // 攔截原始方法的執(zhí)行斧账,在之前、之后增加邏輯
    @Override
    public Object invoke(final MethodInvocation invocation) throws Throwable {
        Method method = invocation.getMethod();

                // 封裝原始方法的執(zhí)行到一個(gè)回調(diào)接口煞肾,便于后續(xù)調(diào)用
        CacheOperationInvoker aopAllianceInvoker = new CacheOperationInvoker() {
            @Override
            public Object invoke() {
                try {
                                        // 原始方法的執(zhí)行
                    return invocation.proceed();
                }
                catch (Throwable ex) {
                    throw new ThrowableWrapper(ex);
                }
            }
        };

        try {
                        // 調(diào)用父類CacheAspectSupport的方法
            return execute(aopAllianceInvoker, invocation.getThis(), method, invocation.getArguments());
        }
        catch (CacheOperationInvoker.ThrowableWrapper th) {
            throw th.getOriginal();
        }
    }

}

CacheAspectSupport:緩存切面支持類咧织,是CacheInterceptor的父類,封裝了所有的緩存操作的主體邏輯

主要流程如下:

  1. 通過CacheOperationSource籍救,獲取所有的CacheOperation列表
  2. 如果有@CacheEvict注解习绢、并且標(biāo)記為在調(diào)用前執(zhí)行,則做刪除/清空緩存的操作
  3. 如果有@Cacheable注解蝙昙,查詢緩存
  4. 如果緩存未命中(查詢結(jié)果為null)闪萄,則新增到cachePutRequests,后續(xù)執(zhí)行原始方法后會(huì)寫入緩存
  5. 緩存命中時(shí)奇颠,使用緩存值作為結(jié)果败去;緩存未命中、或有@CachePut注解時(shí)大刊,需要調(diào)用原始方法为迈,使用原始方法的返回值作為結(jié)果
  6. 如果有@CachePut注解,則新增到cachePutRequests
  7. 如果緩存未命中缺菌,則把查詢結(jié)果值寫入緩存;如果有@CachePut注解搜锰,也把方法執(zhí)行結(jié)果寫入緩存
  8. 如果有@CacheEvict注解伴郁、并且標(biāo)記為在調(diào)用后執(zhí)行,則做刪除/清空緩存的操作
// 核心類蛋叼,緩存切面支持類焊傅,封裝了所有的緩存操作的主體邏輯
public abstract class CacheAspectSupport extends AbstractCacheInvoker
        implements BeanFactoryAware, InitializingBean, SmartInitializingSingleton {

        // CacheInterceptor調(diào)父類的該方法
    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剂陡,獲取所有的CacheOperation列表
            Collection<CacheOperation> operations = getCacheOperationSource().getCacheOperations(method, targetClass);
            if (!CollectionUtils.isEmpty(operations)) {
                                // 繼續(xù)調(diào)一個(gè)private的execute方法執(zhí)行
                return execute(invoker, method, new CacheOperationContexts(operations, method, args, target, targetClass));
            }
        }

                // 如果spring bean未初始化完成,則直接調(diào)用原始方法狐胎。相當(dāng)于原始方法沒有緩存功能鸭栖。
        return invoker.invoke();
    }

        private的execute方法
    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);
            }
        }

                // 如果有@CacheEvict注解、并且標(biāo)記為在調(diào)用前執(zhí)行握巢,則做刪除/清空緩存的操作
        // Process any early evictions
        processCacheEvicts(contexts.get(CacheEvictOperation.class), true,
                CacheOperationExpressionEvaluator.NO_RESULT);

                // 如果有@Cacheable注解晕鹊,查詢緩存
        // Check if we have a cached item matching the conditions
        Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));

                // 如果緩存未命中(查詢結(jié)果為null),則新增到cachePutRequests暴浦,后續(xù)執(zhí)行原始方法后會(huì)寫入緩存
        // Collect puts from any @Cacheable miss, if no cached item is found
        List<CachePutRequest> cachePutRequests = new LinkedList<CachePutRequest>();
        if (cacheHit == null) {
            collectPutRequests(contexts.get(CacheableOperation.class),
                    CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);
        }

        Object cacheValue;
        Object returnValue;

        if (cacheHit != null && cachePutRequests.isEmpty() && !hasCachePut(contexts)) {
                        // 緩存命中的情況溅话,使用緩存值作為結(jié)果
            // If there are no put requests, just use the cache hit
            cacheValue = cacheHit.get();
            returnValue = wrapCacheValue(method, cacheValue);
        }
        else {
                        // 緩存未命中、或有@CachePut注解的情況歌焦,需要調(diào)用原始方法
            // Invoke the method if we don't have a cache hit
                        // 調(diào)用原始方法飞几,得到結(jié)果值
            returnValue = invokeOperation(invoker);
            cacheValue = unwrapReturnValue(returnValue);
        }

                // 如果有@CachePut注解,則新增到cachePutRequests
        // Collect any explicit @CachePuts
        collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);

                // 如果緩存未命中独撇,則把查詢結(jié)果值寫入緩存屑墨;如果有@CachePut注解,也把方法執(zhí)行結(jié)果寫入緩存
        // Process any collected put requests, either from @CachePut or a @Cacheable miss
        for (CachePutRequest cachePutRequest : cachePutRequests) {
            cachePutRequest.apply(cacheValue);
        }

                // 如果有@CacheEvict注解纷铣、并且標(biāo)記為在調(diào)用后執(zhí)行绪钥,則做刪除/清空緩存的操作
        // Process any late evictions
        processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);

        return returnValue;
    }

    private Cache.ValueWrapper findCachedItem(Collection<CacheOperationContext> contexts) {
        Object result = CacheOperationExpressionEvaluator.NO_RESULT;
        for (CacheOperationContext context : contexts) {
                        // 如果滿足condition條件,才查詢緩存
            if (isConditionPassing(context, result)) {
                                // 生成緩存key关炼,如果注解中指定了key程腹,則按照Spring表達(dá)式解析,否則使用KeyGenerator類生成
                Object key = generateKey(context, result);
                                // 根據(jù)緩存key儒拂,查詢緩存值
                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()) {
                        // 調(diào)用父類AbstractCacheInvoker的doGet方法寸潦,查詢緩存
            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;
    }

AbstractCacheInvoker:CacheAspectSupport的父類,封裝了最終查詢Cache接口的邏輯

public abstract class AbstractCacheInvoker {
        // 最終查詢緩存的方法
    protected Cache.ValueWrapper doGet(Cache cache, Object key) {
        try {
                        // 調(diào)用Spring Cache接口的查詢方法
            return cache.get(key);
        }
        catch (RuntimeException ex) {
            getErrorHandler().handleCacheGetError(ex, cache, key);
            return null;  // If the exception is handled, return a cache miss
        }
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末社痛,一起剝皮案震驚了整個(gè)濱河市见转,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蒜哀,老刑警劉巖斩箫,帶你破解...
    沈念sama閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異撵儿,居然都是意外死亡乘客,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進(jìn)店門淀歇,熙熙樓的掌柜王于貴愁眉苦臉地迎上來易核,“玉大人,你說我怎么就攤上這事浪默∧抵保” “怎么了缀匕?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長碰逸。 經(jīng)常有香客問我乡小,道長,這世上最難降的妖魔是什么饵史? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任满钟,我火速辦了婚禮,結(jié)果婚禮上约急,老公的妹妹穿的比我還像新娘零远。我一直安慰自己,他們只是感情好厌蔽,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布牵辣。 她就那樣靜靜地躺著,像睡著了一般奴饮。 火紅的嫁衣襯著肌膚如雪纬向。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天戴卜,我揣著相機(jī)與錄音逾条,去河邊找鬼。 笑死投剥,一個(gè)胖子當(dāng)著我的面吹牛师脂,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播江锨,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼吃警,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了啄育?” 一聲冷哼從身側(cè)響起酌心,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎挑豌,沒想到半個(gè)月后安券,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡氓英,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年侯勉,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片债蓝。...
    茶點(diǎn)故事閱讀 40,503評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡壳鹤,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出饰迹,到底是詐尸還是另有隱情芳誓,我是刑警寧澤,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布啊鸭,位于F島的核電站锹淌,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏赠制。R本人自食惡果不足惜赂摆,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望钟些。 院中可真熱鬧烟号,春花似錦、人聲如沸政恍。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽篙耗。三九已至迫筑,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間宗弯,已是汗流浹背脯燃。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蒙保,地道東北人辕棚。 一個(gè)月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像邓厕,于是被迫代替她去往敵國和親逝嚎。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,512評論 2 359

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