Dubbo服務(wù)引入

轉(zhuǎn)自:https://www.yuque.com/docs/share/10960cb9-449b-4177-94b6-493b8f4ff9b9?

dubbo

一店印、引言

服務(wù)引入是rpc調(diào)用不可或缺的一部分,本文會(huì)圍繞以下幾個(gè)問題對(duì)服務(wù)引入進(jìn)行講解。

  • 什么是服務(wù)引入窥浪?服務(wù)引入需要做什么事情?
  • spring作為當(dāng)前java項(xiàng)目幾乎必備的框架惧财,如何將服務(wù)引入切入spring?
  • 服務(wù)引入如何做到降低對(duì)業(yè)務(wù)代碼的侵入性宋税?
  • 不同的提高方可能支持不同的通信協(xié)議,作為一個(gè)rpc框架怎么做到讓調(diào)用端可以針對(duì)不同提高端使用不同的通信協(xié)議坪圾?甚至支持配置自定義協(xié)議晓折?
  • 服務(wù)提供者可能出現(xiàn)部分宕機(jī)的情況,如何保證引入服務(wù)的可用性兽泄?

本文以Dubbo的實(shí)現(xiàn)進(jìn)行分析漓概,因?yàn)槿粘m?xiàng)目中常常是配合Spring使用的,所以本文會(huì)以Dubbo結(jié)合Spring的使用進(jìn)行分析病梢。

二胃珍、解析

2.1 概況

2.1.1 什么是服務(wù)引入?

RPC(Remote Procedure Call) 即遠(yuǎn)程過程調(diào)用,包含了兩個(gè)最基本的角色蜓陌,服務(wù)提供者和服務(wù)消費(fèi)者觅彰。

既然是遠(yuǎn)程過程調(diào)用,那么消費(fèi)者在調(diào)用提供者之前就得先拿到服務(wù)提供者的服務(wù)信息(比如最基本的提供者的服務(wù)地址)钮热,并對(duì)調(diào)用信息進(jìn)行封裝準(zhǔn)備好填抬,我們才能對(duì)提供者發(fā)起調(diào)用請求。

而服務(wù)引入就是引入提供者服務(wù)信息隧期,并封裝調(diào)用類的一個(gè)過程飒责。

2.1.2 服務(wù)引入類型

服務(wù)引用****有三種方式

(1) 本地引入

一個(gè)服務(wù)即可以提供者,同時(shí)也可以是消費(fèi)者厌秒。

所以會(huì)存在消費(fèi)者消費(fèi)的服務(wù)读拆,同時(shí)也是當(dāng)前服務(wù)提供的服務(wù),如圖所示:

image.png

圖 2.1

針對(duì)這種情況鸵闪,RPC框架應(yīng)該避免發(fā)起網(wǎng)絡(luò)請求檐晕,直接本地調(diào)用。封裝的調(diào)用類應(yīng)當(dāng)通過本地導(dǎo)出的服務(wù)發(fā)起調(diào)用。

(2) 直接服務(wù)

直接服務(wù)指的是直接在消費(fèi)端指定提供端的服務(wù)地址辟灰。

如圖直接將提供者url由消費(fèi)端直接指定个榕,發(fā)起調(diào)用時(shí)直接根據(jù)配置好的url發(fā)起調(diào)用。
image.png

圖 2.2

優(yōu)點(diǎn):方便測試芥喇,直接連接服務(wù)西采,不依賴第三方。

缺點(diǎn):存在服務(wù)可用性問題继控,也不能動(dòng)態(tài)伸縮服務(wù)械馆,不建議在線上使用。

(3) 基于注冊中心引入

為了避免提高可用性武通,引入了注冊中心霹崎。服務(wù)提供者將服務(wù)信息注冊到注冊中心,消費(fèi)者訂閱注冊中心獲取提供者url冶忱、提供者配置等服務(wù)信息尾菇,根據(jù)獲取到的服務(wù)信息封裝調(diào)用類發(fā)起調(diào)用。如圖所示:

image.png

圖 2.3

優(yōu)點(diǎn):提供者服務(wù)不可用時(shí)自動(dòng)刪除提供者信息囚枪,重啟時(shí)自動(dòng)恢復(fù)派诬,可以動(dòng)態(tài)伸縮服務(wù)。

缺點(diǎn):相比直接引用链沼,需要依賴服務(wù)中心默赂,且需要保證注冊中心的可用性。

2.2 切入spring

spring框架作為當(dāng)前java項(xiàng)目幾乎必不可少的框架忆植,如何接入spring也是一個(gè)RPC框架需要考慮的點(diǎn)放可。

那么Dubbo是怎么接入的spring的呢?

在spring項(xiàng)目中引入Dubbo服務(wù)朝刊,只需要配置<****dubbo****:reference/>標(biāo)簽耀里,然后依賴注入提供者就可以實(shí)現(xiàn)RPC調(diào)用。

那么spring項(xiàng)目啟動(dòng)的時(shí)候拾氓,dubbo是如何做到讓<****dubbo****:reference/>標(biāo)簽被spring識(shí)別并解析呢冯挎?

2.2.1 切入入口

Spring啟動(dòng)時(shí)ClassPathXmlApplicationContext會(huì)對(duì)引入的配置文件進(jìn)行解析,并將bean注入到spring容器中咙鞍。

但是spring并不認(rèn)識(shí)第三方自定義的標(biāo)簽房官,為了支持解析外部自定義的標(biāo)簽,Spring提供了擴(kuò)展點(diǎn)续滋,會(huì)通過查找classPath下所有 spring.handlers 文件翰守,從該文件中獲取所有擴(kuò)展的命名空間處理器。也就是獲取外部的標(biāo)簽處理器疲酌。

文件中的內(nèi)容需要以鍵值對(duì)的方式表示蜡峰,NamespaceUrl為key了袁,value為解析器。spring在解析到某個(gè)外部標(biāo)簽時(shí)湿颅,會(huì)以外部標(biāo)簽的NamespaceUrl為key,獲取對(duì)應(yīng)的解析器载绿,解析該xml標(biāo)簽。

image.png

圖 2.4

而Dubbo就是通過這種方式去擴(kuò)展油航,如圖2.4所示崭庸,我們可以看到在Dubbo包下在 META-INF/spring.handlers 文件中,以dubbo標(biāo)簽的NamespaceUrl為key谊囚,解析處理器全限定名為value存儲(chǔ)怕享。Spring解析到dubbo標(biāo)簽時(shí)會(huì)通過dubbo提供的命名空間處理器DubboNamespaceHandler進(jìn)行解析。

在Spring中spring.handlers文件最終由spring的DefaultNamespaceHandlerResolver加載并保存到標(biāo)簽處理器****集合handlerMappings中秒啦。獲取handlerMappings源碼如下:

image.png

圖 2.5

如圖2.5所示getHandllerMapping將DubboNamespaceHandler加載進(jìn)了handlerMappings 中熬粗。

spring加載spring.handlers調(diào)用時(shí)序圖如下:

image.png

圖 2.6

由圖2.6可知,Spring最終獲取到解析xml標(biāo)簽的處理器之后余境,調(diào)用處理器的init方法、paser方法灌诅,最終獲取到一個(gè)BeanDefinition注冊到spring容器中芳来。

(spring要求自定義命名空間處理器要實(shí)現(xiàn)NamespaceHandler接口,因此都會(huì)init方法和parse方法

2.2.2 切入細(xì)節(jié)

我們已經(jīng)的得知切入Spring的入口猜拾,那么作為一個(gè)RPC框架即舌,應(yīng)該如何去實(shí)現(xiàn)這個(gè)命名空間解析器,如何將標(biāo)簽轉(zhuǎn)換成spring的BeanDefinition挎袜?

針對(duì)Dubbo的分析顽聂,我們已經(jīng)得知dubbo標(biāo)簽最終會(huì)由DubboNamespaceHandler進(jìn)行解析,并且最終Spring會(huì)調(diào)用命名空間解析器init方法和parse方法盯仪,最終轉(zhuǎn)換成spring的BeanDefinition紊搪。那么DubboNamespaceHandler這兩步做了什么事情?

DubboNamespaceHandler類圖如下:

image.png

(1)init 方法

一個(gè)完整的RPC服務(wù)不止是包含服務(wù)引入全景,同時(shí)也是還有協(xié)議定義耀石、服務(wù)導(dǎo)出、注冊中心等等模塊爸黄,而RPC框架就得針對(duì)不同的模塊定義不同的聲明標(biāo)簽滞伟。那么在正式解析之前,就得先將不同模塊的解析區(qū)分開炕贵,Spring也考慮了這一點(diǎn)梆奈,提供了NamespaceHandler接口的實(shí)現(xiàn)抽象類NamespaceHandlerSupport,該類提供了針對(duì)不同模塊的標(biāo)簽進(jìn)行注冊的方法称开,實(shí)際上就是以模塊名為key,對(duì)應(yīng)的解析器為value存儲(chǔ)在一個(gè)Map集合亩钟。

init顧名思義就是做一些初始化操作的,Dubbo就選擇在init方法中注冊不同模塊的標(biāo)簽解析器。

DubboNamespaceHandler#init 源碼如下

public class DubboNamespaceHandler extends NamespaceHandlerSupport {

    static {
        Version.checkDuplicate(DubboNamespaceHandler.class);
    }
    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));
        // 服務(wù)引入標(biāo)簽解析 指定BeanDefinition BeanClass 為 ReferenceBean
        registerBeanDefinitionParser("reference", new DubboBeanDefinitionParser(ReferenceBean.class, false));
        registerBeanDefinitionParser("annotation", new DubboBeanDefinitionParser(AnnotationBean.class, true));
    }
}

代碼塊 2.1

如代碼塊2.2.1所示径荔,DubboNamespaceHandler 實(shí)現(xiàn)了NamespaceHandler 的init方法督禽,將各類型標(biāo)簽的對(duì)應(yīng)的解析器注冊到解析器集合Map中。并且可以看出dubbo每個(gè)標(biāo)簽都是通過DubboBeanDefinitionParser進(jìn)行解析总处,只是指定解析后BeanDefinitionbeanClass不同狈惫。

(2) parse方法

parse也就是解析標(biāo)簽的方法,已經(jīng)分析過Dubbo會(huì)注冊不同標(biāo)簽的解析器鹦马,那么可以猜想parse方法會(huì)根據(jù)不同標(biāo)簽取出對(duì)應(yīng)的解析器胧谈,再進(jìn)行解析

NamespaceHandlerSuppor****t#parse 源碼如下:

public BeanDefinition parse(Element element, ParserContext parserContext) {
    // 根據(jù)標(biāo)簽名從解析器map拿出解析bean荸频,根據(jù)解析bean的parse方法進(jìn)行解析
    return findParserForElement(element, parserContext)
           .parse(element, parserContext);
}
private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) {
    // 根據(jù)標(biāo)簽名從解析器map拿出解析bean
    String localName = parserContext.getDelegate().getLocalName(element);
    BeanDefinitionParser parser = this.parsers.get(localName);
    if (parser == null) {
        parserContext.getReaderContext().fatal(
            "Cannot locate BeanDefinitionParser for element [" + localName + "]", element);
    }
    return parser;
}

代碼塊 2.2

如代碼塊2.2所示菱肖,根據(jù)標(biāo)簽名,取出對(duì)應(yīng)的解析器旭从,再通過解析器DubboBeanDefinitionParser進(jìn)行解析稳强。

已經(jīng)拿到對(duì)應(yīng)的解析器后,下一步就是將標(biāo)簽解析處理成BeanDefinition并注冊到spring容器和悦。

解析標(biāo)簽創(chuàng)建BeanDefinition退疫,需要指定實(shí)際引用類beanClass, 保存標(biāo)簽類的各種配置信息,再指定beanName將BeanDefinition注冊到spring容器鸽素。

DubboBeanDefinitionParser 通過parse方法進(jìn)行解析xml標(biāo)簽褒繁,部分源碼如下:

public BeanDefinition parse(Element element, ParserContext parserContext) {
        // beanClass 即為Dubbo在init創(chuàng)建DubboNamespaceHandler時(shí)指定beanClass.
        return parse(element, parserContext, beanClass, required);
}
private static BeanDefinition parse(Element element, ParserContext parserContext, Class<?> beanClass, boolean required) {
        RootBeanDefinition beanDefinition = new RootBeanDefinition();
        // 配置RootBeanDefinition bean類型
        // 比如服務(wù)引用類型 則為ReferenceBean,id屬性即為引用的bean名
        beanDefinition.setBeanClass(beanClass);
        String id = element.getAttribute("id");
        ...
        if (id != null && id.length() > 0) {
            if (parserContext.getRegistry().containsBeanDefinition(id))  {
                throw new IllegalStateException("Duplicate spring bean id " + id);
            }
            // 以id作為bean名注冊beanDefinition 到spring容器
            parserContext.getRegistry().registerBeanDefinition(id, beanDefinition);
            beanDefinition.getPropertyValues().addPropertyValue("id", id);
        }
    
        ... 一系列配置bean過程
        ... 將標(biāo)簽上配置的參數(shù)存到beandefinition的PropertyValues屬性中
 
        return beanDefinition;
}

代碼塊 2.3

以服務(wù)引用標(biāo)簽為例子馍忽,beanClass為ReferenceBean棒坏。

根據(jù)代碼塊2.3,DubboBeanDefinitionParser 通過 parse方法進(jìn)行解析獲取到beanClass為ReferenceBeanBeanDefinition遭笋,注冊到spring容器并返回解析結(jié)果坝冕。

ps:

從該源碼還可以看到,在解析的時(shí)候坐梯,發(fā)現(xiàn)spring容器中已經(jīng)包含這個(gè)bean名徽诲,那么會(huì)報(bào)錯(cuò)重復(fù)的bean id,因此****dubbo****:reference標(biāo)簽指定的id不能是已經(jīng)存在spring容器中的bean名

2.2.3 結(jié)論

基于Dubbo在服務(wù)引入切入Spring的方式吵血,我們可以得到一下結(jié)論:

  • RPC框架服務(wù)引入切入spring可以通過spring提供的xml解析擴(kuò)展點(diǎn)spring.handlers谎替,對(duì)xml解析進(jìn)行了擴(kuò)展。
  • 框架定義的解析器蹋辅,可以基于NamespaceHandlerSupport在init針對(duì)不同標(biāo)簽注冊不同的自定義解析器钱贯,NamespaceHandlerSupport#****parse方法會(huì)去根據(jù)不同標(biāo)簽獲取對(duì)應(yīng)的自定義解析器。
  • 自定義解析器parse方法需要?jiǎng)?chuàng)建BeanDefinition,保存標(biāo)簽配置信息侦另,注冊BeanDefinition到Spring容器秩命。

2.3 服務(wù)引入實(shí)現(xiàn)分析

服務(wù)引入需要根據(jù)配置信息封裝服務(wù)調(diào)用類尉共,作為一個(gè)RPC框架還需要考慮如何支持區(qū)分多種服務(wù)引入類型,如何避免服務(wù)引入對(duì)業(yè)務(wù)代碼的侵入弃锐,如何提高服務(wù)引入的可擴(kuò)展性袄友,支持用戶自己指定引入服務(wù)調(diào)用使用的協(xié)議。

本節(jié)通過分析Dubbo的實(shí)現(xiàn)霹菊,解析Dubbo是怎么處理的剧蚣。

根據(jù)2.2節(jié)的分析,xml解析只是將引入的bean轉(zhuǎn)化成BeanDefinition旋廷,保存了配置一些信息鸠按,并沒有去封裝一個(gè)服務(wù)調(diào)用類。Dubbo的服務(wù)引用標(biāo)簽<reference>標(biāo)簽最終會(huì)被spring解析成ReferenceBean類型的BeanDefinition饶碘,并將標(biāo)簽上配置的參數(shù)注入到BeanDefinition(比如引用服務(wù)的權(quán)限定名)目尖,并加載到bean容器中。

我們根據(jù)以下幾點(diǎn)做下分析

  • ReferenceBean是什么時(shí)候真正去封裝服務(wù)調(diào)用類的
  • 我們依賴注入的是引用類, 為什么beanClass是ReferenceBean
  • 為什么我們直接注入引用類就可以關(guān)聯(lián)上提供者類扎运,并發(fā)起rpc調(diào)用

ReferenceBean類圖如下:

image.png

圖 2.7

2.3.1 封裝調(diào)用類時(shí)機(jī)

(1)懶漢式

懶漢式即用到ReferenceBean這個(gè)引入服務(wù)被用到才去封裝瑟曲。

由圖2.7可以看到ReferenceBean實(shí)現(xiàn)了FactoryBean,因此當(dāng)我們依賴ReferenceBean的時(shí)候绪囱,spring會(huì)調(diào)用getObject()方法去獲取真實(shí)的bean测蹲。

ReferenceBean#getObject部分源碼如下:

public Object getObject() throws Exception {
    return get();
}
public synchronized T get() {
     if (destroyed){
         throw new IllegalStateException("Already destroyed!");
     }
     // ReferenceBean 全局變量ref 代表這個(gè)引用bean真實(shí)的業(yè)務(wù)bean
     if (ref == null) {
         init();
     }
     return ref;
}
 private void init() {
     // 做一些創(chuàng)建代理類前的校驗(yàn)和配置操作(比如校驗(yàn)接口合法性、封裝URL參數(shù)到map(比如interface=com.xxService))
     ...
     // 根據(jù)參數(shù)map調(diào)用封裝調(diào)用類
     // 根據(jù)調(diào)用類創(chuàng)建代理類鬼吵,將代理類賦予ref變量   
     ref = createProxy(map);
     ...
}

代碼塊 2.4

可以看到這個(gè)方法就是調(diào)用init()方法初始化,并將引用ref返回, 也就是當(dāng)引入服務(wù)被依賴到的時(shí)候篮赢,會(huì)去封裝調(diào)用類齿椅。

(2)****餓漢式

餓漢式即引用類沒有被依賴也會(huì)

ReferenceBean實(shí)現(xiàn)了InitializingBean,因此初始化ReferenceBean時(shí)启泣,Spring容器會(huì)調(diào)用 ReferenceBean的afterPropertiesSet方法涣脚。

ReferenceBean部分源碼如下:

 public void afterPropertiesSet() throws Exception {
     ····
         裝載 監(jiān)控器、注冊中心信息寥茫、應(yīng)用配置信息遣蚀、消費(fèi)端配置信息等等
     ····    
     Boolean b = isInit();
     if (b == null && getConsumer() != null) {
         b = getConsumer().isInit();
     }
     if (b != null && b.booleanValue()) {
         getObject();
     }
 }

代碼塊 2.5

通過代碼塊2.5 我們可以看到,afterPropertiesSet 方法主要做了一些初始化操作纱耻,最后判斷是否初始化bean, 如果需要?jiǎng)t會(huì)調(diào)用init()方法初始化bean芭梯,賦予ref變量。

ps:

默認(rèn)是關(guān)閉狀態(tài)弄喘,即不會(huì)開啟玖喘。需要初始化可通過配置<dubbo:reference> 的 init 屬性開啟。

2.3.2 調(diào)用類封裝細(xì)節(jié)

服務(wù)引入需要封裝調(diào)用類蘑志,需要做哪些事情累奈?

  1. 獲取提供端地址贬派。封裝調(diào)用類,首先要知道提供者的地址澎媒,并且因?yàn)橛卸喾N類型的服務(wù)引入搞乏,得區(qū)分多種服務(wù)引入方式的服務(wù)地址。
  2. 獲取傳輸協(xié)議戒努。不同的提供者可能支持的傳輸協(xié)議不一致请敦,因此需要獲取傳輸協(xié)議類型。
  3. 根據(jù)獲取到的配置信息創(chuàng)建調(diào)用類柏卤。
  4. 封裝代理類冬三。為了避免造成代碼侵入,不能讓業(yè)務(wù)代碼直接依賴框架封裝的調(diào)用類缘缚,所以需要支持讓業(yè)務(wù)代碼可以直接依賴提供端勾笆。那么RPC框架就需要根據(jù)提供端和調(diào)用類封裝一個(gè)代理類。

2.3.2.1 獲取提供端地址

前面我們講過桥滨,服務(wù)引入分為三種類型(本地引入窝爪、直接引入、基于注冊中心引入)齐媒,三種引入類型獲取的提供端地址也不同蒲每。

  • 本地引入

判斷是否為本地調(diào)用鬼癣,如果是本地調(diào)用則根據(jù)提供端信息拼接URL,格式為injvm:127.0.0.1:0/com.service?param矢劲。

判斷是否為本地調(diào)用流程圖如下:

image.png

圖 2.8

通過流程圖我們可以看到,如果ReferenceBean指定的inJvm=ture或者scope=local則認(rèn)為是本地調(diào)用(通過標(biāo)簽配置)他膳。

否則如果作用域沒有指定remote唬血、并且不是泛化調(diào)用望蜡、并且本地暴露的服務(wù)包含該服務(wù)才認(rèn)為是本地引用。

  • 直接引用

如果判斷不是本地調(diào)用拷恨,則判斷是否存在直接引用地址(通過標(biāo)簽的url指定)脖律。

如果是存在直接引用URL,假設(shè)配置的URL是dubbo協(xié)議的腕侄,則url的格式為 dubbo://service-host/com.service?param小泉。

因?yàn)橹苯右靡部赡苁桥渲米灾行牡刂?/p>

因此Dubbo判斷是直接引用是registry前綴的地址,則會(huì)加上refer參數(shù)冕杠,標(biāo)示實(shí)際調(diào)用哪個(gè)提供者微姊,如下。

  • 基于注冊中心引用

如果沒有指定引用URL拌汇,則會(huì)通過加載注冊中心地址柒桑,獲取到注冊中心的地址集合,URL的格式為

registry://registry-host/org.apache.dubbo.registry.RegistryService?refer=URL.encode("consumer://host/com.Service?version=1.0.0")

即地址為注冊中心地址,refer參數(shù)為實(shí)際引用的提供者

需要注意的是噪舀,直接引用都是可能配置多個(gè)地址的魁淳,而通過注冊中心獲取也可能會(huì)獲取到多個(gè)提供端地址飘诗,因此獲取到的地址可能是多個(gè)的。

2.3.2.2 獲取傳輸協(xié)議

Dubbo針對(duì)不同協(xié)議都封裝了對(duì)應(yīng)的Protocol類界逛,因此本節(jié)分析Dubbo如何根據(jù)當(dāng)前傳輸協(xié)議獲取對(duì)應(yīng)的Protocol類昆稿。

截取ReferenceBean Protocol的獲取如下,

private static final Protocol refprotocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();

我們可以看到ReferenceBean中并沒有固定創(chuàng)建某一個(gè)ReferenceBean的實(shí)現(xiàn)類息拜,那么Dubbo是怎么做到根據(jù)不同傳輸協(xié)議獲取對(duì)應(yīng)Protocol的呢溉潭?

分析getAdaptiveExtension()的實(shí)現(xiàn),該方法最終創(chuàng)建了一個(gè)Protocol的代理對(duì)象少欺,由該代理對(duì)象來根據(jù)當(dāng)前傳輸U(kuò)RL獲取對(duì)應(yīng)的Protocol喳瓣。

該方法內(nèi)部根據(jù)代理的對(duì)象類型(比如:Protocol)動(dòng)態(tài)拼接java代碼,動(dòng)態(tài)拼接code生成自適應(yīng)擴(kuò)展對(duì)象赞别,并動(dòng)態(tài)編譯畏陕,通過類加載器加載到j(luò)vm中,返回代理對(duì)象仿滔。

流程圖如下:

image.png

圖 2.9

Protocol為例子惠毁,拼接后的java代碼如下:

package com.alibaba.dubbo.rpc;
import com.alibaba.dubbo.common.extension.ExtensionLoader;
public class Protocol$Adpative implements com.alibaba.dubbo.rpc.Protocol {
    // 不被代理的方法 如果被調(diào)用直接報(bào)錯(cuò)
    public void destroy() {throw new UnsupportedOperationException("method public abstract void com.alibaba.dubbo.rpc.Protocol.destroy() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
    }
    public int getDefaultPort() {throw new UnsupportedOperationException("method public abstract int com.alibaba.dubbo.rpc.Protocol.getDefaultPort() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
    }
    // 代理導(dǎo)出方法
    public com.alibaba.dubbo.rpc.Exporter export(com.alibaba.dubbo.rpc.Invoker arg0) throws com.alibaba.dubbo.rpc.Invoker {
        if (arg0 == null) throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument == null");
        if (arg0.getUrl() == null) throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument getUrl() == null");com.alibaba.dubbo.common.URL url = arg0.getUrl();
        String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() );
        if(extName == null) throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])");
        com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
        return extension.export(arg0);
    }
    public void destroyServer() {throw new UnsupportedOperationException("method public default void com.alibaba.dubbo.rpc.Protocol.destroyServer() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
    }
    // 代理引入方法
    public com.alibaba.dubbo.rpc.Invoker refer(java.lang.Class arg0, com.alibaba.dubbo.common.URL arg1) throws java.lang.Class {
        if (arg1 == null) throw new IllegalArgumentException("url == null");
        // 獲取url參數(shù)
        com.alibaba.dubbo.common.URL url = arg1;
        // 獲取url上配置的協(xié)議
        String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() );
        if(extName == null) throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])");
        // 根據(jù)協(xié)議名稱 從dubbo容器中獲取對(duì)應(yīng)的協(xié)議類
        com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
        // 根據(jù)協(xié)議類創(chuàng)建調(diào)用對(duì)應(yīng)的服務(wù)引入方法
        return extension.refer(arg0, arg1);
    }
}

dubbo就是通過動(dòng)態(tài)拼接java code,在運(yùn)行時(shí)生成自適應(yīng)擴(kuò)展bean崎页,由這個(gè)bean來獲取ReferenceBean配置的protocol和cluster對(duì)應(yīng)的實(shí)現(xiàn)類.

動(dòng)態(tài)拼接code邏輯如下:

image.png

圖 2.10

主要思想:

通過@Adaptive注解表明哪些方法需要被代理鞠绰,被代理的方法都要能提供URL參數(shù),代理對(duì)象會(huì)根據(jù)URL以被代理類為key飒焦,獲取對(duì)應(yīng)參數(shù)值蜈膨,從而返回對(duì)應(yīng)的實(shí)現(xiàn)類的bean名,再通過bean名從Dubbo容器中獲取對(duì)應(yīng)的實(shí)現(xiàn)累牺荠。如果URL上沒有指明用哪個(gè)實(shí)現(xiàn)類丈挟,則用@SPI注解上的值為key獲取對(duì)應(yīng)的實(shí)現(xiàn)類。

以Protocol為例,源碼如下:

@SPI("dubbo")
public interface Protocol {
    int getDefaultPort();
    @Adaptive
    <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;
    @Adaptive
    <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;
    void destroy();
    void destroyServer();
}

代碼塊 2.6

export和refer方法都被@Adaptive修飾志电,因此這兩個(gè)方法。生成的代理類從URL上獲取以protocol為名的參數(shù)值作為key蛔趴,從dubbo容器中取出對(duì)應(yīng)的實(shí)現(xiàn)類去執(zhí)行挑辆。如果沒有獲取到則以@SPI上的值dubbo作為key取出對(duì)應(yīng)的protocol。即默認(rèn)得到了DubboProtocol孝情。

ps

同樣通過這個(gè)機(jī)制鱼蝉,也進(jìn)而得以支持Spi擴(kuò)展,可以在運(yùn)行時(shí)才確認(rèn)使用哪個(gè)實(shí)現(xiàn)類箫荡,方便外部擴(kuò)展魁亦。比如我新增一個(gè)自定義協(xié)議MyProtocol,配置指定服務(wù)引入的協(xié)議為myProtocol, 并將這個(gè)協(xié)議對(duì)應(yīng)的實(shí)現(xiàn)類基于dubbo的spi擴(kuò)展注入到Dubbo容器中羔挡。那么代理類就可以根據(jù)URL上的協(xié)議名洁奈,基于spi獲取對(duì)應(yīng)的協(xié)議實(shí)現(xiàn)類间唉,再根據(jù)調(diào)用類調(diào)用refer方法。

2.3.2.3 封裝調(diào)用類

獲取到提供者的url和傳輸?shù)膮f(xié)議對(duì)象后利术,就開始封裝調(diào)用對(duì)象了呈野。

封裝調(diào)用對(duì)象需要考慮做幾個(gè)事情

  • 先前基于注冊中心引入服務(wù)的地址,并非最終發(fā)起調(diào)用的協(xié)議和地址印叁,而是以注冊中心地址為路徑被冒,提供端地址為參數(shù)組合。因此需要區(qū)分開來轮蜕,封裝真正的調(diào)用地址昨悼。并且registry協(xié)議并非真正傳輸協(xié)議,只是標(biāo)識(shí)是注冊中心引入跃洛,封裝調(diào)用類還得替換成真正的傳輸協(xié)議率触,比如dubbo協(xié)議。
  • 由于提供者有可能有多個(gè)提供者税课,因此需要考慮如何將多個(gè)提供者封裝成一個(gè)調(diào)用者闲延,發(fā)起調(diào)用時(shí)如何處理。
  • 為了便于知道消費(fèi)端消費(fèi)情況韩玩,消費(fèi)端也需要將消費(fèi)的服務(wù)注冊到注冊中心垒玲。并且為了在提供者發(fā)生變動(dòng)時(shí)收到通知,還需要訂閱提供者的節(jié)點(diǎn)數(shù)據(jù)找颓。

截取ReferenceConfig#createProxy封裝調(diào)用類invoke源碼如下:

// 截取部分注冊中心和直接引用獲取到URL集合后的代碼
if (urls.size() == 1) {
    invoker = refprotocol.refer(interfaceClass, urls.get(0));
} else {
    ....
    // 遍歷多個(gè)url 生成invoker集合
    for (URL url : urls) {
        invokers.add(refprotocol.refer(interfaceClass, url));
    }
     if (registryURL != null) { 
         // 指定Cluster為AvailableCluster 選擇任意可用的服務(wù)
         // 如果是注冊中心則說明當(dāng)前遍歷的是注冊中心地址合愈,所以使用AvailableCluster封裝invoke
         URL u = registryURL.addParameter(Constants.CLUSTER_KEY, AvailableCluster.NAME);
         // 基于Cluster合并多個(gè)invoker 基于集群容錯(cuò)策略調(diào)用
         invoker = cluster.join(new StaticDirectory(u, invokers));
     } else {
         invoker = cluster.join(new StaticDirectory(invokers));
     }
}

代碼塊 2.7

如代碼塊2.7所示,只有一個(gè)URL時(shí)直接通過協(xié)議類封裝一個(gè)invoker對(duì)象击狮,如果有多個(gè)URL(即多個(gè)服務(wù)提供者)則通過Cluster封裝多個(gè)佛析,后續(xù)基于集群容錯(cuò)策略做調(diào)用(關(guān)于Cluster相關(guān)本文不做講解,屬于負(fù)載均衡處理模塊的范疇)彪蓬。

Protocol為例寸莫,調(diào)用該代理對(duì)象refer方法,代理對(duì)象會(huì)解析refer方法傳入的URL不同的協(xié)議獲取到不同的Protocol實(shí)現(xiàn)類档冬,通過對(duì)應(yīng)協(xié)議類****Protocol創(chuàng)建對(duì)應(yīng)invoke實(shí)現(xiàn)類膘茎,不同協(xié)議類會(huì)創(chuàng)建不同的invoke類。

關(guān)于refer方法的實(shí)現(xiàn)酷誓,本地引入和直接引入都是直接根據(jù)URL披坏、interfaceClass創(chuàng)建Invoke實(shí)現(xiàn)類, 重點(diǎn)講一下RegistryProtocol。

RegistryProtocol的refer方法盐数,截取關(guān)鍵源碼如下:

public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
    // 修改url的協(xié)議內(nèi)容 根據(jù)參數(shù)配置的協(xié)議進(jìn)行修改棒拂,如果沒有配置默認(rèn)為dubbo
    url = url.setProtocol(url.getParameter(Constants.REGISTRY_KEY, Constants.DEFAULT_REGISTRY)).removeParameter(Constants.REGISTRY_KEY);
    // 連接注冊中心
    Registry registry = registryFactory.getRegistry(url);
    ...
    return doRefer(cluster, registry, type, url);
}
private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) {
    RegistryDirectory<T> directory = new RegistryDirectory<T>(type, url);
    directory.setRegistry(registry);
    directory.setProtocol(protocol);
    // 生成消費(fèi)服務(wù)地址 如consumer:consumer-host/com.xxService?param  
    // com.xxService表示消費(fèi)的提供者全限制定名
    URL subscribeUrl = new URL(Constants.CONSUMER_PROTOCOL, NetUtils.getLocalHost(), 0, type.getName(), directory.getUrl().getParameters());
    ...
    // 在注冊中心消費(fèi)者目錄注冊消費(fèi)端地址  
    // 例如:在zk注冊消費(fèi)端地址的目錄為 /分組名/服務(wù)權(quán)限定名/consumers/subscribeUrl
    registry.register(subscribeUrl.addParameters(Constants.CATEGORY_KEY, Constants.CONSUMERS_CATEGORY,
                                                     Constants.CHECK_KEY, String.valueOf(false)));
    // 訂閱注冊中心節(jié)點(diǎn)數(shù)據(jù) providers、routers玫氢、configurators
    // 訂閱的時(shí)候會(huì)
    directory.subscribe(subscribeUrl.addParameter(Constants.CATEGORY_KEY, Constants.PROVIDERS_CATEGORY + 
                                                  "," + Constants.CONFIGURATORS_CATEGORY + "," + Constants.ROUTERS_CATEGORY));
    // 這里使用cluster是因?yàn)樽灾行目赡軙?huì)有多個(gè)提供者帚屉,因此返回的invoke是具備選擇提供者能力的invoke
    return cluster.join(directory);
}

代碼塊 2.8

如代碼塊2.8所示谜诫,RegistryProtocol的refer方法主要是做了幾個(gè)事情

  • 將原本url為registry協(xié)議,修改成真正發(fā)起調(diào)用使用的協(xié)議涮阔,默認(rèn)為dubbo猜绣。
  • 連接注冊中心,創(chuàng)建注冊中心實(shí)例敬特。
  • 注冊消費(fèi)端地址掰邢。
  • 訂閱注冊中心提供端和配置數(shù)據(jù)。
  • 基于RegistryDirectory和cluster創(chuàng)建invoke對(duì)象返回伟阔。

思考:

  1. 當(dāng)多注冊中心和多提供者時(shí)辣之,怎么選擇注冊中心和怎么選提供者流程是怎么設(shè)計(jì)?
  2. 注冊中心最大的作用是可以提高服務(wù)調(diào)用的可用性皱炉,某個(gè)提供者服務(wù)掛了之后怀估,自動(dòng)下線該提供者,避免調(diào)用到該提供者合搅,或者流量增大服務(wù)無法應(yīng)對(duì)多搀,動(dòng)態(tài)擴(kuò)容提供者。那么如何在提供者變動(dòng)時(shí)去更新invoke.
image.png

圖 2.10

針對(duì)第一點(diǎn)灾部,dubbo基于多個(gè)注冊中心url生成invoke集合康铭,再通過StaticDirectory包裝,cluster固定使用****AvailableCluster進(jìn)行選擇任意可用的節(jié)點(diǎn)(代碼塊2.7)赌髓。獲取到clusterInvoke之后从藤,再根據(jù)cluster策略(服務(wù)引入配置)選擇一個(gè)提供者。

針對(duì)第二點(diǎn)锁蠕,dubbo處理某個(gè)注冊中心url時(shí)夷野,返回的是通過RegistryDirectory與cluster創(chuàng)建的invoke,RegistryDirectory會(huì)監(jiān)聽注冊中心的通知,動(dòng)態(tài)更新提供者集合荣倾。

dubbo基于RegistryDirectory訂閱注冊中心悯搔,訂閱的時(shí)候會(huì)將當(dāng)前RegistryDirectory作為監(jiān)聽器,當(dāng)訂閱的節(jié)點(diǎn)發(fā)生變動(dòng)的時(shí)候就會(huì)通知RegistryDirectory更新invoke集合舌仍。notity方法源碼截取如下

public synchronized void notify(List<URL> urls) {
    // 1. 根據(jù)URL的協(xié)議類型封裝各種訂閱URL集合
    // 2. 更新configurators URL
    // 3. 更新routers URL
    ...
    // 4. 更新providers URL
    refreshInvoker(invokerUrls);
}
    
private void refreshInvoker(List<URL> invokerUrls){
    ...
    // 將URL列表轉(zhuǎn)成Invoker列表    
    Map<String, Invoker<T>> newUrlInvokerMap = toInvokers(invokerUrls) ;
    ...
    this.urlInvokerMap = newUrlInvokerMap;
    // 關(guān)閉未使用的Invoker 
    destroyUnusedInvokers(oldUrlInvokerMap,newUrlInvokerMap);
}

代碼塊 2.10

2.3.2.4 創(chuàng)建代理類

根據(jù)引用的服務(wù)類和invoke對(duì)象生成代理對(duì)象返回鳖孤。

// ReferenceConfig源碼 代理工廠代理類,根據(jù)url獲取對(duì)應(yīng)代理類
private static final ProxyFactory proxyFactory = ExtensionLoader.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension();
proxyFactory.getProxy(invoker);

代碼塊 2.9

默認(rèn)代理方式支持Javassist和Jdk代理抡笼,可以通過引用標(biāo)簽配置proxy指定。

與上面講述的Protocol一樣黄鳍,是一個(gè)動(dòng)態(tài)生成的類推姻,會(huì)根據(jù)url上的參數(shù)獲取對(duì)應(yīng)的動(dòng)態(tài)代理實(shí)現(xiàn)類。

為什么要?jiǎng)?chuàng)建代理類框沟?

假設(shè)我們不創(chuàng)建代理類藏古,那么生成的就是invoke對(duì)象增炭,客戶端就不能通過直接注入提供服務(wù)類方式,而是要依賴invoke對(duì)象拧晕,造成代碼入侵隙姿。

有了代理類我們就可以直接注入提供者,實(shí)際上調(diào)用的時(shí)候就是通過invoke發(fā)起調(diào)用了厂捞。

2.4 拓展思考回顧

  1. 本地通過<dubbo:provider>暴露了dubbo服務(wù)输玷,那么我們調(diào)用本地dubbo服務(wù)時(shí)是否會(huì)發(fā)起網(wǎng)絡(luò)請求?
  2. 通過手動(dòng)創(chuàng)建T****estService注入spring容器,又通過<dubbo:reference>引用服務(wù)T****estService靡馁,依賴注入獲取到的是哪個(gè)bean欲鹏?
  3. 使用懶漢式時(shí),只通過Spring注入引用類就不會(huì)立即創(chuàng)建調(diào)用類臭墨,而是實(shí)際用到才創(chuàng)建嗎赔嚎?

問題解答:

  1. 不會(huì),使用的是本地服務(wù)調(diào)用
  2. 以注入beanName為主,beanName無法對(duì)應(yīng)則隨機(jī)取一個(gè)胧弛。
  3. 會(huì)取封裝調(diào)用類尤误。

針對(duì)第三個(gè)問題進(jìn)行解析,在依賴注入的時(shí)候结缚,注入的bean就會(huì)被加載了损晤,因此ReferenceBean實(shí)現(xiàn)的getObject方法就會(huì)被調(diào)用,調(diào)用類也會(huì)被封裝創(chuàng)建掺冠。

ReferenceBean****懶加載常規(guī)情況下沉馆,只能保證當(dāng)你服務(wù)中沒有依賴引入的服務(wù)時(shí),保證getObject不會(huì)被執(zhí)行德崭。

這時(shí)有人可能想問了斥黑,ReferenceBeanBeanDefinition不是都加入到Spring容器中了嗎,Spring容器不是會(huì)對(duì)進(jìn)行所有BeanDefinition進(jìn)行初始化創(chuàng)建嗎眉厨?

其實(shí)Spring加載所有BeanDefinition去創(chuàng)建時(shí)锌奴,BeanDefinition因?yàn)楸旧韺?shí)際上是ReferenceBean,會(huì)先以 &beanName 創(chuàng)建ReferenceBean本身憾股。然后再判斷要不要是否需要早期初始化鹿蜀,如果需要才會(huì)去創(chuàng)建真實(shí)的bean。

所以如果在沒有被依賴的情況下服球,也就不會(huì)以beanName去創(chuàng)建bean茴恰,所以也就不會(huì)去調(diào)用getObject。

public void preInstantiateSingletons() throws BeansException {
        ...
        for (String beanName : beanNames) {
            RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
            // 是否為懶加載
            if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
                // 判斷是否為FactoryBean
                if (isFactoryBean(beanName)) {
                    // FACTORY_BEAN_PREFIX = & 
                    // &beanName 表示獲取實(shí)現(xiàn)FactoryBean的類本身
                    final FactoryBean<?> factory = (FactoryBean<?>) getBean(FACTORY_BEAN_PREFIX + beanName);
                    // 判斷是否是SmartFactoryBean 
                    // 如果只是實(shí)現(xiàn)FactoryBean斩熊,則默認(rèn)不會(huì)去創(chuàng)建真實(shí)的object
                    boolean isEagerInit = (factory instanceof SmartFactoryBean &&
                                ((SmartFactoryBean<?>) factory).isEagerInit());
                    // 不需要早期初始化 因此不會(huì)去調(diào)用getObject
                    if (isEagerInit) {
                        // 獲取beanName對(duì)應(yīng)的bean 真正創(chuàng)建調(diào)用getObject創(chuàng)建bean
                        getBean(beanName);
                    }
                }
                ....
            }
        }
    }

怎么實(shí)現(xiàn)懶漢式加載ReferenceBean****往枣,但是又要依賴引用類?

可以搭配@Lazy使用,讓依賴的bean被懶加載分冈,這時(shí)獲取到的是懶加載bean代理類圾另,只有真正發(fā)起調(diào)用時(shí)才會(huì)去獲取bean,這樣就可以實(shí)現(xiàn)在真正發(fā)起調(diào)用才調(diào)用getObject創(chuàng)建服務(wù)引用調(diào)用類雕沉。

三集乔、總結(jié)

服務(wù)引入最基本的實(shí)現(xiàn)就是根據(jù)提供者信息封裝成一個(gè)調(diào)用類,但是作為一個(gè)優(yōu)秀的RPC框架坡椒,得考慮方方面面的問題扰路。

  • 避免代碼侵入。為引入服務(wù)生成代理類肠牲。
  • 提高可用性幼衰。引入了注冊中心處理機(jī)制。
  • 提高擴(kuò)展性缀雳。引入了自定義適應(yīng)類渡嚣,根據(jù)url參數(shù)自動(dòng)選擇對(duì)應(yīng)的實(shí)現(xiàn)類。同時(shí)也牽扯到Dubbo實(shí)現(xiàn)了自己的IOC容器肥印。
  • 提高啟動(dòng)性能识椰,避免加載無效引入。引入了懶加載機(jī)制深碱。
  • Spring作為廣泛使用的框架如何接入啟動(dòng)腹鹉。基于Spring的擴(kuò)展機(jī)制敷硅,實(shí)現(xiàn)了一套加載機(jī)制功咒。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市绞蹦,隨后出現(xiàn)的幾起案子力奋,更是在濱河造成了極大的恐慌,老刑警劉巖幽七,帶你破解...
    沈念sama閱讀 211,743評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件景殷,死亡現(xiàn)場離奇詭異,居然都是意外死亡澡屡,警方通過查閱死者的電腦和手機(jī)猿挚,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來驶鹉,“玉大人绩蜻,你說我怎么就攤上這事∈衣瘢” “怎么了辜羊?”我有些...
    開封第一講書人閱讀 157,285評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵踏兜,是天一觀的道長。 經(jīng)常有香客問我八秃,道長,這世上最難降的妖魔是什么肉盹? 我笑而不...
    開封第一講書人閱讀 56,485評(píng)論 1 283
  • 正文 為了忘掉前任昔驱,我火速辦了婚禮,結(jié)果婚禮上上忍,老公的妹妹穿的比我還像新娘骤肛。我一直安慰自己,他們只是感情好窍蓝,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,581評(píng)論 6 386
  • 文/花漫 我一把揭開白布腋颠。 她就那樣靜靜地躺著,像睡著了一般吓笙。 火紅的嫁衣襯著肌膚如雪淑玫。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,821評(píng)論 1 290
  • 那天面睛,我揣著相機(jī)與錄音絮蒿,去河邊找鬼。 笑死叁鉴,一個(gè)胖子當(dāng)著我的面吹牛土涝,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播幌墓,決...
    沈念sama閱讀 38,960評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼但壮,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了常侣?” 一聲冷哼從身側(cè)響起蜡饵,我...
    開封第一講書人閱讀 37,719評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎袭祟,沒想到半個(gè)月后验残,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,186評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡巾乳,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,516評(píng)論 2 327
  • 正文 我和宋清朗相戀三年您没,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片胆绊。...
    茶點(diǎn)故事閱讀 38,650評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡氨鹏,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出压状,到底是詐尸還是另有隱情仆抵,我是刑警寧澤跟继,帶...
    沈念sama閱讀 34,329評(píng)論 4 330
  • 正文 年R本政府宣布,位于F島的核電站镣丑,受9級(jí)特大地震影響舔糖,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜莺匠,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,936評(píng)論 3 313
  • 文/蒙蒙 一金吗、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧趣竣,春花似錦摇庙、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,757評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至单匣,卻和暖如春夕凝,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背封孙。 一陣腳步聲響...
    開封第一講書人閱讀 31,991評(píng)論 1 266
  • 我被黑心中介騙來泰國打工迹冤, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人虎忌。 一個(gè)月前我還...
    沈念sama閱讀 46,370評(píng)論 2 360
  • 正文 我出身青樓泡徙,卻偏偏與公主長得像,于是被迫代替她去往敵國和親膜蠢。 傳聞我的和親對(duì)象是個(gè)殘疾皇子堪藐,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,527評(píng)論 2 349