☆聊聊Dubbo(九):核心源码-服务端启动流程1

0 前言

Dubbo是阿里巴巴开源的基于Java实现的高性能、透明化的RPC框架。深入了解Dubbo源码,有助于快速定位问题、高效实现自定义拓展。本文以Dubbo服务端初始化过程为例,分析Dubbo怎么从配置转化成可被调用的服务。

以典型的服务端结合Spring配置为例:

<!-- 提供方应用信息,用于计算依赖关系 -->  
<dubbo:application name="demo-provider"/>  
<!-- 用dubbo协议在20880端口暴露服务 -->  
<dubbo:protocol name="dubbo" port="20880"/>  
<!-- 使用zookeeper注册中心暴露服务地址 -->  
<dubbo:registry address="zookeeper://127.0.0.1:1234" id="registry"/>  
<!-- 默认的服务端配置 -->  
<dubbo:provider registry="registry" retries="0" timeout="5000"/>  
<!-- 和本地bean一样实现服务 -->  
<bean id="demoService" class="com.alibaba.dubbo.demo.provider.DemoServiceImpl"/>  
<!-- 声明需要暴露的服务接口 -->  
<dubbo:service interface="com.alibaba.dubbo.demo.DemoService" ref="demoService"/>  

在Dubbo命名空间下定义了一系列XML节点,如:applicationprotocolregistryproviderservice 等,Dubbo通过实现Spring提供的 NamespaceHandler 接口,向Spring注册 BeanDefinition 解析器,使Spring能识别Dubbo命名空间下的节点,并且通过实现 BeanDefinitionParser 接口,使Spring能解析各节点的具体配置。

DubboNamespaceHandler#init() ,源码如下:

public void init() {  
    registerBeanDefinitionParser("application", new DubboBeanDefinitionParser(ApplicationConfig.class, true));
    registerBeanDefinitionParser("module", new DubboBeanDefinitionParser(ModuleConfig.class, true));
    registerBeanDefinitionParser("registry", new DubboBeanDefinitionParser(RegistryConfig.class, true));
    registerBeanDefinitionParser("monitor", new DubboBeanDefinitionParser(MonitorConfig.class, true));
    registerBeanDefinitionParser("provider", new DubboBeanDefinitionParser(ProviderConfig.class, true));
    registerBeanDefinitionParser("consumer", new DubboBeanDefinitionParser(ConsumerConfig.class, true));
    registerBeanDefinitionParser("protocol", new DubboBeanDefinitionParser(ProtocolConfig.class, true));
    registerBeanDefinitionParser("service", new DubboBeanDefinitionParser(ServiceBean.class, true));
    registerBeanDefinitionParser("reference", new DubboBeanDefinitionParser(ReferenceBean.class, false));
    registerBeanDefinitionParser("annotation", new DubboBeanDefinitionParser(AnnotationBean.class, true));
}

由以上代码可以看出,各个节点最终被转化为各种Bean,配置的各种属性也被转化为Bean的属性。从Bean的类型可以看出,大部分Bean只用于提供Dubbo的运行参数,只有 ServiceBean 才是本文服务发布分析入口。

备注DubboNamespaceHandler.java & DubboBeanDefinitionParser.java 源码分析,请参考《☆聊聊Dubbo(四):核心源码-切入Spring》一文。

1 ServiceBean 核心入口

Dubbo服务提供者由 dubbo:service 来定义的,从前面可以看到,Spring把 dubbo:service 解析成一个ServiceBean,ServiceBean实现了 ApplicationListenerInitializingBean 接口,ServiceBean有个核心方法 export,在这个方法中初始化服务提供者并且暴露远程服务。这个方法在bean初始化或容器中所有bean刷新完毕时被调用,根据 provider 的延迟设置决定,如果设置了延迟( delay 属性)则在bean初始化结束之后调用,否则在刷新事件中被调用,默认会延迟 export,即在所有bean的刷新结束被调用

com.alibaba.dubbo.config.spring.ServiceBean 类,源码如下:

public class ServiceBean<T> extends ServiceConfig<T> implements InitializingBean, DisposableBean, ApplicationContextAware, ApplicationListener, BeanNameAware {  
    ...
    public void afterPropertiesSet() {}
    ...
    public void onApplicationEvent(ApplicationEvent event) {}
    ...
    public void destroy() {}
}

ServiceBean 实现了Spring的 InitializingBeanDisposableBeanApplicationListener 等接口,实现了 afterPropertiesSet()destroy()onApplicationEvent() 等典型方法,这里便是Dubbo和Spring整合的关键,一般第三方框架基本都是通过这几个接口和Spring整合的

afterPropertiesSet() 主要用来注入各种 ConfigBean,便于服务注册过程中各种参数的获取,注意看最后关于延迟发布的几行代码,大意是如果不延迟,就立即注册和暴露服务

ServiceBean#afterPropertiesSet(),源码如下:

    public void afterPropertiesSet() throws Exception {
        // @ step1
        if (getProvider() == null) { 
            // BeanFactoryUtils.beansOfTypeIncludingAncestors 究竟做了什么?
            // 返回指定类型和子类型的所有bean,若该bean factory 是一个继承类型的beanFactory,这个方法也会获取祖宗factory中定义的指定类型的bean。
            Map<String, ProviderConfig> providerConfigMap = applicationContext == null ? null : BeanFactoryUtils.beansOfTypeIncludingAncestors(applicationContext, ProviderConfig.class, false, false);
            if (providerConfigMap != null && providerConfigMap.size() > 0) {
                Map<String, ProtocolConfig> protocolConfigMap = applicationContext == null ? null : BeanFactoryUtils.beansOfTypeIncludingAncestors(applicationContext, ProtocolConfig.class, false, false);
                if ((protocolConfigMap == null || protocolConfigMap.size() == 0)
                        && providerConfigMap.size() > 1) { // backward compatibility
                    List<ProviderConfig> providerConfigs = new ArrayList<ProviderConfig>();
                    for (ProviderConfig config : providerConfigMap.values()) {
                        if (config.isDefault() != null && config.isDefault()) {
                            providerConfigs.add(config);
                        }
                    }
                    if (!providerConfigs.isEmpty()) {
                        setProviders(providerConfigs);
                    }
                } else {
                    ProviderConfig providerConfig = null;
                    for (ProviderConfig config : providerConfigMap.values()) {
                        if (config.isDefault() == null || config.isDefault()) {
                            if (providerConfig != null) {
                                throw new IllegalStateException("Duplicate provider configs: " + providerConfig + " and " + config);
                            }
                            providerConfig = config;
                        }
                    }
                    if (providerConfig != null) {
                        setProvider(providerConfig);
                    }
                }
            }
        }
        // @ step2
        if (getApplication() == null
                && (getProvider() == null || getProvider().getApplication() == null)) {
            Map<String, ApplicationConfig> applicationConfigMap = applicationContext == null ? null : BeanFactoryUtils.beansOfTypeIncludingAncestors(applicationContext, ApplicationConfig.class, false, false);
            if (applicationConfigMap != null && applicationConfigMap.size() > 0) {
                ApplicationConfig applicationConfig = null;
                for (ApplicationConfig config : applicationConfigMap.values()) {
                    if (config.isDefault() == null || config.isDefault()) {
                        if (applicationConfig != null) {
                            throw new IllegalStateException("Duplicate application configs: " + applicationConfig + " and " + config);
                        }
                        applicationConfig = config;
                    }
                }
                if (applicationConfig != null) {
                    setApplication(applicationConfig);
                }
            }
        }
        // @ step3
        if (getModule() == null
                && (getProvider() == null || getProvider().getModule() == null)) {
            Map<String, ModuleConfig> moduleConfigMap = applicationContext == null ? null : BeanFactoryUtils.beansOfTypeIncludingAncestors(applicationContext, ModuleConfig.class, false, false);
            if (moduleConfigMap != null && moduleConfigMap.size() > 0) {
                ModuleConfig moduleConfig = null;
                for (ModuleConfig config : moduleConfigMap.values()) {
                    if (config.isDefault() == null || config.isDefault()) {
                        if (moduleConfig != null) {
                            throw new IllegalStateException("Duplicate module configs: " + moduleConfig + " and " + config);
                        }
                        moduleConfig = config;
                    }
                }
                if (moduleConfig != null) {
                    setModule(moduleConfig);
                }
            }
        }
        // @ step4
        if ((getRegistries() == null || getRegistries().isEmpty())
                && (getProvider() == null || getProvider().getRegistries() == null || getProvider().getRegistries().isEmpty())
                && (getApplication() == null || getApplication().getRegistries() == null || getApplication().getRegistries().isEmpty())) {
            Map<String, RegistryConfig> registryConfigMap = applicationContext == null ? null : BeanFactoryUtils.beansOfTypeIncludingAncestors(applicationContext, RegistryConfig.class, false, false);
            if (registryConfigMap != null && registryConfigMap.size() > 0) {
                List<RegistryConfig> registryConfigs = new ArrayList<RegistryConfig>();
                for (RegistryConfig config : registryConfigMap.values()) {
                    if (config.isDefault() == null || config.isDefault()) {
                        registryConfigs.add(config);
                    }
                }
                if (!registryConfigs.isEmpty()) {
                    super.setRegistries(registryConfigs);
                }
            }
        }
        // @ step5
        if (getMonitor() == null
                && (getProvider() == null || getProvider().getMonitor() == null)
                && (getApplication() == null || getApplication().getMonitor() == null)) {
            Map<String, MonitorConfig> monitorConfigMap = applicationContext == null ? null : BeanFactoryUtils.beansOfTypeIncludingAncestors(applicationContext, MonitorConfig.class, false, false);
            if (monitorConfigMap != null && monitorConfigMap.size() > 0) {
                MonitorConfig monitorConfig = null;
                for (MonitorConfig config : monitorConfigMap.values()) {
                    if (config.isDefault() == null || config.isDefault()) {
                        if (monitorConfig != null) {
                            throw new IllegalStateException("Duplicate monitor configs: " + monitorConfig + " and " + config);
                        }
                        monitorConfig = config;
                    }
                }
                if (monitorConfig != null) {
                    setMonitor(monitorConfig);
                }
            }
        }
        // @ step6
        if ((getProtocols() == null || getProtocols().isEmpty())
                && (getProvider() == null || getProvider().getProtocols() == null || getProvider().getProtocols().isEmpty())) {
            Map<String, ProtocolConfig> protocolConfigMap = applicationContext == null ? null : BeanFactoryUtils.beansOfTypeIncludingAncestors(applicationContext, ProtocolConfig.class, false, false);
            if (protocolConfigMap != null && protocolConfigMap.size() > 0) {
                List<ProtocolConfig> protocolConfigs = new ArrayList<ProtocolConfig>();
                for (ProtocolConfig config : protocolConfigMap.values()) {
                    if (config.isDefault() == null || config.isDefault()) {
                        protocolConfigs.add(config);
                    }
                }
                if (!protocolConfigs.isEmpty()) {
                    super.setProtocols(protocolConfigs);
                }
            }
        }
        // @ step7
        if (getPath() == null || getPath().length() == 0) {
            if (beanName != null && beanName.length() > 0
                    && getInterface() != null && getInterface().length() > 0
                    && beanName.startsWith(getInterface())) {
                setPath(beanName);
            }
        }
        // @ step8
        if (!isDelay()) {
            export();
        }
    }

Step1:如果 provider 为空,说明 dubbo:service 标签未设置 provider 属性,则尝试从 BeanFactory 中查询 dubbo:provider 实例,如果存在一个 dubbo:provider 标签,则取该实例,如果存在多个 dubbo:provider 配置则 provider 属性不能为空,否则抛出异常:“Duplicate provider configs”。

Step2:如果 application 为空,说明 dubbo:service 标签未设置 application 属性,则尝试从 BeanFactory 中查询 dubbo:application 实例,如果存在一个 dubbo:application 标签,则取该实例,如果存在多个 dubbo:application 配置,则抛出异常:“Duplicate application configs”。

Step3:如果 module 为空,说明 dubbo:service 标签未设置 module 属性,则尝试从 BeanFactory 中查询 dubbo:module 实例,如果存在一个 dubbo:module 标签,则取该实例,如果存在多个 dubbo:module,则抛出异常:“Duplicate module configs”。

Step4:(逻辑同上)尝试从 BeanFactory 中加载所有的注册中心,注意 ServiceBeanList registries 属性,为注册中心集合。

Step5:(逻辑同上)尝试从 BeanFacotry 中加载一个监控中心,填充 ServiceBeanMonitorConfig monitor 属性,如果存在多个 dubbo:monitor 配置,则抛出”Duplicate monitor configs: “。

Step6:(逻辑同上)尝试从 BeanFactory 中加载所有的协议,注意: ServiceBeanList protocols 是一个集合,也即一个服务可以通过多种协议暴露给消费者。

Step7:(逻辑同上)设置 ServiceBeanpath 属性,path 属性存放的是 dubbo:servicebeanName(dubbo:service id)。

Step8:如果为启用延迟暴露机制,则调用 export 暴露服务。首先看一下 isDelay 的实现,然后重点分析 export 的实现原理(服务暴露的整个实现原理)。

ServiceBean#isDelay(),源码如下:

    private boolean isDelay() {
        Integer delay = getDelay();
        ProviderConfig provider = getProvider();
        if (delay == null && provider != null) {
            delay = provider.getDelay();
        }
        return supportedApplicationListener && (delay == null || delay == -1);
    }

先从 ServiceConfig 获取 delay 属性,如果为 null,则获取 ProviderConfigdelay 属性,最后如果还是 null 或配置为 -1 表示延迟暴露服务。可见Dubbo获取运行参数的层级,便于更精确化的配置各种参数。

通过 supportedApplicationListener 可以猜到服务延迟暴露是通过Spring容器的监听器触发的。个人更倾向于明确设置 delay=-1 或者所有层级都不配置,因为如果提早暴露服务,此时其他的Spring bean可能还未初始化完成,而暴露出去的服务大部分情况下依赖于Spring的其他bean来实现业务功能,如果提早接收到客户端的请求,难免会出现各种异常

ServiceBean#onApplicationEvent(),源码如下:

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        if (isDelay() && !isExported() && !isUnexported()) {
            if (logger.isInfoEnabled()) {
                logger.info("The service ready on spring started. service: " + getInterface());
            }
            export();
        }
    }

如果有设置 dubbo:servicedubbo:provider 的属性 delay,或配置 delay-1,都表示启用延迟机制,单位为毫秒,设置为 -1表示等到Spring容器初始化后再暴露服务

从这里也可以看出,Dubbo暴露服务的处理入口为:ServiceBean#export --> ServiceConfig#export

2 ServiceConfig 暴露服务

从前一节代码分析可知,最后一步是调用 ServiceBean 的父类 ServiceConfig#export方法暴露服务。

2.1 第一步:ServiceConfig#export 暴露服务

调用链:ServiceBean#afterPropertiesSet --> ServiceConfig#export

    public synchronized void export() {
        if (provider != null) {
            if (export == null) {
                export = provider.getExport();
            }
            if (delay == null) {
                delay = provider.getDelay();
            }
        }
        if (export != null && !export) {   // @ step1
            return;
        }

        if (delay != null && delay > 0) {    // @ step2
            delayExportExecutor.schedule(new Runnable() {
                @Override
                public void run() {
                    doExport();
                }
            }, delay, TimeUnit.MILLISECONDS);
        } else {
            doExport();    // @ step3
        }
    }

Step1:判断是否暴露服务,由 dubbo:service export=“true|false” 来指定。

Step2:如果启用了 delay 机制,如果 delay 大于0,表示延迟多少毫秒后暴露服务,使用 ScheduledExecutorService 延迟调度,最终调用 doExport 方法。

Step3:执行具体的暴露逻辑 doExport,需要大家留意:delay=-1 的处理逻辑( 基于Spring事件机制触发 )。

2.2 第二步:ServiceConfig#doExport 暴露服务

调用链:ServiceBean#afterPropertiesSet --> ServiceConfig#export --> ServiceConfig#doExport

    protected synchronized void doExport() {
        if (unexported) {
            throw new IllegalStateException("Already unexported!");
        }
        if (exported) {
            return;
        }
        exported = true;
        if (interfaceName == null || interfaceName.length() == 0) {
            throw new IllegalStateException("<dubbo:service interface=\"\" /> interface not allow null!");
        }
        checkDefault(); // @ step1
        if (provider != null) {
            if (application == null) {
                application = provider.getApplication();
            }
            if (module == null) {
                module = provider.getModule();
            }
            if (registries == null) {
                registries = provider.getRegistries();
            }
            if (monitor == null) {
                monitor = provider.getMonitor();
            }
            if (protocols == null) {
                protocols = provider.getProtocols();
            }
        }
        if (module != null) {
            if (registries == null) {
                registries = module.getRegistries();
            }
            if (monitor == null) {
                monitor = module.getMonitor();
            }
        }
        if (application != null) {
            if (registries == null) {
                registries = application.getRegistries();
            }
            if (monitor == null) {
                monitor = application.getMonitor();
            }
        }
        if (ref instanceof GenericService) { // @ step2
            interfaceClass = GenericService.class;
            if (StringUtils.isEmpty(generic)) {
                generic = Boolean.TRUE.toString();
            }
        } else {
            try {
                interfaceClass = Class.forName(interfaceName, true, Thread.currentThread()
                        .getContextClassLoader());
            } catch (ClassNotFoundException e) {
                throw new IllegalStateException(e.getMessage(), e);
            }
            checkInterfaceAndMethods(interfaceClass, methods);
            checkRef();
            generic = Boolean.FALSE.toString();
        }
        if (local != null) { // @ step3
            if ("true".equals(local)) {
                local = interfaceName + "Local";
            }
            Class<?> localClass;
            try {
                localClass = ClassHelper.forNameWithThreadContextClassLoader(local);
            } catch (ClassNotFoundException e) {
                throw new IllegalStateException(e.getMessage(), e);
            }
            if (!interfaceClass.isAssignableFrom(localClass)) {
                throw new IllegalStateException("The local implementation class " + localClass.getName() + " not implement interface " + interfaceName);
            }
        }
        if (stub != null) { // @ step4
            if ("true".equals(stub)) {
                stub = interfaceName + "Stub";
            }
            Class<?> stubClass;
            try {
                stubClass = ClassHelper.forNameWithThreadContextClassLoader(stub);
            } catch (ClassNotFoundException e) {
                throw new IllegalStateException(e.getMessage(), e);
            }
            if (!interfaceClass.isAssignableFrom(stubClass)) {
                throw new IllegalStateException("The stub implementation class " + stubClass.getName() + " not implement interface " + interfaceName);
            }
        }
        checkApplication(); // @ step5
        checkRegistry();
        checkProtocol();
        appendProperties(this);

        checkStubAndMock(interfaceClass); // @ step6
        if (path == null || path.length() == 0) {
            path = interfaceName;
        }
        doExportUrls(); // @ step7

        ProviderModel providerModel = new ProviderModel(getUniqueServiceName(), ref, interfaceClass); // @ step8
        ApplicationModel.initProviderModel(getUniqueServiceName(), providerModel);
    }

    private void checkDefault() { // @ step1
        if (provider == null) {
            provider = new ProviderConfig();
        }
        appendProperties(provider);
    }

Step1:如果 dubbo:servce 标签也就是 ServiceBeanprovider 属性为空,调用 appendProperties 方法,填充默认属性,其具体加载顺序:

1. 从系统属性加载对应参数值,参数键:dubbo.provider.属性名,System.getProperty。 

2. 从属性配置文件加载对应参数值,可通过系统属性指定属性配置文件: dubbo.properties.file,如果该值未配置,则默认取 dubbo.properties 属性配置文件。

Step2:校验 refinterface 属性。如果 refGenericService,则为Dubbo的泛化实现,然后验证 interface 接口与 ref 引用的类型是否一致。

Step3dubbo:service local机制,已经废弃,被 stub属性所替换。

Step4:处理本地存根 Stub

Step5:校验 ServiceBeanapplicationregistryprotocol 是否为空,并从系统属性(优先)、资源文件中填充其属性。系统属性、资源文件属性的配置如下:

application dubbo.application.属性名,例如 dubbo.application.name 

registry dubbo.registry.属性名,例如 dubbo.registry.address 

protocol dubbo.protocol.属性名,例如 dubbo.protocol.port 

service dubbo.service.属性名,例如 dubbo.service.stub

Step6:校验 stubmock 类的合理性,是否是 interface 的实现类。

Step7:执行 doExportUrls() 方法暴露服务,接下来会重点分析该方法。

Step8:将服务提供者信息注册到 ApplicationModel 实例中。

2.3 第三步:ServiceConfig#doExportUrls 暴露服务

调用链:ServiceBean#afterPropertiesSet --> ServiceConfig#export --> ServiceConfig#doExport --> ServiceConfig#doExportUrls

private void doExportUrls() {  
    List<URL> registryURLs = loadRegistries(true); // @ step1
    for (ProtocolConfig protocolConfig : protocols) {
        doExportUrlsFor1Protocol(protocolConfig, registryURLs); // @ step2
    }
}

Step1:首先遍历 ServiceBeanList registries(所有注册中心的配置信息),然后将地址封装成URL对象,关于注册中心的所有配置属性,最终转换成url的属性(?属性名=属性值),loadRegistries(true),参数的意思:true,代表服务提供者,false:代表服务消费者,如果是服务提供者,则检测注册中心的配置,如果配置了 register=“false”,则忽略该地址,如果是服务消费者,并配置了 subscribe=“false” 则表示不从该注册中心订阅服务,故也不返回。

Step2:然后遍历配置的所有协议,根据每个协议,向注册中心暴露服务,接下来重点分析 doExportUrlsFor1Protocol 方法的实现细节。

所以,从上面代码,可以看出 Dubbo同一个服务支持多种服务协议、支持向多种注册中心注册,很方便同一功能由各种不同实现方式的客户端调用。

扫码入群讨论

Dubbo Geek-1群
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容