基于動態(tài)代理 Mock dubbo 服務(wù)的實(shí)現(xiàn)方案

原文鏈接:https://tech.youzan.com/ji-yu-dong-tai-dai-li-mock-dubbofu-wu-de-shi-xian-fang-an/

序言

背景概述

公司目前 Java 項(xiàng)目提供服務(wù)都是基于 Dubbo 框架的,而且 Dubbo 框架已經(jīng)成為大部分國內(nèi)互聯(lián)網(wǎng)公司選擇的一個(gè)基礎(chǔ)組件稚照。

在日常項(xiàng)目協(xié)作過程中,其實(shí)會碰到服務(wù)不穩(wěn)定、不滿足需求場景等情況针贬,很多開發(fā)都會通過在本地使用 Mocktio 等單測工具作為自測輔助。那么蝗蛙,在聯(lián)調(diào)、測試等協(xié)作過程中怎么處理衫仑?

其實(shí),Dubbo 開發(fā)者估計(jì)也是遇到了這樣的問題堕花,所以提供了一個(gè)提供泛化服務(wù)注冊的入口文狱。但是在服務(wù)發(fā)現(xiàn)的時(shí)候有個(gè)弊端,就說通過服務(wù)發(fā)現(xiàn)去請求這個(gè) Mock 服務(wù)的話缘挽,在注冊中心必須只有一個(gè)服務(wù)有效瞄崇,否則消費(fèi)者會請求到其他非Mock服務(wù)上去。

為了解決這個(gè)問題壕曼,Dubbo 開發(fā)者又提供了泛化調(diào)用的入口苏研。既支持通過注冊中心發(fā)現(xiàn)服務(wù),又支持通過 IP+PORT 去直接調(diào)用服務(wù)腮郊,這樣就能保證消費(fèi)者調(diào)用的是 Mock 出來的服務(wù)了摹蘑。

以上泛化服務(wù)注冊和泛化服務(wù)調(diào)用結(jié)合起來,看似已經(jīng)是一個(gè)閉環(huán)轧飞,可以解決 Dubbo 服務(wù)的 Mock 問題衅鹿。但是,結(jié)合日常工作使用時(shí)过咬,會出現(xiàn)一些麻煩的問題:

  • 服務(wù)提供方使用公用的注冊中心大渤,消費(fèi)方無法準(zhǔn)確調(diào)用
  • 消費(fèi)者不可能更改代碼,去直連 Mock 服務(wù)
  • 使用私有注冊中心能解決以上問題掸绞,但是 Mock 最小緯度為 Method兼犯,一個(gè) Service 中被 Mock 的 Method 會正常處理,沒有被 Mock 的 Method 會異常集漾,導(dǎo)致服務(wù)方需要 Mock Service 的全部方法

在解決以上麻煩的前提下切黔,為了能快速注冊一個(gè)需要的 Dubbo 服務(wù),提高項(xiàng)目協(xié)作過程中的工作效率具篇,開展了 Mock 工廠的設(shè)計(jì)與實(shí)現(xiàn)纬霞。

功能概述

  • Mock Dubbo 服務(wù)
  • 單個(gè)服務(wù)器,支持部署多個(gè)相同和不同的 Service
  • 動態(tài)上驱显、下線服務(wù)
  • 非 Mock 的 Method 透傳到基礎(chǔ)服務(wù)

一诗芜、方案探索

1.1 基于 Service Chain 選擇 Mock 服務(wù)的實(shí)現(xiàn)方式

1.1.1 Service Chain 簡單介紹

在業(yè)務(wù)發(fā)起的源頭添加 Service Chain 標(biāo)識,這些標(biāo)識會在接下來的跨應(yīng)用遠(yuǎn)程調(diào)用中一直透傳并且基于這些標(biāo)識進(jìn)行路由埃疫,這樣我們只需要把涉及到需求變更的應(yīng)用的實(shí)例單獨(dú)部署伏恐,并添加到 Service Chain 的數(shù)據(jù)結(jié)構(gòu)定義里面,就可以虛擬出一個(gè)邏輯鏈路栓霜,該鏈路從邏輯上與其他鏈路是完全隔離的翠桦,并且可以共享那些不需要進(jìn)行需求變更的應(yīng)用實(shí)例。

根據(jù)當(dāng)前調(diào)用的透傳標(biāo)識以及 Service Chain 的基礎(chǔ)元數(shù)據(jù)進(jìn)行路由,路由原則如下:

  • 當(dāng)前調(diào)用包含 Service Chain 標(biāo)識销凑,則路由到歸屬于該 Service Chain 的任意服務(wù)節(jié)點(diǎn)丛晌,如果沒有歸屬于該
  • Service Chain 的服務(wù)節(jié)點(diǎn),則排除掉所有隸屬于 Service Chain 的服務(wù)節(jié)點(diǎn)之后路由到任意服務(wù)節(jié)點(diǎn)
  • 當(dāng)前調(diào)用沒有包含 Service Chain 標(biāo)識斗幼,則排除掉所有隸屬于 Service Chain 的服務(wù)節(jié)點(diǎn)之后路由到任意服務(wù)節(jié)點(diǎn)
  • 當(dāng)前調(diào)用包含 Service Chain 標(biāo)識澎蛛,并且當(dāng)前應(yīng)用也屬于某個(gè) Service Chain 時(shí),如果兩者不等則拋出路由異常

以 Dubbo 框架為例蜕窿,給出了一個(gè) Service Chain 實(shí)現(xiàn)架構(gòu)圖(下圖來自有贊架構(gòu)團(tuán)隊(duì))

[圖片上傳失敗...(image-cbfbd7-1617204457869)]

1.1.2 Mock 服務(wù)實(shí)現(xiàn)設(shè)計(jì)方案

方案一谋逻、基于 GenericService 生成需要 Mock 接口的泛化實(shí)現(xiàn),并注冊到 ETCD 上(主要實(shí)現(xiàn)思路如下圖所示)桐经。
image

方案二斤贰、使用 Javassist,生成需要mock接口的Proxy實(shí)現(xiàn)次询,并注冊到 ETCD 上(主要實(shí)現(xiàn)思路如下圖所示)。
image

1.1.3 設(shè)計(jì)方案比較

方案一優(yōu)點(diǎn):實(shí)現(xiàn)簡單瓷叫,能滿足mock需求

  • 繼承 GenericService屯吊,只要實(shí)現(xiàn)一個(gè) $invoke(String methodName, String[] parameterTypes, Object[] objects),可以根據(jù)具體請求參數(shù)做出自定義返回信息摹菠。
  • 接口信息只要知道接口名盒卸、protocol 即可。
  • 即使該服務(wù)已經(jīng)存在次氨,也能因?yàn)?generic 字段蔽介,讓消費(fèi)者優(yōu)先消費(fèi)該 mock service。

缺點(diǎn):與公司的服務(wù)發(fā)現(xiàn)機(jī)制沖突

由于有贊服務(wù)背景煮寡,在使用 Haunt 服務(wù)發(fā)現(xiàn)時(shí)虹蓄,是會同時(shí)返回正常服務(wù)和帶有 Service Chain 標(biāo)記的泛化服務(wù),所以必然存在兩種類型的服務(wù)幸撕。導(dǎo)致帶有 Service Chain 標(biāo)記的消費(fèi)者在正常請求泛化服務(wù)時(shí)報(bào) no available invoke薇组。 例:注冊了 2個(gè) HelloService:

  • 正常的 :generic=false&interface=com.alia.api.HelloService&methods=doNothing,say,age
  • 泛化的:generic=true&interface=com.alia.api.HelloService&methods=*

在服務(wù)發(fā)現(xiàn)的時(shí)候,RegistryDirectory 中有個(gè) map坐儿,保存了所有 Service 的注冊信息律胀。也就是說, method=* 和正常 method=doNothing,say,age 被保存在了一起貌矿。
image

客戶端請求服務(wù)的時(shí)候炭菌,優(yōu)先匹配到正常的服務(wù)的 method,而不會去調(diào)用泛化服務(wù)逛漫。 導(dǎo)致結(jié)果:訪問時(shí)黑低,會跳過 genericFilter,報(bào) no available invoke酌毡。

方案二優(yōu)點(diǎn):Proxy 實(shí)現(xiàn)投储,自動生成一個(gè)正常的 Dubbo 接口實(shí)現(xiàn)

1.Javassist 有現(xiàn)成的方法生成接口實(shí)現(xiàn)字節(jié)碼第练,大大簡化了對用戶代碼依賴。例如:

  • 返回 String玛荞、Json 等娇掏,對單 method 的 mock 實(shí)現(xiàn),都無需用戶上傳實(shí)現(xiàn)類勋眯。
  • 透傳時(shí)統(tǒng)一由平臺控制婴梧,不配置 mock 的方法默認(rèn)就會進(jìn)行透傳,而且保留 Service Chain 標(biāo)記客蹋。

2.Mock 服務(wù)注冊 method 信息完整塞蹭。
3.生成接口 Proxy 對象時(shí),嚴(yán)格按照接口定義進(jìn)行生成讶坯,返回?cái)?shù)據(jù)類型有保障番电。

缺點(diǎn):

  • 無優(yōu)先消費(fèi)選擇功能。
  • 字節(jié)碼后臺生成辆琅,不利于排查生成的 Proxy 中存在問題漱办。

1.1.4 選擇結(jié)果

由于做為平臺,不僅僅需要滿足 mock 需求婉烟,還需要減少用戶操作娩井,以及支持現(xiàn)有公司服務(wù)架構(gòu)體系,所以選擇設(shè)計(jì)方案二似袁。

1.2 基于動態(tài)代理結(jié)合 ServiceConfig 實(shí)現(xiàn)動態(tài)上洞辣、下線服務(wù)

1.2.1 Dubbo 暴露服務(wù)的過程介紹

image

上圖(來自 dubbo 開發(fā)者文檔)暴露服務(wù)時(shí)序圖: 首先 ServiceConfig 類拿到對外提供服務(wù)的實(shí)際類 ref(如:StudentInfoServiceImpl),然后通過 ProxyFactory 類的 getInvoker 方法使用 ref 生成一個(gè) AbstractProxyInvoker 實(shí)例。到這一步就完成具體服務(wù)到 Invoker 的轉(zhuǎn)化昙衅。接下來就是 Invoker 轉(zhuǎn)換到 Exporter 的過程,Exporter 會通過轉(zhuǎn)化為 URL 的方式暴露服務(wù)扬霜。 從 dubbo 源碼來看,dubbo 通過 Spring 框架提供的 Schema 可擴(kuò)展機(jī)制而涉,擴(kuò)展了自己的配置支持畜挥。dubbo-container 通過封裝 Spring 容器,來啟動了 Spring 上下文婴谱,此時(shí)它會去解析 Spring 的 bean 配置文件(Spring 的 xml 配置文件)蟹但,當(dāng)解析 dubbo:service 標(biāo)簽時(shí),會用 dubbo 自定義 BeanDefinitionParser 進(jìn)行解析谭羔。dubbo 的 BeanDefinitonParser 實(shí)現(xiàn)為 DubboBeanDefinitionParser华糖。 Spring.handlers 文件:http://code.alibabatech.com/schema/dubbo=com.alibaba.dubbo.config.spring.schema.DubboNamespaceHandler

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

DubboBeanDefinitionParser 會將配置標(biāo)簽進(jìn)行解析,并生成對應(yīng)的 Javabean,最終注冊到 Spring Ioc 容器中瘟裸。 對 ServiceBean 進(jìn)行注冊時(shí)客叉,其 implements InitializingBean 接口,當(dāng) bean 完成注冊后,會調(diào)用 afterPropertiesSet() 方法兼搏,該方法中調(diào)用 export() 完成服務(wù)的注冊卵慰。在 ServiceConfig 中的 doExport() 方法中,會對服務(wù)的各個(gè)參數(shù)進(jìn)行校驗(yàn)佛呻。

    if(this.ref instanceof GenericService) {
        this.interfaceClass = GenericService.class;
        this.generic = true;
    } else {
        try {
            this.interfaceClass = Class.forName(this.interfaceName, true, Thread.currentThread().getContextClassLoader());
        } catch (ClassNotFoundException var5) {
            throw new IllegalStateException(var5.getMessage(), var5);
        }
        this.checkInterfaceAndMethods(this.interfaceClass, this.methods);
        this.checkRef();
        this.generic = false;
    }

注冊過程中會進(jìn)行判斷該實(shí)現(xiàn)類的類型裳朋。其中如果實(shí)現(xiàn)了 GenericService 接口,那么會在暴露服務(wù)信息時(shí)吓著,將 generic 設(shè)置為 true鲤嫡,暴露方法就為*。如果不是绑莺,就會按正常服務(wù)進(jìn)行添加服務(wù)的方法暖眼。此處就是我們可以實(shí)現(xiàn) Mock 的切入點(diǎn),使用 Javassist 根據(jù)自定義的 Mock 信息纺裁,寫一個(gè)實(shí)現(xiàn)類的 class 文件并生成一個(gè)實(shí)例注入到 ServiceConfig 中诫肠。生成 class 實(shí)例如下所示,與一個(gè)正常的實(shí)現(xiàn)類完全一致欺缘,以及注冊的服務(wù)跟正常服務(wù)也完全一致栋豫。

    package 123.com.youzan.api;
    import com.youzan.api.StudentInfoService;
    import com.youzan.pojo.Pojo;
    import com.youzan.test.mocker.internal.common.reference.ServiceReference;

    public class StudentInfoServiceImpl implements StudentInfoService {
        private Pojo getNoValue0;
        private Pojo getNoValue1;
        private ServiceReference service;
        public void setgetNoValue0(Pojo var1) {
            this.getNoValue0 = var1;
        }
        public void setgetNoValue1(Pojo var1) {
            this.getNoValue1 = var1;
        }
        public Pojo getNo(int var1) {
            return var1 == 1 ? this.getNoValue0 : this.getNoValue1;
        }
        public void setService(ServiceReference var1) {
            this.service = var1;
        }
        public double say() {
            return (Double)this.service.reference("say", "", (Object[])null);
        }
        public void findInfo(String var1, long var2) {
            this.service.reference("findInfo", "java.lang.String,long", new Object[]{var1, new Long(var2)});
        }
        public StudentInfoServiceImpl() {}
       }

使用 ServiceConfig 將自定義的實(shí)現(xiàn)類注入,并完成注冊浪南,實(shí)現(xiàn)如下:

    void registry(Object T, String sc) {
        service.setFilter("request")
        service.setRef(T)
        service.setParameters(new HashMap<String, String>())
        service.getParameters().put(Constants.SERVICE_CONFIG_PARAMETER_SERVICE_CHAIN_NAME, sc)
        service.export()
        if (service.isExported()) {
            log.warn "發(fā)布成功 : ${sc}-${service.interface}"
        } else {
            log.error "發(fā)布失敗 : ${sc}-${service.interface}"
        }
    }

通過service.setRef(genericService)完成實(shí)現(xiàn)類的注入,最終通過service.export()完成服務(wù)注冊漱受。ref 的值已經(jīng)被塞進(jìn)來络凿,并附帶 ServiceChain 標(biāo)記保存至 service 的 paramters 中。具體服務(wù)到 Invoker 的轉(zhuǎn)化以及 Invoker 轉(zhuǎn)換到 Exporter昂羡,Exporter 到 URL 的轉(zhuǎn)換都會附帶上 ServiceChain 標(biāo)記注冊到注冊中心絮记。

1.2.2 生成實(shí)現(xiàn)類設(shè)計(jì)方案

方案一、 支持指定 String(或 Json) 對單個(gè) method 進(jìn)行 mock虐先。

功能介紹:根據(jù)入?yún)?String or Json怨愤,生成代理對象。由 methodName 和 methodParams 獲取唯一 method 定義蛹批。(指支持單個(gè)方法mock)撰洗。消費(fèi)者請求到Mock服務(wù)的對應(yīng)Mock Method時(shí),Mock服務(wù)將保存的數(shù)據(jù)轉(zhuǎn)成對應(yīng)的返回類型腐芍,并返回差导。

方案二、 支持指定 String(或 Json) 對多個(gè) method生成 mock猪勇。

功能介紹:根據(jù)入?yún)?String or Json设褐,生成代理對象。method 對應(yīng)的 mock 數(shù)據(jù)由 methodMockMap 指定,由 methodName 獲取唯一 method 定義助析,所以被 mock 接口不能有重載方法(只支持多個(gè)不同方法 mock)犀被。消費(fèi)者請求到 Mock 服務(wù)的對應(yīng) mock method 時(shí),Mock 服務(wù)將保存的數(shù)據(jù)轉(zhuǎn)成對應(yīng)的返回類型外冀,并返回寡键。

方案三、 在使用 實(shí)現(xiàn)類(Impl) 的情況下锥惋,支持傳入一個(gè)指定的 method 進(jìn)行 mock昌腰。

功能介紹:根據(jù)入?yún)⒌膶?shí)現(xiàn)類,生成代理對象膀跌。由 methodName 和 methodParams 獲取唯一 method 定義遭商。(支持 mock 一個(gè)方法)。消費(fèi)者請求到 Mock 服務(wù)的對應(yīng) mock method 時(shí)捅伤,Mock 服務(wù)調(diào)用該實(shí)現(xiàn)類的對應(yīng)方法劫流,并返回。

方案四丛忆、 在使用 實(shí)現(xiàn)類(Impl) 的情況下祠汇,支持傳入多個(gè) method 進(jìn)行 mock。

功能介紹:根據(jù)入?yún)⒌膶?shí)現(xiàn)類熄诡,生成代理對象可很。由 methodName 獲取唯一 method 定義,所以被 mock 接口不能有重載方法(只支持一個(gè)實(shí)現(xiàn)類 mock 多個(gè)方法)凰浮。消費(fèi)者請求到 Mock 服務(wù)的對應(yīng) mock method 時(shí)我抠,Mock 服務(wù)調(diào)用該實(shí)現(xiàn)類的對應(yīng)方法,并返回袜茧。

方案五菜拓、 使用 Custom Reference 對多個(gè) method 進(jìn)行 mock。

功能介紹:根據(jù)入?yún)?ServiceReference笛厦,生成代理對象纳鼎。method 對應(yīng)的自定義 ServiceReference 由 methodMockMap 指定,由 methodName 獲取唯一method定義裳凸,所以被 mock 接口不能有重載方法(只支持多個(gè)不同方法 mock)贱鄙。消費(fèi)者請求到 Mock 服務(wù)的對應(yīng) mock method 時(shí),Mock 服務(wù)會主動請求自定義的 Dubbo 服務(wù)姨谷。

1.2.3 設(shè)計(jì)方案選擇

以上五種方案贰逾,其實(shí)就是整個(gè) Mock 工廠實(shí)現(xiàn)的一個(gè)迭代過程。在每個(gè)方案的嘗試中菠秒,發(fā)現(xiàn)各自的弊端然后出現(xiàn)了下一種方案疙剑。目前氯迂,在結(jié)合各種使用場景后,選擇了方案二言缤、方案五嚼蚀。

方案三、方案四被排除的主要原因:Dubbo 對已經(jīng)發(fā)布的 Service 保存了實(shí)現(xiàn)類的 ClassLoader管挟,相同 className 的類一旦注冊成功后轿曙,會將實(shí)現(xiàn)類的 ClassLoader 保存到內(nèi)存中,很難被刪除僻孝。所以想要使用這兩種方案的話导帝,需要頻繁變更實(shí)現(xiàn)類的 className,大大降低了一個(gè)工具的易用性穿铆。改用自定義 Dubbo 服務(wù)(方案五)您单,替代自定義實(shí)現(xiàn)類,但是需要使用者自己起一個(gè) Dubbo 服務(wù)荞雏,并告知 IP+PORT虐秦。

方案一其實(shí)是方案二的補(bǔ)集,能支持 Service 重載方法的 Mock凤优。由于在使用時(shí)悦陋,需要傳入具體 Method 的簽名信息,增加了用戶操作成本筑辨。由于公司內(nèi)部保證一個(gè) Service 不可能有重載方法俺驶,且為了提高使用效率,不開放該方案棍辕。后期如果出現(xiàn)這樣的有重載方法的情況暮现,再進(jìn)行開放。

1.2.4 遇到的坑

基礎(chǔ)數(shù)據(jù)類型需要特殊處理

使用 Javassist 根據(jù)接口 class 寫一個(gè)實(shí)現(xiàn)類的 class 文件痢毒,遇到最讓人頭疼的就是方法簽名和返回值送矩。如果方法的簽名和返回值為基礎(chǔ)數(shù)據(jù)類型時(shí)蚕甥,那在傳參和返回時(shí)需要做特殊處理哪替。平臺中本人使用了最笨的枚舉處理方法,如果有使用 Javassist 的高手菇怀,有好的建議麻煩不吝賜教凭舶。代碼如下:

    /** 參數(shù)存在基本數(shù)據(jù)類型時(shí),默認(rèn)使用基本數(shù)據(jù)類型
     * 基本類型包含:
     * 實(shí)數(shù):double爱沟、float
     * 整數(shù):byte帅霜、short、int呼伸、long
     * 字符:char
     * 布爾值:boolean
     * */
    private static CtClass getParamType(ClassPool classPool, String paramType) {
        switch (paramType) {
            case "char":
                return CtClass.charType
            case "byte":
                return CtClass.byteType
            case "short":
                return CtClass.shortType
            case "int":
                return CtClass.intType
            case "long":
                return CtClass.longType
            case "float":
                return CtClass.floatType
            case "double":
                return CtClass.doubleType
            case "boolean":
                return CtClass.booleanType
            default:
                return classPool.get(paramType)
        }
    }

1.3 非 Mock 的 Method 透傳到基礎(chǔ)服務(wù)

1.3.1 Dubbo 服務(wù)消費(fèi)的過程介紹

image

在消費(fèi)端:Spring 解析 dubbo:reference 時(shí)身冀,Dubbo 首先使用 com.alibaba.dubbo.config.spring.schema.NamespaceHandler 注冊解析器钝尸,當(dāng) Spring 解析 xml 配置文件時(shí)就會調(diào)用這些解析器生成對應(yīng)的 BeanDefinition 交給 Spring 管理。Spring 在初始化 IOC 容器時(shí)會利用這里注冊的 BeanDefinitionParser 的 parse 方法獲取對應(yīng)的 ReferenceBean 的 BeanDefinition 實(shí)例搂根,由于 ReferenceBean 實(shí)現(xiàn)了 InitializingBean 接口珍促,在設(shè)置了 Bean 的所有屬性后會調(diào)用 afterPropertiesSet 方法。afterPropertiesSet 方法中的 getObject 會調(diào)用父類 ReferenceConfig 的 init 方法完成組裝剩愧。ReferenceConfig 類的 init 方法調(diào)用 Protocol 的 refer 方法生成 Invoker 實(shí)例猪叙,這是服務(wù)消費(fèi)的關(guān)鍵。接下來把 Invoker 轉(zhuǎn)換為客戶端需要的接口(如:StudentInfoService)仁卷。由 ReferenceConfig 切入穴翩,通過 API 方式使用 Dubbo 的泛化調(diào)用,代碼如下:

Object reference(String s, String paramStr, Object[] objects) {
    if (StringUtils.isEmpty(serviceInfoDO.interfaceName) || serviceInfoDO.interfaceName.length() <= 0) {
        throw new NullPointerException("The 'interfaceName' should not be ${serviceInfoDO.interfaceName}, please make sure you have the correct 'interfaceName' passed in")
    }

    // set interface name
    referenceConfig.setInterface(serviceInfoDO.interfaceName)
    referenceConfig.setApplication(serviceInfoDO.applicationConfig)
    // set version
    if (serviceInfoDO.version != null && serviceInfoDO.version != "" && serviceInfoDO.version.length() > 0) {
        referenceConfig.setVersion(serviceInfoDO.version)
    }
    if (StringUtils.isEmpty(serviceInfoDO.refUrl) || serviceInfoDO.refUrl.length() <= 0) {
        throw new NullPointerException("The 'refUrl' should not be ${serviceInfoDO.refUrl} , please make sure you have the correct 'refUrl' passed in")
    }
    //set refUrl
    referenceConfig.setUrl(serviceInfoDO.refUrl)
    reference.setGeneric(true)// 聲明為泛化接口

     //使用com.alibaba.dubbo.rpc.service.GenericService可以代替所有接口引用
    GenericService genericService = reference.get()

    String[] strs = null

    if(paramStr != ""){
        strs = paramStr.split(",")
    }

    Object result = genericService.$invoke(s, strs, objects)

     // 返回值類型不定锦积,需要做特殊處理
    if (result.getClass().isAssignableFrom(HashMap.class)) {
        Class dtoClass = Class.forName(result.get("class"))
        result.remove("class")
        String resultJson = JSON.toJSONString(result)

        return JSON.parseObject(resultJson, dtoClass)
    }

    return result
}

如上代碼所示芒帕,具體業(yè)務(wù) DTO 類型,泛化調(diào)用結(jié)果非僅結(jié)果數(shù)據(jù)充包,還包含 DTO 的 class 信息副签,需要特殊處理結(jié)果,取出需要的結(jié)果進(jìn)行返回基矮。

1.3.2 記錄dubbo服務(wù)請求設(shè)計(jì)方案

方案一淆储、捕獲請求信息

服務(wù)提供方和服務(wù)消費(fèi)方調(diào)用過程攔截,Dubbo 本身的大多功能均基于此擴(kuò)展點(diǎn)實(shí)現(xiàn)家浇,每次遠(yuǎn)程方法執(zhí)行本砰,該攔截都會被執(zhí)行。Provider 提供的調(diào)用鏈钢悲,具體的調(diào)用鏈代碼是在 ProtocolFilterWrapper 的 buildInvokerChain 完成的点额,具體是將注解中含有 group=provider 的 Filter 實(shí)現(xiàn),按照 order 排序莺琳,最后的調(diào)用順序是 EchoFilter->ClassLoaderFilter->GenericFilter->ContextFilter->ExceptionFilter->TimeoutFilter->MonitorFilter->TraceFilter还棱。 其中:EchoFilter 的作用是判斷是否是回聲測試請求,是的話直接返回內(nèi)容惭等≌涫郑回聲測試用于檢測服務(wù)是否可用,回聲測試按照正常請求流程執(zhí)行辞做,能夠測試整個(gè)調(diào)用是否通暢琳要,可用于監(jiān)控。ClassLoaderFilter 則只是在主功能上添加了功能秤茅,更改當(dāng)前線程的 ClassLoader稚补。

在 ServiceConfig 繼承 AbstractInterfaceConfig,中有 filter 屬性框喳。以此為切入點(diǎn)课幕,給每個(gè) Mock 服務(wù)添加 filter,記錄每次 dubbo 服務(wù)請求信息(接口厦坛、方法、入?yún)⒄Ь⒎祷胤喟恪㈨憫?yīng)時(shí)長)。

方案二污桦、記錄請求信息

將請求信息保存在內(nèi)存中亩歹,一個(gè)接口的每個(gè)被 Mock 的方法保存近 10次 記錄信息。使用二級緩存保存凡橱,緩存代碼如下:

    @Singleton(lazy = true)
    class CacheUtil {
        private static final Object PRESENT = new Object()
        private int maxInterfaceSize = 10000    // 最大接口緩存數(shù)量
        private int maxRequestSize = 10         // 最大請求緩存數(shù)量
        private Cache<String, Cache<RequestDO, Object>> caches = CacheBuilder.newBuilder()
                .maximumSize(maxInterfaceSize)
                .expireAfterAccess(7, TimeUnit.DAYS)    // 7天未被請求的接口小作,緩存回收
                .build()
    }   

如上代碼所示,二級緩存中的一個(gè) Object 是被浪費(fèi)的內(nèi)存空間稼钩,但是由于想不到其他更好的方案顾稀,所以暫時(shí)保留該設(shè)計(jì)。

1.3.3 遇到的坑

泛化調(diào)用時(shí)參數(shù)對象轉(zhuǎn)換

使用 ReferenceConfig 進(jìn)行服務(wù)直接調(diào)用坝撑,繞過了對一個(gè)接口方法簽名的校驗(yàn)静秆,所以在進(jìn)行泛化調(diào)用時(shí),最大的問題就是 Object[] 內(nèi)的參數(shù)類型了巡李。每次當(dāng)遇到數(shù)據(jù)類型問題時(shí)抚笔,本人只會用最笨的辦法,枚舉解決侨拦。代碼如下:

    /** 參數(shù)存在基本數(shù)據(jù)類型時(shí)殊橙,默認(rèn)使用基本數(shù)據(jù)類型
     * 基本類型包含:
     * 實(shí)數(shù):double、float
     * 整數(shù):byte狱从、short膨蛮、int、long
     * 字符:char
     * 布爾值:boolean
     * */
    private Object getInstance(String paramType, String value) {
        switch (paramType) {
            case "java.lang.String":
                return value
            case "byte":
            case "java.lang.Byte":
                return Byte.parseByte(value)
            case "short":
                return Short.parseShort(value)
            case "int":
            case "java.lang.Integer":
                return Integer.parseInt(value)
            case "long":
            case "java.lang.Long":
                return Long.parseLong(value)
            case "float":
            case "java.lang.Float":
                return Float.parseFloat(value)
            case "double":
            case "java.lang.Double":
                return Double.parseDouble(value)
            case "boolean":
            case "java.lang.Boolean":
                return Boolean.parseBoolean(value)
            default:
                JSONObject jsonObject = JSON.parseObject(value) // 轉(zhuǎn)成JSONObject
                return jsonObject
        }
    }

如以上代碼所示季研,是將傳入?yún)?shù)轉(zhuǎn)成對應(yīng)的包裝類型敞葛。當(dāng)接口的簽名如果為 int,那么入?yún)ο笫?Integer 也是可以的。因?yàn)?$invoke(String methodName, String[] paramsTypes, Object[] objects)与涡,是由 paramsTypes 檢查方法簽名惹谐,然后再將 objects 傳入具體服務(wù)中進(jìn)行調(diào)用。

ReferenceConfig 初始化優(yōu)先設(shè)置 initialize 為 true

使用泛化調(diào)用發(fā)起遠(yuǎn)程 Dubbo 服務(wù)請求递沪,在發(fā)起 invoke 前豺鼻,有 GenericService genericService = referenceConfig.get() 操作综液。當(dāng) Dubbo 服務(wù)沒有起來款慨,此時(shí)首次發(fā)起調(diào)用后,進(jìn)行 ref 初始化操作谬莹。ReferenceConfig 初始化 ref 代碼如下:

    private void init() {
        if (initialized) {
            return;
        }
        initialized = true;
        if (interfaceName == null || interfaceName.length() == 0) {
            throw new IllegalStateException("<dubbo:reference interface=\"\" /> interface not allow null!");
        }
        // 獲取消費(fèi)者全局配置
        checkDefault();
        appendProperties(this);
        if (getGeneric() == null && getConsumer() != null) {
            setGeneric(getConsumer().getGeneric());
        }
        ...
    }

結(jié)果導(dǎo)致:由于第一次初始化的時(shí)候檩奠,先把 initialize 設(shè)置為 true桩了,但是后面未獲取到有效的 genericService,導(dǎo)致后面即使 Dubbo 服務(wù)起來后埠戳,也會泛化調(diào)用失敗井誉。

解決方案:泛化調(diào)用就是使用 genericService 執(zhí)行 invoke 調(diào)用,所以每次請求都使用一個(gè)新的 ReferenceConfig整胃,當(dāng)初始化進(jìn)行 get() 操作時(shí)報(bào)異晨攀ィ或返回為 null 時(shí),不保存屁使;直到初始化進(jìn)行 get() 操作時(shí)獲取到有效的 genericService 時(shí)在岂,將該 genericService 保存起來。實(shí)現(xiàn)代碼如下:

    synchronized (hasInit) {
        if (!hasInit) {
            ReferenceConfig referenceConfig = new ReferenceConfig();
            // set interface name
            referenceConfig.setInterface(serviceInfoDO.interfaceName)
            referenceConfig.setApplication(serviceInfoDO.applicationConfig)
            // set version
            if (serviceInfoDO.version != null && serviceInfoDO.version != "" && serviceInfoDO.version.length() > 0) {
                referenceConfig.setVersion(serviceInfoDO.version)
            }
            if (StringUtils.isEmpty(serviceInfoDO.refUrl) || serviceInfoDO.refUrl.length() <= 0) {
                throw new NullPointerException("The 'refUrl' should not be ${serviceInfoDO.refUrl} , please make sure you have the correct 'refUrl' passed in")
            }
            referenceConfig.setUrl(serviceInfoDO.refUrl)
            referenceConfig.setGeneric(true)// 聲明為泛化接口
            genericService = referenceConfig.get()
            if (null != genericService) {
                hasInit = true
            }
        }
    }

1.4 單個(gè)服務(wù)器蛮寂,支持部署多個(gè)相同和不同的Service

根據(jù)需求蔽午,需要解決兩個(gè)問題:1.服務(wù)器運(yùn)行過程中,外部API的Jar包加載問題酬蹋;2.注冊多個(gè)相同接口服務(wù)時(shí)及老,名稱相同的問題。

1.4.1 動態(tài)外部Jar包加載的設(shè)計(jì)方案

方案一范抓、為外部 Jar 包生成單獨(dú)的 URLClassLoader,然后在泛化注冊時(shí)使用保存的 ClassLoader骄恶,在回調(diào)時(shí)進(jìn)行切換 currentThread 的 ClassLoader,進(jìn)行相同 API 接口不同版本的 Mock匕垫。

不可用原因: JavassistProxyFactory 中 final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type);
wapper 獲取的時(shí)候叠蝇,使用的 makeWrapper 中默認(rèn)使用的是ClassHelper.getClassLoader(c);導(dǎo)致一直會使用 AppClassLoader。API 信息會保存在一個(gè) WapperMap 中年缎,當(dāng)消費(fèi)者請求過來的時(shí)候悔捶,會優(yōu)先取這個(gè) Map 找對應(yīng)的 API 信息。

導(dǎo)致結(jié)果:

  • 1.由于使用泛化注冊单芜,所以 class 不在 AppClassLoader 中蜕该。設(shè)置了 currentThread 的 ClassLoader 不生效。
  • 2.由于 dubbo 保存 API 信息只有一個(gè) Map洲鸠,所以導(dǎo)致發(fā)布的服務(wù)的 API 也只能有一套堂淡。

解決方案:

  • 使用自定義 ClassLoader 進(jìn)行加載外部 Jar 包中的 API 信息。
  • 一臺 Mock 終端存一套 API 信息扒腕,更新 API 時(shí)需要重啟服務(wù)器绢淀。
方案二、在程序啟動時(shí)瘾腰,使用自定義 TestPlatformClassLoader皆的。還是給每個(gè) Jar 包生成對應(yīng)的 ApiClassLoader,由 TestPlatformClassLoader 統(tǒng)一管理蹋盆。

不可用原因:

在 Mock 終端部署時(shí)费薄,使用 -Djava.system.class.loader 設(shè)置 ClassLoader 時(shí)硝全,JVM 啟動參數(shù)不可用。因?yàn)槔懵眨琓estPlatformClassLoader 不存在于當(dāng)前 JVM 中伟众,而是在工程代碼中。詳細(xì)參數(shù)如下:

-Djava.system.class.loader=com.youzan.test.mocker.internal.classloader.TestPlatformClassLoader

解決方案:(由架構(gòu)師汪興提供)

  • 使用自定義 Runnable()召廷,保存程序啟動需要的 ClassLoader凳厢、啟動參數(shù)、mainClass 信息竞慢。
  • 在程序啟動時(shí)数初,新起一個(gè) Thread,傳入自定義 Runnable()梗顺,然后將該線程啟動泡孩。
方案三、使用自定義容器啟動服務(wù)

應(yīng)用啟動流程寺谤,如下圖所示(下圖來自有贊架構(gòu)團(tuán)隊(duì))

image

Java 的類加載遵循雙親委派的設(shè)計(jì)模式仑鸥,從 AppClassLoader 開始自底向上尋找,并自頂向下加載变屁,所以在沒有自定義 ClassLoader 時(shí)眼俊,應(yīng)用的啟動是通過 AppClassLoader 去加載 Main 啟動類去運(yùn)行。

自定義 ClassLoader 后,系統(tǒng) ClassLoader 將被設(shè)置成容器自定義的 ClassLoader,自定義 ClassLoader 重新去加載 Main 啟動類運(yùn)行凉袱,此時(shí)后續(xù)所有的類加載都會先去自定義的 ClassLoader 里查找。

難點(diǎn):應(yīng)用默認(rèn)系統(tǒng)類加載器是 AppClassLoader澎灸,在 New 對象時(shí)不會經(jīng)過自定義的 ClassLoader。

巧妙之處:Main 函數(shù)啟動時(shí)遮晚,AppClassLoader 加載 Main 和容器性昭,容器獲取到 Main class,用自定義 ClassLoader 重新加載Main县遣,設(shè)置系統(tǒng)類加載器為自定義類加載器糜颠,此時(shí) New 對象都會經(jīng)過自定義的 ClassLoader。

1.4.2 設(shè)計(jì)方案選擇

以上三個(gè)方案萧求,其實(shí)是實(shí)踐過程中的一個(gè)迭代其兴。最終結(jié)果:

  • 方案一、保留為外部Jar包生成單獨(dú)的 URLClassLoader夸政。
  • 方案二元旬、保留自定義 TestPlatformClassLoader,使用 TestPlatformClassLoader 保存每個(gè) Jar 包中 API 與其 ClassLoader 的對應(yīng)關(guān)系。
  • 方案三法绵、采用自定義容器啟動,新起一個(gè)線程酪碘,并設(shè)置其 concurrentThreadClassLoader 為 TestPlatformClassLoader朋譬,用該線程啟動 Main.class。

1.4.3 遇到的坑

使用 Javassist 生成的 Class 名稱相同

使用 Javassist 生成的 Class兴垦,每個(gè) Class 有單獨(dú)的 ClassName 以 Service Chain + className 組成徙赢。在重新生成相同名字的 class 時(shí),即使使用 new ClassPool() 也不能完全隔離探越。因?yàn)樯?Class 的時(shí)候 Class<?> clazz = ctClass.toClass() 默認(rèn)使用的是同一個(gè) ClassLoader狡赐,所以會報(bào)“attempted duplicate class definition for name:****”。

解決方案:基于 ClassName 不是隨機(jī)生成的钦幔,所以只能基于之前的 ClassLoader 生成一個(gè)新的 SecureClassLoader(ClassLoader parent) 加載新的 class枕屉,舊的 ClassLoader 靠 Java 自動 GC。代碼如下:

Class<?> clazz = ctClass.toClass(new SecureClassLoader(clz.classLoader))

PS:該方案目前沒有做過壓測鲤氢,不知道會不會導(dǎo)致內(nèi)存溢出搀擂。

二、方案實(shí)現(xiàn)

2.1 Mock 工廠整體設(shè)計(jì)架構(gòu)

image

2.2 Mocker 容器設(shè)計(jì)圖

image

2.3 二方包管理時(shí)序圖

image

2.4 Mocker 容器服務(wù)注冊時(shí)序圖

image

三卷玉、支持場景

3.1 元素及名詞解釋

image

上圖所示為基本元素組成哨颂,相關(guān)名詞解釋如下:

  • 消費(fèi)者:調(diào)用方發(fā)起 DubboRequest
  • Base 服務(wù):不帶 Service Chain 標(biāo)識的正常服務(wù)
  • Mock 服務(wù):通過 Mock 工廠生成的 dubbo 服務(wù)
  • ETCD:注冊中心,此處同時(shí)注冊著 Base 服務(wù)和 Mock 服務(wù)
  • 默認(rèn)服務(wù)透傳:對接口中不需要 Mock 的方法相种,直接泛化調(diào)用 Base 服務(wù)
  • 自定義服務(wù)(CF):用戶自己起一個(gè)泛化 dubbo 服務(wù)(PS:不需要注冊到注冊中心威恼,也不需要 Service Chain 標(biāo)識)

3.2 支持場景簡述

場景1:不帶 Service Chain 請求(不使用 Mock 服務(wù)時(shí))

消費(fèi)者從注冊中心獲取到 Base 環(huán)境服務(wù)的 IP+PORT,直接請求 Base 環(huán)境的服務(wù)寝并。
image

場景2箫措、帶 Service Chain 請求、Mock 服務(wù)采用 JSON 返回實(shí)現(xiàn)

消費(fèi)者從注冊中心獲取到兩個(gè)地址:1.Base 環(huán)境服務(wù)的 IP+PORT衬潦;2.帶 Service Chain 標(biāo)記服務(wù)(Mock服務(wù))的 IP+PORT蒂破。根據(jù) Service Chain 調(diào)用路由,去請求 Mock 服務(wù)中的該方法别渔,并返回 Mock 數(shù)據(jù)附迷。
image

場景3、帶 Service Chain 請求哎媚、Mock 服務(wù)沒有該方法實(shí)現(xiàn)

消費(fèi)者從注冊中心獲取到兩個(gè)地址:1.Base 環(huán)境服務(wù)的 IP+PORT喇伯;2.帶 Service Chain 標(biāo)記服務(wù)(Mock 服務(wù))的 IP+PORT。根據(jù) Service Chain 調(diào)用路由拨与,去請求 Mock 服務(wù)稻据。由于 Mock 服務(wù)中該方法是默認(rèn)服務(wù)透傳,所以由 Mock 服務(wù)直接泛化調(diào)用 Base 服務(wù),并返回?cái)?shù)據(jù)捻悯。
image

場景4匆赃、帶 Service Chain 請求頭、Mock 服務(wù)采用自定義服務(wù)(CR)實(shí)現(xiàn)

消費(fèi)者從注冊中心獲取到兩個(gè)地址:1.Base 環(huán)境服務(wù)的 IP+PORT今缚;2.帶 Service Chain 標(biāo)記服務(wù)(Mock 服務(wù))的 IP+PORT算柳。根據(jù) Service Chain 調(diào)用路由,去請求Mock服務(wù)姓言。由于 Mock 服務(wù)中該方法是自定義服務(wù)(CF)瞬项,所以由 Mock 服務(wù)調(diào)用用戶的 dubbo 服務(wù),并返回?cái)?shù)據(jù)何荚。
image

場景5囱淋、帶 Service Chain 請求頭、Mock 服務(wù)沒有該方法實(shí)現(xiàn)餐塘、該方法又調(diào)用帶 Service Chain 的 InterfaceB 的方法

消費(fèi)者調(diào)用 InterfaceA 的 Method3 時(shí)妥衣,從注冊中心獲取到兩個(gè)地址:1.Base 環(huán)境服務(wù)的 IP+PORT;2.帶 Service Chain 標(biāo)記服務(wù)(Mock 服務(wù))的 IP+PORT戒傻。根據(jù) Service Chain 調(diào)用路由称鳞,去請求 InterfaceA 的 Mock 服務(wù)。由于 Mock 服務(wù)中該方法是默認(rèn)服務(wù)透傳稠鼻,所以由 Mock 服務(wù)直接泛化調(diào)用 InterfaceA 的 Base 服務(wù)的Method3冈止。

但是,由于 InterfaceA 的 Method3 是調(diào)用 InterfaceB 的 Method2候齿,從注冊中心獲取到兩個(gè)地址:1.Base 環(huán)境服務(wù)的 IP+PORT熙暴;2.帶 Service Chain 標(biāo)記服務(wù)(Mock 服務(wù))的 IP+PORT。由于 Service Chain 標(biāo)識在整個(gè)請求鏈路中是一直被保留的慌盯,所以根據(jù)Service Chain調(diào)用路由周霉,最終請求到 InterfaceB 的 Mock 服務(wù),并返回?cái)?shù)據(jù)亚皂。
image

場景6俱箱、帶 Service Chain 請求頭、Mock已經(jīng)存在的 Service Chain 服務(wù)

由于不能同時(shí)存在兩個(gè)相同的 Service Chain 服務(wù)灭必,所以需要降原先的 Service Chain 服務(wù)進(jìn)行只訂閱狞谱、不注冊的操作。然后將Mock服務(wù)的透傳地址禁漓,配置為原 Service Chain 服務(wù)(即訂閱)跟衅。 消費(fèi)者在進(jìn)行請求時(shí),只會從 ETCD 發(fā)現(xiàn) Mock 服務(wù)播歼,其他同場景2伶跷、3、4、5叭莫。
image
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蹈集,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子雇初,更是在濱河造成了極大的恐慌拢肆,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件抵皱,死亡現(xiàn)場離奇詭異善榛,居然都是意外死亡辩蛋,警方通過查閱死者的電腦和手機(jī)呻畸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來悼院,“玉大人伤为,你說我怎么就攤上這事【萃荆” “怎么了绞愚?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長颖医。 經(jīng)常有香客問我位衩,道長,這世上最難降的妖魔是什么熔萧? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任糖驴,我火速辦了婚禮,結(jié)果婚禮上佛致,老公的妹妹穿的比我還像新娘贮缕。我一直安慰自己,他們只是感情好俺榆,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布感昼。 她就那樣靜靜地躺著,像睡著了一般罐脊。 火紅的嫁衣襯著肌膚如雪定嗓。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天萍桌,我揣著相機(jī)與錄音蜕乡,去河邊找鬼。 笑死梗夸,一個(gè)胖子當(dāng)著我的面吹牛层玲,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼辛块,長吁一口氣:“原來是場噩夢啊……” “哼畔派!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起润绵,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤线椰,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后尘盼,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體憨愉,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年卿捎,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了配紫。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,690評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡午阵,死狀恐怖躺孝,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情底桂,我是刑警寧澤植袍,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站籽懦,受9級特大地震影響于个,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜暮顺,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一厅篓、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧拖云,春花似錦贷笛、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至尤筐,卻和暖如春汇荐,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背盆繁。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工掀淘, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人油昂。 一個(gè)月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓革娄,卻偏偏與公主長得像倾贰,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子拦惋,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評論 2 353

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