源碼簡(jiǎn)析XXL-JOB的注冊(cè)和執(zhí)行過(guò)程

一晋南,前言

XXL-JOB是一個(gè)優(yōu)秀的國(guó)產(chǎn)開源分布式任務(wù)調(diào)度平臺(tái)妆丘,他有著自己的一套調(diào)度注冊(cè)中心,提供了豐富的調(diào)度和阻塞策略等暇务,這些都是可視化的操作劣针,使用起來(lái)十分方便。

由于是國(guó)產(chǎn)的押搪,所以上手還是比較快的树酪,而且他的源碼也十分優(yōu)秀浅碾,因?yàn)槭钦{(diào)試平臺(tái)所以線程這一塊的使用是很頻繁的,特別值得學(xué)習(xí)研究续语。

XXL-JOB一同分為兩個(gè)模塊垂谢,調(diào)度中心模塊和執(zhí)行模塊。具體解釋疮茄,我們copy下官網(wǎng)的介紹:

  • 調(diào)度模塊(調(diào)度中心):
    負(fù)責(zé)管理調(diào)度信息滥朱,按照調(diào)度配置發(fā)出調(diào)度請(qǐng)求,自身不承擔(dān)業(yè)務(wù)代碼娃豹。調(diào)度系統(tǒng)與任務(wù)解耦焚虱,提高了系統(tǒng)可用性和穩(wěn)定性,同時(shí)調(diào)度系統(tǒng)性能不再受限于任務(wù)模塊懂版;
    支持可視化鹃栽、簡(jiǎn)單且動(dòng)態(tài)的管理調(diào)度信息,包括任務(wù)新建躯畴,更新民鼓,刪除,GLUE開發(fā)和任務(wù)報(bào)警等蓬抄,所有上述操作都會(huì)實(shí)時(shí)生效丰嘉,同時(shí)支持監(jiān)控調(diào)度結(jié)果以及執(zhí)行日志,支持執(zhí)行器Failover嚷缭。

  • 執(zhí)行模塊(執(zhí)行器):
    負(fù)責(zé)接收調(diào)度請(qǐng)求并執(zhí)行任務(wù)邏輯饮亏。任務(wù)模塊專注于任務(wù)的執(zhí)行等操作,開發(fā)和維護(hù)更加簡(jiǎn)單和高效阅爽;
    接收“調(diào)度中心”的執(zhí)行請(qǐng)求路幸、終止請(qǐng)求和日志請(qǐng)求等。

image

XXL-JOB中“調(diào)度模塊”和“任務(wù)模塊”完全解耦付翁,調(diào)度模塊進(jìn)行任務(wù)調(diào)度時(shí)简肴,將會(huì)解析不同的任務(wù)參數(shù)發(fā)起遠(yuǎn)程調(diào)用,調(diào)用各自的遠(yuǎn)程執(zhí)行器服務(wù)百侧。這種調(diào)用模型類似RPC調(diào)用砰识,調(diào)度中心提供調(diào)用代理的功能,而執(zhí)行器提供遠(yuǎn)程服務(wù)的功能佣渴。

下面看下springboot環(huán)境下的使用方式辫狼,首先看下執(zhí)行器的配置:

    @Bean
    public XxlJobSpringExecutor xxlJobExecutor() {
        logger.info(">>>>>>>>>>> xxl-job config init.");
        XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
        //調(diào)度中心地址
        xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
        //執(zhí)行器AppName
        xxlJobSpringExecutor.setAppname(appname);
        //執(zhí)行器注冊(cè)地址,默認(rèn)為空即可
        xxlJobSpringExecutor.setAddress(address);
        //執(zhí)行器IP [選填]:默認(rèn)為空表示自動(dòng)獲取IP
        xxlJobSpringExecutor.setIp(ip);
        //執(zhí)行器端口
        xxlJobSpringExecutor.setPort(port);
        //執(zhí)行器通訊TOKEN
        xxlJobSpringExecutor.setAccessToken(accessToken);
        //執(zhí)行器運(yùn)行日志文件存儲(chǔ)磁盤路徑
        xxlJobSpringExecutor.setLogPath(logPath);
        //執(zhí)行器日志文件保存天數(shù)
        xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);

        return xxlJobSpringExecutor;
    }

XXL-JOB提供了多種任務(wù)執(zhí)行方式观话,我們今天看下最簡(jiǎn)單的bean執(zhí)行模式予借。如下:

    /**
     * 1、簡(jiǎn)單任務(wù)示例(Bean模式)
     */
    @XxlJob("demoJobHandler")
    public void demoJobHandler() throws Exception {
        XxlJobHelper.log("XXL-JOB, Hello World.");

        for (int i = 0; i < 5; i++) {
            XxlJobHelper.log("beat at:" + i);
            TimeUnit.SECONDS.sleep(2);
        }
        // default success
    }

現(xiàn)在在調(diào)度中心稍做配置,我們這段代碼就可以按照一定的策略進(jìn)行調(diào)度執(zhí)行灵迫,是不是很神奇秦叛?我們先看下官網(wǎng)上的解釋:

原理:每個(gè)Bean模式任務(wù)都是一個(gè)Spring的Bean類實(shí)例,它被維護(hù)在“執(zhí)行器”項(xiàng)目的Spring容器中瀑粥。任務(wù)類需要加“@JobHandler(value=”名稱”)”注解挣跋,因?yàn)椤皥?zhí)行器”會(huì)根據(jù)該注解識(shí)別Spring容器中的任務(wù)。任務(wù)類需要繼承統(tǒng)一接口“IJobHandler”狞换,任務(wù)邏輯在execute方法中開發(fā)避咆,因?yàn)椤皥?zhí)行器”在接收到調(diào)度中心的調(diào)度請(qǐng)求時(shí),將會(huì)調(diào)用“IJobHandler”的execute方法修噪,執(zhí)行任務(wù)邏輯查库。

紙上得來(lái)終覺淺,絕知此事要躬行,今天的任務(wù)就是跟著這段話黄琼,我們大體看一波源碼的實(shí)現(xiàn)方式樊销。

二,XxlJobSpringExecutor

XxlJobSpringExecutor其實(shí)看名字脏款,我們都能想到围苫,這是XXL-JOB為了適應(yīng)spring模式的應(yīng)用而開發(fā)的模板類,先看下他的實(shí)現(xiàn)結(jié)構(gòu)撤师。

image

XxlJobSpringExecutor繼承自XxlJobExecutor剂府,同時(shí)由于是用在spring環(huán)境,所以實(shí)現(xiàn)了多個(gè)spring內(nèi)置的接口來(lái)配合實(shí)現(xiàn)整個(gè)執(zhí)行器模塊功能剃盾,每個(gè)接口的功能就不細(xì)說(shuō)了腺占,相信大家都可以百度查到。

我們看下初始化方法afterSingletonsInstantiated

    // start
    @Override
    public void afterSingletonsInstantiated() {

        //注冊(cè)每個(gè)任務(wù)
        initJobHandlerMethodRepository(applicationContext);

        // refresh GlueFactory
        GlueFactory.refreshInstance(1);

        // super start
        try {
            super.start();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

主流程看上去是比較簡(jiǎn)單的痒谴,首先是注冊(cè)每一個(gè)JobHandler,然后進(jìn)行初始化操作湾笛,GlueFactory.refreshInstance(1)是為了另一種調(diào)用模式時(shí)用到的,主要是用到了groovy闰歪,不在這次的分析中,我們就不看了蓖墅。我們繼續(xù)看下如何注冊(cè)JobHandler的库倘。

 private void initJobHandlerMethodRepository(ApplicationContext applicationContext) {
        if (applicationContext == null) {
            return;
        }
        // 遍歷所有beans,取出所有包含有@XxlJob的方法
        String[] beanDefinitionNames = applicationContext.getBeanNamesForType(Object.class, false, true);
        for (String beanDefinitionName : beanDefinitionNames) {
            Object bean = applicationContext.getBean(beanDefinitionName);

            Map<Method, XxlJob> annotatedMethods = null;   // referred to :org.springframework.context.event.EventListenerMethodProcessor.processBean
            try {
                annotatedMethods = MethodIntrospector.selectMethods(bean.getClass(),
                        new MethodIntrospector.MetadataLookup<XxlJob>() {
                            @Override
                            public XxlJob inspect(Method method) {
                                return AnnotatedElementUtils.findMergedAnnotation(method, XxlJob.class);
                            }
                        });
            } catch (Throwable ex) {
                logger.error("xxl-job method-jobhandler resolve error for bean[" + beanDefinitionName + "].", ex);
            }
            if (annotatedMethods==null || annotatedMethods.isEmpty()) {
                continue;
            }
            //遍歷@XxlJob方法论矾,取出executeMethod以及注解中對(duì)應(yīng)的initMethod, destroyMethod進(jìn)行注冊(cè)
            for (Map.Entry<Method, XxlJob> methodXxlJobEntry : annotatedMethods.entrySet()) {
                Method executeMethod = methodXxlJobEntry.getKey();
                XxlJob xxlJob = methodXxlJobEntry.getValue();
                if (xxlJob == null) {
                    continue;
                }

                String name = xxlJob.value();
                if (name.trim().length() == 0) {
                    throw new RuntimeException("xxl-job method-jobhandler name invalid, for[" + bean.getClass() + "#" + executeMethod.getName() + "] .");
                }
                if (loadJobHandler(name) != null) {
                    throw new RuntimeException("xxl-job jobhandler[" + name + "] naming conflicts.");
                }

                executeMethod.setAccessible(true);

                // init and destory
                Method initMethod = null;
                Method destroyMethod = null;

                if (xxlJob.init().trim().length() > 0) {
                    try {
                        initMethod = bean.getClass().getDeclaredMethod(xxlJob.init());
                        initMethod.setAccessible(true);
                    } catch (NoSuchMethodException e) {
                        throw new RuntimeException("xxl-job method-jobhandler initMethod invalid, for[" + bean.getClass() + "#" + executeMethod.getName() + "] .");
                    }
                }
                if (xxlJob.destroy().trim().length() > 0) {
                    try {
                        destroyMethod = bean.getClass().getDeclaredMethod(xxlJob.destroy());
                        destroyMethod.setAccessible(true);
                    } catch (NoSuchMethodException e) {
                        throw new RuntimeException("xxl-job method-jobhandler destroyMethod invalid, for[" + bean.getClass() + "#" + executeMethod.getName() + "] .");
                    }
                }

                // 注冊(cè) jobhandler
                registJobHandler(name, new MethodJobHandler(bean, executeMethod, initMethod, destroyMethod));
            }
        }

    }

XxlJobSpringExecutor由于實(shí)現(xiàn)了ApplicationContextAware教翩,所以通過(guò)applicationContext可以獲得所有容器中的bean實(shí)例,再通過(guò)MethodIntrospector來(lái)過(guò)濾出所有包含@XxlJob注解的方法贪壳,最后把對(duì)應(yīng)的executeMethod以及注解中對(duì)應(yīng)的initMethod, destroyMethod進(jìn)行注冊(cè)到jobHandlerRepository中饱亿,jobHandlerRepository是一個(gè)線程安全ConcurrentMap,MethodJobHandler實(shí)現(xiàn)自IJobHandler接口的一個(gè)模板類,主要作用就是通過(guò)反射去執(zhí)行對(duì)應(yīng)的方法彪笼∽曜ⅲ看到這,之前那句話任務(wù)類需要加“@JobHandler(value=”名稱”)”注解配猫,因?yàn)椤皥?zhí)行器”會(huì)根據(jù)該注解識(shí)別Spring容器中的任務(wù)幅恋。我們就明白了。

public class MethodJobHandler extends IJobHandler {
    ....
    public MethodJobHandler(Object target, Method method, Method initMethod, Method destroyMethod) {
        this.target = target;
        this.method = method;

        this.initMethod = initMethod;
        this.destroyMethod = destroyMethod;
    }

    @Override
    public void execute() throws Exception {
        Class<?>[] paramTypes = method.getParameterTypes();
        if (paramTypes.length > 0) {
            method.invoke(target, new Object[paramTypes.length]);       // method-param can not be primitive-types
        } else {
            method.invoke(target);
        }
    }

三泵肄,執(zhí)行服務(wù)器initEmbedServer

看完上面的JobHandler注冊(cè)捆交,后面緊著就是執(zhí)行器模塊的啟動(dòng)操作了,下面看下start方法:

    public void start() throws Exception {

        // 初始化日志path
        XxlJobFileAppender.initLogPath(logPath);

        // 注冊(cè)adminBizList
        initAdminBizList(adminAddresses, accessToken);

        // 初始化日志清除線程
        JobLogFileCleanThread.getInstance().start(logRetentionDays);

        // 初始化回調(diào)線程腐巢,用來(lái)把執(zhí)行結(jié)果回調(diào)給調(diào)度中心
        TriggerCallbackThread.getInstance().start();

        // 執(zhí)行服務(wù)器啟動(dòng)
        initEmbedServer(address, ip, port, appname, accessToken);
    }

前幾個(gè)操作品追,我們就不細(xì)看了,大家有興趣的可以自行查看冯丙,我們直接進(jìn)入initEmbedServer方法查看內(nèi)部服務(wù)器如何啟動(dòng)肉瓦,以及向調(diào)試中心注冊(cè)的。

    private void initEmbedServer(String address, String ip, int port, String appname, String accessToken) throws Exception {
        ...
        // start
        embedServer = new EmbedServer();
        embedServer.start(address, port, appname, accessToken);
    }

    public void start(final String address, final int port, final String appname, final String accessToken) {
        ```
        // 啟動(dòng)netty服務(wù)器
        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap.group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    public void initChannel(SocketChannel channel) throws Exception {
                        channel.pipeline()
                                .addLast(new IdleStateHandler(0, 0, 30 * 3, TimeUnit.SECONDS))  // beat 3N, close if idle
                                .addLast(new HttpServerCodec())
                                .addLast(new HttpObjectAggregator(5 * 1024 * 1024))  // merge request & reponse to FULL
                                .addLast(new EmbedHttpServerHandler(executorBiz, accessToken, bizThreadPool));
                    }
                })
                .childOption(ChannelOption.SO_KEEPALIVE, true);

        // bind
        ChannelFuture future = bootstrap.bind(port).sync();

        logger.info(">>>>>>>>>>> xxl-job remoting server start success, nettype = {}, port = {}", EmbedServer.class, port);

        // 執(zhí)行向調(diào)度中心注冊(cè)
        startRegistry(appname, address);
        ```
    }

因?yàn)閳?zhí)行器模塊本身需要有通訊交互的需求银还,不然調(diào)度中心是無(wú)法調(diào)用他的风宁,所以內(nèi)嵌了一個(gè)netty服務(wù)器進(jìn)行通信。啟動(dòng)成功后蛹疯,正式向調(diào)試中心執(zhí)行注冊(cè)請(qǐng)求戒财。我們直接看注冊(cè)的代碼:

    RegistryParam registryParam = new RegistryParam(RegistryConfig.RegistType.EXECUTOR.name(), appname, address);
    for (AdminBiz adminBiz: XxlJobExecutor.getAdminBizList()) {
        try {
            //執(zhí)行注冊(cè)請(qǐng)求
            ReturnT<String> registryResult = adminBiz.registry(registryParam);
            if (registryResult!=null && ReturnT.SUCCESS_CODE == registryResult.getCode()) {
                registryResult = ReturnT.SUCCESS;
                logger.debug(">>>>>>>>>>> xxl-job registry success, registryParam:{}, registryResult:{}", new Object[]{registryParam, registryResult});
                break;
            } else {
                logger.info(">>>>>>>>>>> xxl-job registry fail, registryParam:{}, registryResult:{}", new Object[]{registryParam, registryResult});
            }
        } catch (Exception e) {
            logger.info(">>>>>>>>>>> xxl-job registry error, registryParam:{}", registryParam, e);
        }
    }

    @Override
    public ReturnT<String> registry(RegistryParam registryParam) {
        return XxlJobRemotingUtil.postBody(addressUrl + "api/registry", accessToken, timeout, registryParam, String.class);
    }

XxlJobRemotingUtil.postBody就是個(gè)符合XXL-JOB規(guī)范的restful的http請(qǐng)求處理,里面不止有注冊(cè)請(qǐng)求捺弦,還有下線請(qǐng)求饮寞,回調(diào)請(qǐng)求等,礙于篇幅列吼,就不一一展示了幽崩,調(diào)度中心接到對(duì)應(yīng)的請(qǐng)求,會(huì)有對(duì)應(yīng)的DB處理:

        // services mapping
        if ("callback".equals(uri)) {
            List<HandleCallbackParam> callbackParamList = GsonTool.fromJson(data, List.class, HandleCallbackParam.class);
            return adminBiz.callback(callbackParamList);
        } else if ("registry".equals(uri)) {
            RegistryParam registryParam = GsonTool.fromJson(data, RegistryParam.class);
            return adminBiz.registry(registryParam);
        } else if ("registryRemove".equals(uri)) {
            RegistryParam registryParam = GsonTool.fromJson(data, RegistryParam.class);
            return adminBiz.registryRemove(registryParam);
        } else {
            return new ReturnT<String>(ReturnT.FAIL_CODE, "invalid request, uri-mapping("+ uri +") not found.");
        }

跟到這里寞钥,我們就已經(jīng)大概了解了整個(gè)注冊(cè)的流程慌申。同樣當(dāng)調(diào)度中心向我們執(zhí)行器發(fā)送請(qǐng)求,譬如說(shuō)執(zhí)行任務(wù)調(diào)度的請(qǐng)求時(shí)理郑,也是同樣的http請(qǐng)求發(fā)送我們上面分析的執(zhí)行器中內(nèi)嵌netty服務(wù)進(jìn)行操作蹄溉,這邊只展示調(diào)用方法:

    @Override
    public ReturnT<String> run(TriggerParam triggerParam) {
        return XxlJobRemotingUtil.postBody(addressUrl + "run", accessToken, timeout, triggerParam, String.class);
    }

這樣,我們執(zhí)行器模塊收到請(qǐng)求后會(huì)執(zhí)行我們上面注冊(cè)中的jobHandle進(jìn)行對(duì)應(yīng)的方法執(zhí)行您炉,執(zhí)行器會(huì)將請(qǐng)求存入“異步執(zhí)行隊(duì)列”并且立即響應(yīng)調(diào)度中心柒爵,異步運(yùn)行對(duì)應(yīng)方法。這樣一套注冊(cè)和執(zhí)行的流程就大致走下來(lái)了赚爵。

四棉胀,結(jié)尾

當(dāng)然事實(shí)上XXL-JOB的代碼還有許多豐富的特性法瑟,礙于本人實(shí)力不能一一道明,我這也是拋轉(zhuǎn)引玉唁奢,只是把最基礎(chǔ)的一些地方介紹給大家霎挟,有興趣的話,大家可以自行查閱相關(guān)代碼驮瞧,總的來(lái)說(shuō)氓扛,畢竟是國(guó)產(chǎn)開源的優(yōu)秀項(xiàng)目,還是值得贊賞的论笔,也希望國(guó)內(nèi)以后有越來(lái)越多優(yōu)秀開源框架采郎。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市狂魔,隨后出現(xiàn)的幾起案子蒜埋,更是在濱河造成了極大的恐慌,老刑警劉巖最楷,帶你破解...
    沈念sama閱讀 222,104評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件整份,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡籽孙,警方通過(guò)查閱死者的電腦和手機(jī)烈评,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)犯建,“玉大人讲冠,你說(shuō)我怎么就攤上這事∈释撸” “怎么了竿开?”我有些...
    開封第一講書人閱讀 168,697評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)玻熙。 經(jīng)常有香客問(wèn)我否彩,道長(zhǎng),這世上最難降的妖魔是什么嗦随? 我笑而不...
    開封第一講書人閱讀 59,836評(píng)論 1 298
  • 正文 為了忘掉前任列荔,我火速辦了婚禮,結(jié)果婚禮上枚尼,老公的妹妹穿的比我還像新娘肌毅。我一直安慰自己,他們只是感情好姑原,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,851評(píng)論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著呜舒,像睡著了一般锭汛。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,441評(píng)論 1 310
  • 那天唤殴,我揣著相機(jī)與錄音般婆,去河邊找鬼。 笑死朵逝,一個(gè)胖子當(dāng)著我的面吹牛蔚袍,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播配名,決...
    沈念sama閱讀 40,992評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼啤咽,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了渠脉?” 一聲冷哼從身側(cè)響起宇整,我...
    開封第一講書人閱讀 39,899評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎芋膘,沒想到半個(gè)月后鳞青,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,457評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡为朋,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,529評(píng)論 3 341
  • 正文 我和宋清朗相戀三年臂拓,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片习寸。...
    茶點(diǎn)故事閱讀 40,664評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡胶惰,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出融涣,到底是詐尸還是另有隱情童番,我是刑警寧澤,帶...
    沈念sama閱讀 36,346評(píng)論 5 350
  • 正文 年R本政府宣布威鹿,位于F島的核電站剃斧,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏忽你。R本人自食惡果不足惜幼东,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,025評(píng)論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望科雳。 院中可真熱鬧根蟹,春花似錦、人聲如沸糟秘。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)尿赚。三九已至散庶,卻和暖如春蕉堰,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背悲龟。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工屋讶, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人须教。 一個(gè)月前我還...
    沈念sama閱讀 49,081評(píng)論 3 377
  • 正文 我出身青樓皿渗,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親轻腺。 傳聞我的和親對(duì)象是個(gè)殘疾皇子乐疆,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,675評(píng)論 2 359

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