☆聊聊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閱讀 228,461評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內容