【每天學(xué)點(diǎn)Spring】Spring Cache源碼分析

關(guān)于Spring Cache的介紹,參考:http://www.reibang.com/p/51389fbaf411,本文主要講關(guān)于注解@Cacheable, @CachePut, @CacheEvict背后是怎樣實(shí)現(xiàn)的瓤荔。

1. 相關(guān)類介紹

  • 1.1 Cache, CacheManager: 這兩個(gè)接口是Spring Cache的核心接口洞就。

    • Cache接口主要定義了cache的操作,如get, put等轴脐。
    • CacheManager接口定義的是cache的集合,為什么會有多個(gè)Cache對象主要是每個(gè)cache可能會有自己的過期時(shí)間、Cache的元素需要被限制等原因匀哄。
  • 1.2 CacheInterceptor, CacheAspectSupport, AbstractCacheInvoker:標(biāo)記Cache注解的方法被調(diào)用時(shí)的AOP相關(guān)的類。

    • CacheInterceptor繼承了AOP的MethodInterceptor雏蛮,其中方法invoke(invocation)方法相當(dāng)于@Aspect中的Around涎嚼,可以在目標(biāo)方法之前和之后寫一些額外的邏輯,比如從cache中查詢數(shù)據(jù)挑秉,或是寫數(shù)據(jù)至cache等法梯。
    • CacheAspectSupport:真正提供cache操作的類,被上述的CacheInterceptor繼承犀概。
    • AbstractCacheInvoker:抽象類立哑,被上述的CacheAspectSupport繼續(xù),提供讀cache姻灶、寫cache等操作铛绰,如方法:doGet(cache, key) , doPut(cache, key, result)等。
  • 1.3 CacheOperation, AnnotationCacheOperationSource, SpringCacheAnnotationParser:主要是為annotation做解析的木蹬。

    • CacheOperation是個(gè)抽象類至耻,定義了cache的name\key\condition等,繼承它的主要也是上述三個(gè)注解的operation镊叁,分別是:CacheableOperation, CachePutOperation, CacheEvictOperation尘颓。
    • AnnotationCacheOperationSource類主要是讀取Spring Cache的三個(gè)annotation(@Cacheable, @CachePut, @CacheEvict),轉(zhuǎn)成CacheOperation晦譬。
    • SpringCacheAnnotationParser類則負(fù)責(zé)具體的annotation parse邏輯疤苹。

2. Annotation解析

2.1 SpringCacheAnnotationParser

在1.3中介紹,SpringCacheAnnotationParser類負(fù)責(zé)具體的annotation parse邏輯敛腌。

public class SpringCacheAnnotationParser implements CacheAnnotationParser, Serializable {
    @Override
    @Nullable
    public Collection<CacheOperation> parseCacheAnnotations(Method method) {
        DefaultCacheConfig defaultConfig = new DefaultCacheConfig(method.getDeclaringClass());
        return parseCacheAnnotations(defaultConfig, method);
    }

    @Nullable
    private Collection<CacheOperation> parseCacheAnnotations(DefaultCacheConfig cachingConfig, AnnotatedElement ae) {
        Collection<CacheOperation> ops = parseCacheAnnotations(cachingConfig, ae, false);
        if (ops != null && ops.size() > 1) {
            // More than one operation found -> local declarations override interface-declared ones...
            Collection<CacheOperation> localOps = parseCacheAnnotations(cachingConfig, ae, true);
            if (localOps != null) {
                return localOps;
            }
        }
        return ops;
    }

    @Nullable
    private Collection<CacheOperation> parseCacheAnnotations(
            DefaultCacheConfig cachingConfig, AnnotatedElement ae, boolean localOnly) {

        Collection<? extends Annotation> anns = (localOnly ?
                AnnotatedElementUtils.getAllMergedAnnotations(ae, CACHE_OPERATION_ANNOTATIONS) :
                AnnotatedElementUtils.findAllMergedAnnotations(ae, CACHE_OPERATION_ANNOTATIONS));
        if (anns.isEmpty()) {
            return null;
        }

        final Collection<CacheOperation> ops = new ArrayList<>(1);
        anns.stream().filter(ann -> ann instanceof Cacheable).forEach(
                ann -> ops.add(parseCacheableAnnotation(ae, cachingConfig, (Cacheable) ann)));
        anns.stream().filter(ann -> ann instanceof CacheEvict).forEach(
                ann -> ops.add(parseEvictAnnotation(ae, cachingConfig, (CacheEvict) ann)));
        anns.stream().filter(ann -> ann instanceof CachePut).forEach(
                ann -> ops.add(parsePutAnnotation(ae, cachingConfig, (CachePut) ann)));
        anns.stream().filter(ann -> ann instanceof Caching).forEach(
                ann -> parseCachingAnnotation(ae, cachingConfig, (Caching) ann, ops));
        return ops;
    }

    // 具體的parse邏輯:
    private CacheableOperation parseCacheableAnnotation(
            AnnotatedElement ae, DefaultCacheConfig defaultConfig, Cacheable cacheable) {

        CacheableOperation.Builder builder = new CacheableOperation.Builder();

        builder.setName(ae.toString());
        builder.setCacheNames(cacheable.cacheNames());
        builder.setCondition(cacheable.condition());
        builder.setUnless(cacheable.unless());
        builder.setKey(cacheable.key());
        builder.setKeyGenerator(cacheable.keyGenerator());
        builder.setCacheManager(cacheable.cacheManager());
        builder.setCacheResolver(cacheable.cacheResolver());
        builder.setSync(cacheable.sync());

        defaultConfig.applyDefault(builder);
        CacheableOperation op = builder.build();
        validateCacheOperation(ae, op);

        return op;
    }
}

比如我CourseService有個(gè)方法getById(int)上有@Cacheable注解:

@Cacheable(cacheNames = "courses")
public Course getById(int id) {...}

Debug上述parseCacheAnnotations()方法的結(jié)果卧土,可以看到CourseService上的注解,最終會被轉(zhuǎn)成CacheOperation對象像樊,這個(gè)對象在#1.3有介紹過尤莺,主要是存放了注解的name, key, condition等信息:

image.png

【小結(jié)】SpringCacheAnnotationParser類圖:

image.png

2.2 AnnotationCacheOperationSource

那么是誰在調(diào)用上述#2.1的parseCacheAnnotations()方法?

AnnotationCacheOperationSource在構(gòu)造方法中new了上述的SpringCacheAnnotationParser生棍,并且在findCacheOperations(method)方法中調(diào)用了#2.1的parse方法:

public class AnnotationCacheOperationSource extends AbstractFallbackCacheOperationSource implements Serializable {

    public AnnotationCacheOperationSource(boolean publicMethodsOnly) {
        this.publicMethodsOnly = publicMethodsOnly;
        this.annotationParsers = Collections.singleton(new SpringCacheAnnotationParser());
    }

    @Override
    @Nullable
    protected Collection<CacheOperation> findCacheOperations(Method method) {
        return determineCacheOperations(parser -> parser.parseCacheAnnotations(method));
    }
}

那么颤霎,findCacheOperations(method)是誰在調(diào)用呢?
AnnotationCacheOperationSource繼續(xù)了抽象類AbstractFallbackCacheOperationSource,在抽象類中:

public abstract class AbstractFallbackCacheOperationSource implements CacheOperationSource {
    @Override
    @Nullable
    public Collection<CacheOperation> getCacheOperations(Method method, @Nullable Class<?> targetClass) {
        if (method.getDeclaringClass() == Object.class) {
            return null;
        }

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

        if (cached != null) {
            return (cached != NULL_CACHING_ATTRIBUTE ? cached : null);
        }
        else {
            Collection<CacheOperation> cacheOps = computeCacheOperations(method, targetClass);
            if (cacheOps != null) {
                if (logger.isTraceEnabled()) {
                    logger.trace("Adding cacheable method '" + method.getName() + "' with attribute: " + cacheOps);
                }
                this.attributeCache.put(cacheKey, cacheOps);
            }
            else {
                this.attributeCache.put(cacheKey, NULL_CACHING_ATTRIBUTE);
            }
            return cacheOps;
        }
    }

    @Nullable
    private Collection<CacheOperation> computeCacheOperations(Method method, @Nullable Class<?> targetClass) {
        // Don't allow non-public methods, as configured.
        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 = AopUtils.getMostSpecificMethod(method, targetClass);

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

    @Nullable
    protected abstract Collection<CacheOperation> findCacheOperations(Class<?> clazz);

    @Nullable
    protected abstract Collection<CacheOperation> findCacheOperations(Method method);
}

抽象類中:方法getCacheOperations(method, targetClass)
調(diào)用了 --> computeCacheOperations(method, targetClass)友酱,調(diào)用了抽象方法 --> findCacheOperations(method)

而方法getCacheOperations(method, targetClass)則在第3章介紹的類中被調(diào)用晴音。

【小結(jié)】打星的方法即是annotation parse中最終會被外部調(diào)用的方法:
image.png

3. 目標(biāo)方法被執(zhí)行時(shí)的邏輯

本章節(jié)圍繞CacheInterceptor相關(guān)類展開。

@Cacheable的定義是缔杉,在執(zhí)行目標(biāo)方法前锤躁,先查一遍緩存,如果緩存里有或详,那么就返回結(jié)果系羞,如果沒有,那么執(zhí)行目標(biāo)方法鸭叙,并將結(jié)果寫入緩存觉啊。

不難想象,用切面實(shí)現(xiàn)比較容易沈贝,即目標(biāo)方法前做一些邏輯(查緩存杠人,返回結(jié)果)宋下,目標(biāo)方法后做一些邏輯(將結(jié)果寫入緩存)嗡善。

創(chuàng)建CacheInterceptor

由類ProxyCachingConfiguration負(fù)責(zé)創(chuàng)建CacheInterceptor,該類位于spring-context包中,這個(gè)類本身是個(gè)Configuration枝笨,最終由@EnableCaching負(fù)責(zé)激活横浑。

關(guān)于@EnableCache相關(guān)洒缀,具體參考:http://it.cha138.com/nginx/show-291168.html

ProxyCachingConfiguration具體源碼,具體的解釋在源碼下面:

public class ProxyCachingConfiguration extends AbstractCachingConfiguration {

    @Bean
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    public CacheOperationSource cacheOperationSource() {
        return new AnnotationCacheOperationSource();
    }
    
    @Bean
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    public CacheInterceptor cacheInterceptor(CacheOperationSource cacheOperationSource) {
        CacheInterceptor interceptor = new CacheInterceptor();
        interceptor.configure(this.errorHandler, this.keyGenerator, this.cacheResolver, this.cacheManager);
        interceptor.setCacheOperationSource(cacheOperationSource);
        return interceptor;
    }

    @Bean(name = CacheManagementConfigUtils.CACHE_ADVISOR_BEAN_NAME)
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    public BeanFactoryCacheOperationSourceAdvisor cacheAdvisor(
            CacheOperationSource cacheOperationSource, CacheInterceptor cacheInterceptor) {

        BeanFactoryCacheOperationSourceAdvisor advisor = new BeanFactoryCacheOperationSourceAdvisor();
        advisor.setCacheOperationSource(cacheOperationSource);
        advisor.setAdvice(cacheInterceptor);
        if (this.enableCaching != null) {
            advisor.setOrder(this.enableCaching.<Integer>getNumber("order"));
        }
        return advisor;
    }
}

上述定義的ProxyCachingConfiguration砰奕,主要做了3件事:

  • a. 創(chuàng)建了我們上述第#2章的CacheOperationSource接口實(shí)現(xiàn)胸哥。
  • b. 創(chuàng)建了CacheInterceptor银酬,并將a的值set到該類中。CacheInterceptor實(shí)現(xiàn)了MethodInterceptor接口李破,定義了切面方法的邏輯嗤攻。
  • c. 定義Advisor承粤,并定義切點(diǎn)(PointCut)辛臊,將和上述b綁定在一起淹遵。這里定義的切點(diǎn)類為CacheOperationSourcePointcut,其中重要的方法(即每次匹配要用到的邏輯):matches()辐真,這里用到了第2章的annotation parse方法
abstract class CacheOperationSourcePointcut extends StaticMethodMatcherPointcut implements Serializable {
    @Override
    public boolean matches(Method method, Class<?> targetClass) {
        CacheOperationSource cas = getCacheOperationSource();
        return (cas != null && !CollectionUtils.isEmpty(cas.getCacheOperations(method, targetClass)));
    }
}

【小結(jié)】左邊是第2章的類圖撩轰,右邊是本章的類圖:
image.png
3.2 詳解CacheInterceptorinvoke(invocation)方法

CacheInterceptor類源碼:

  • 首先它實(shí)現(xiàn)了aop中的接口MethodInterceptor木柬。
  • 其次眉枕,繼承了CacheAspectSupport類拓哟,該類提供的execute()方法即為具體的邏輯疮蹦。
public class CacheInterceptor extends CacheAspectSupport implements MethodInterceptor, Serializable {

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

        CacheOperationInvoker aopAllianceInvoker = () -> {
            try {
                return invocation.proceed();
            }
            catch (Throwable ex) {
                throw new CacheOperationInvoker.ThrowableWrapper(ex);
            }
        };

        Object target = invocation.getThis();
        Assert.state(target != null, "Target must not be null");
        try {
            return execute(aopAllianceInvoker, target, method, invocation.getArguments());
        }
        catch (CacheOperationInvoker.ThrowableWrapper th) {
            throw th.getOriginal();
        }
    }
}

具體看CacheAspectSupport的execute()方法:

  • 先拿到targetClasss
  • 再拿到CacheOperationSource,這個(gè)類hold了注解@Cacheable, @CachePut, @CacheEvict的定義囊陡,即:Collection<CacheOperation> operations撞反。
  • 調(diào)用第二個(gè)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();
    }

第二個(gè)execute()方法:

  • Synchronized case略過续扔,看常規(guī)的case雕拼。
  • 當(dāng)注解為@Cacheable時(shí)胡诗,查詢cacheHit肴颊,不為空表示有緩存存在祟身。如果為空担神,需要放入cache待寫的list中(即:cachePutRequests)躬窜。
  • 然后就是進(jìn)行判斷:
    • 如果cacheHit不為空并且不是@CachePut操作这溅,那么從緩存里拿到cacheValue胳徽。
    • 如果cacheHit為空虎锚,則執(zhí)行真正的目標(biāo)方法。
  • 如果為@CachePut操作薄货,那么也需要放入cache待寫的list中(即:cachePutRequests)翁都。
  • 讀取cachePutRequests,執(zhí)行cache寫操作谅猾。
  • @CacheEvict注解進(jìn)行清理cache操作柄慰。
    @Nullable
    private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
        // Special handling of synchronized invocation
        if (contexts.isSynchronized()) {
            // 略
        }

        // Process any early evictions
        processCacheEvicts(contexts.get(CacheEvictOperation.class), true,
                CacheOperationExpressionEvaluator.NO_RESULT);

        // Check if we have a cached item matching the conditions
        Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));

        // Collect puts from any @Cacheable miss, if no cached item is found
        List<CachePutRequest> cachePutRequests = new ArrayList<>();
        if (cacheHit == null) {
            collectPutRequests(contexts.get(CacheableOperation.class),
                    CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);
        }

        Object cacheValue;
        Object returnValue;

        if (cacheHit != null && !hasCachePut(contexts)) {
            // If there are no put requests, just use the cache hit
            cacheValue = cacheHit.get();
            returnValue = wrapCacheValue(method, cacheValue);
        }
        else {
            // Invoke the method if we don't have a cache hit
            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;
    }

上述CacheAspectSupport類,execute()方法中的從緩存里拿數(shù)據(jù)税娜,用的方法是:findCachedItem(contexts)坐搔,具體源碼:
可以看到findCachedItem(contexts)調(diào)用了--> findInCaches(context, key) --> 調(diào)用了doGet(cache, key),而doGet方法位于CacheAspectSupport的父類AbstractCacheInvoker中敬矩,該抽象類提供基礎(chǔ)的doGet概行,doPut,doEvict方法弧岳。

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

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

【小結(jié)】
image.png

【參考】

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末凳忙,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子禽炬,更是在濱河造成了極大的恐慌涧卵,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,718評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件腹尖,死亡現(xiàn)場離奇詭異柳恐,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)热幔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,683評論 3 385
  • 文/潘曉璐 我一進(jìn)店門乐设,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人绎巨,你說我怎么就攤上這事近尚。” “怎么了认烁?”我有些...
    開封第一講書人閱讀 158,207評論 0 348
  • 文/不壞的土叔 我叫張陵肿男,是天一觀的道長。 經(jīng)常有香客問我却嗡,道長舶沛,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,755評論 1 284
  • 正文 為了忘掉前任窗价,我火速辦了婚禮如庭,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己坪它,他們只是感情好骤竹,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,862評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著往毡,像睡著了一般蒙揣。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上开瞭,一...
    開封第一講書人閱讀 50,050評論 1 291
  • 那天懒震,我揣著相機(jī)與錄音,去河邊找鬼嗤详。 笑死个扰,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的葱色。 我是一名探鬼主播递宅,決...
    沈念sama閱讀 39,136評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼苍狰!你這毒婦竟也來了办龄?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,882評論 0 268
  • 序言:老撾萬榮一對情侶失蹤淋昭,失蹤者是張志新(化名)和其女友劉穎土榴,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體响牛,經(jīng)...
    沈念sama閱讀 44,330評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,651評論 2 327
  • 正文 我和宋清朗相戀三年赫段,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了呀打。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,789評論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡糯笙,死狀恐怖贬丛,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情给涕,我是刑警寧澤豺憔,帶...
    沈念sama閱讀 34,477評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站够庙,受9級特大地震影響恭应,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜耘眨,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,135評論 3 317
  • 文/蒙蒙 一昼榛、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧剔难,春花似錦胆屿、人聲如沸奥喻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,864評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽环鲤。三九已至,卻和暖如春憎兽,著一層夾襖步出監(jiān)牢的瞬間冷离,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,099評論 1 267
  • 我被黑心中介騙來泰國打工唇兑, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留酒朵,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,598評論 2 362
  • 正文 我出身青樓扎附,卻偏偏與公主長得像蔫耽,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子留夜,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,697評論 2 351

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