【SpringCloud】Zuul在何種情況下使用Hystrix

首先,引入 spring-cloud-starter-zuul 之后會間接引入:

hystrix依賴已經引入玄窝,那么何種情況下使用hystrix呢牵寺?

在Zuul的自動配置類 ZuulServerAutoConfiguration 和 ZuulProxyAutoConfiguration 中總共會向Spring容器注入3個Zuul的RouteFilter,分別是

SimpleHostRoutingFilter簡單路由恩脂,通過HttpClient向預定的URL發(fā)送請求生效條件:RequestContext.getCurrentContext().getRouteHost() != null

? && RequestContext.getCurrentContext().sendZuulResponse()

1帽氓、RequestContext中的routeHost不為空,routeHost就是URL俩块,即使用URL直連

2黎休、RequestContext中的sendZuulResponse為true浓领,即是否將response發(fā)送給客戶端,默認為true

RibbonRoutingFilter使用Ribbon势腮、Hystrix和可插入的http客戶端發(fā)送請求生效條件:(RequestContext.getRouteHost() == null && RequestContext.get(SERVICE_ID_KEY) != null

? && RequestContext.sendZuulResponse())

1联贩、RequestContext中的routeHost為空,即URL為空

2捎拯、RequestContext中的serviceId不為空

3泪幌、RequestContext中的sendZuulResponse為true,即是否將response發(fā)送給客戶端玄渗,默認為true

SendForwardFilterforward到本地URL生效條件:RequestContext.containsKey(FORWARD_TO_KEY)

? && !RequestContext.getBoolean(SEND_FORWARD_FILTER_RAN, false)

1座菠、RequestContext中包含FORWARD_TO_KEY,即URL使用 forward: 映射

2藤树、RequestContext中SEND_FORWARD_FILTER_RAN為false浴滴,SEND_FORWARD_FILTER_RAN意為“send forward是否運行過了”,在SendForwardFilter#run()時會 ctx.set(SEND_FORWARD_FILTER_RAN, true)

綜上所述岁钓,在使用serviceId映射的方法路由轉發(fā)的時候升略,會使用Ribbon+Hystrix

而哪種路由配置方式是“URL映射”,哪種配置方式又是“serviceId映射”呢屡限?

Zuul有一個前置過濾器 PreDecorationFilter 用于通過 RouteLocator路由定位器 決定在何時以何種方式路由轉發(fā)

RouteLocator是用于通過請求地址匹配到Route路由的品嚣,之后 PreDecorationFilter 再通過Route信息設置RequestContext上下文,決定后續(xù)使用哪個RouteFilter做路由轉發(fā)

所以就引出以下問題:

什么是Route

RouteLocator路由定位器如何根據請求路徑匹配路由

匹配到路由后钧大,PreDecorationFilter如何設置RequestContext請求上下文

什么是Route

我總共見到兩個和Route相關的類

ZuulProperties.ZuulRoute 翰撑,用于和zuul配置文件關聯,保存相關信息

org.springframework.cloud.netflix.zuul.filters.Route 啊央, RouteLocator找到的路由信息就是這個類眶诈,用于路由轉發(fā)

public static class ZuulRoute {

? ? private String id;? ? //ZuulRoute的id

? ? private String path;? //路由的pattern,如 /foo/**

? ? private String serviceId;? //要映射到此路由的服務id

? ? private String url;? //要映射到路由的完整物理URL

? ? private boolean stripPrefix = true;? //用于確定在轉發(fā)之前是否應剝離此路由前綴的標志位

? ? private Boolean retryable;? //此路由是否可以重試瓜饥,通常重試需要serviceId和ribbon

? ? private Set<String> sensitiveHeaders = new LinkedHashSet(); //不會傳遞給下游請求的敏感標頭列表

? ? private boolean customSensitiveHeaders = false; //是否自定義了敏感頭列表

}

public class Route {

? ? private String id;

? ? private String fullPath;

? ? private String path;

? ? private String location;? //可能是 url 或 serviceId

? ? private String prefix;

? ? private Boolean retryable;

? ? private Set<String> sensitiveHeaders = new LinkedHashSet<>();

? ? private boolean customSensitiveHeaders;

}

可以看到 org.springframework.cloud.netflix.zuul.filters.Route 和 ZuulProperties.ZuulRoute 基本一致逝撬,只是Route用于路由轉發(fā)定位的屬性location根據不同的情況,可能是一個具體的URL乓土,可能是一個serviceId

RouteLocator路由定位器如何根據請求路徑匹配路由

Zuul在自動配置加載時注入了2個RouteLocator

CompositeRouteLocator : 組合的RouteLocator宪潮,在 getMatchingRoute() 時會依次調用其它的RouteLocator,先找到先返回趣苏;CompositeRouteLocator的routeLocators集合中只有DiscoveryClientRouteLocator

DiscoveryClientRouteLocator : 可以將靜態(tài)的狡相、已配置的路由與來自DiscoveryClient服務發(fā)現的路由組合在一起,來自DiscoveryClient的路由優(yōu)先拦键;SimpleRouteLocator的子類(SimpleRouteLocator 基于加載到 ZuulProperties 中的配置定位Route路由信息)

其中CompositeRouteLocator是 @Primary 的谣光,它是組合多個RouteLocator的Locator,其 getMatchingRoute() 方法會分別調用其它所有RouteLocator的getMatchingRoute()方法芬为,通過請求路徑匹配路由信息萄金,只要匹配到了就馬上返回

默認CompositeRouteLocator混合路由定位器的routeLocators只有一個DiscoveryClientRouteLocator蟀悦,故只需分析 DiscoveryClientRouteLocator#getMatchingRoute(path)

//----------DiscoveryClientRouteLocator是SimpleRouteLocator子類,其實是調用的SimpleRouteLocator##getMatchingRoute(path)

@Override

public Route getMatchingRoute(final String path) {

? ? return getSimpleMatchingRoute(path);

}

protected Route getSimpleMatchingRoute(final String path) {

? ? if (log.isDebugEnabled()) {

? ? ? ? log.debug("Finding route for path: " + path);

? ? }

? ? // routes是保存路由信息的map氧敢,如果此時還未加載日戈,調用locateRoutes()

? ? if (this.routes.get() == null) {

? ? ? ? this.routes.set(locateRoutes());

? ? }

? ? if (log.isDebugEnabled()) {

? ? ? ? log.debug("servletPath=" + this.dispatcherServletPath);

? ? ? ? log.debug("zuulServletPath=" + this.zuulServletPath);

? ? ? ? log.debug("RequestUtils.isDispatcherServletRequest()="

? ? ? ? ? ? ? ? + RequestUtils.isDispatcherServletRequest());

? ? ? ? log.debug("RequestUtils.isZuulServletRequest()="

? ? ? ? ? ? ? ? + RequestUtils.isZuulServletRequest());

? ? }

? ? /**

? ? * 下面的方法主要是先對path做微調

? ? * 再根據path到routes中匹配到ZuulRoute

? ? * 最后根據 ZuulRoute 和 adjustedPath 生成 Route

? ? */

? ? String adjustedPath = adjustPath(path);

? ? ZuulRoute route = getZuulRoute(adjustedPath);

? ? return getRoute(route, adjustedPath);

}

下面我們來看看 locateRoutes() 是如何加載靜態(tài)的、已配置的路由與來自DiscoveryClient服務發(fā)現的路由的

//----------DiscoveryClientRouteLocator#locateRoutes()? 服務發(fā)現路由定位器的locateRoutes()

@Override

protected LinkedHashMap<String, ZuulRoute> locateRoutes() {

? ? //保存ZuulRoute的LinkedHashMap

? ? LinkedHashMap<String, ZuulRoute> routesMap = new LinkedHashMap<String, ZuulRoute>();


? ? //調用父類SimpleRouteLocator#locateRoutes()

? ? //加載ZuulProperties中的所有配置文件中的路由信息

? ? routesMap.putAll(super.locateRoutes());


? ? //如果服務發(fā)現客戶端discovery存在

? ? if (this.discovery != null) {

? ? ? ? //將routesMap已經存在的配置文件中的ZuulRoute放入staticServices<serviceId, ZuulRoute>

? ? ? ? Map<String, ZuulRoute> staticServices = new LinkedHashMap<String, ZuulRoute>();

? ? ? ? for (ZuulRoute route : routesMap.values()) {

? ? ? ? ? ? String serviceId = route.getServiceId();


? ? ? ? ? ? //如果serviceId為null孙乖,以id作為serviceId浙炼,此情況適合 zuul.routes.xxxx=/xxxx/** 的情況

? ? ? ? ? ? if (serviceId == null) {

? ? ? ? ? ? ? ? serviceId = route.getId();

? ? ? ? ? ? }

? ? ? ? ? ? if (serviceId != null) {

? ? ? ? ? ? ? ? staticServices.put(serviceId, route);

? ? ? ? ? ? }

? ? ? ? }



? ? ? ? // Add routes for discovery services by default

? ? ? ? List<String> services = this.discovery.getServices(); //到注冊中心找到所有service

? ? ? ? String[] ignored = this.properties.getIgnoredServices()

? ? ? ? ? ? ? ? .toArray(new String[0]);


? ? ? ? //遍歷services

? ? ? ? for (String serviceId : services) {

? ? ? ? ? ? // Ignore specifically ignored services and those that were manually

? ? ? ? ? ? // configured

? ? ? ? ? ? String key = "/" + mapRouteToService(serviceId) + "/**";


? ? ? ? ? ? //如果注冊中心的serviceId在staticServices集合中,并且此路由沒有配置URL

? ? ? ? ? ? //那么唯袄,更新路由的location為serviceId

? ? ? ? ? ? if (staticServices.containsKey(serviceId)

? ? ? ? ? ? ? ? ? ? && staticServices.get(serviceId).getUrl() == null) {

? ? ? ? ? ? ? ? // Explicitly configured with no URL, cannot be ignored

? ? ? ? ? ? ? ? // all static routes are already in routesMap

? ? ? ? ? ? ? ? // Update location using serviceId if location is null

? ? ? ? ? ? ? ? ZuulRoute staticRoute = staticServices.get(serviceId);

? ? ? ? ? ? ? ? if (!StringUtils.hasText(staticRoute.getLocation())) {

? ? ? ? ? ? ? ? ? ? staticRoute.setLocation(serviceId);

? ? ? ? ? ? ? ? }

? ? ? ? ? ? }


? ? ? ? ? ? //如果注冊中心的serviceId不在忽略范圍內弯屈,且routesMap中還沒有包含,添加到routesMap

? ? ? ? ? ? if (!PatternMatchUtils.simpleMatch(ignored, serviceId)

? ? ? ? ? ? ? ? ? ? && !routesMap.containsKey(key)) {

? ? ? ? ? ? ? ? // Not ignored

? ? ? ? ? ? ? ? routesMap.put(key, new ZuulRoute(key, serviceId));

? ? ? ? ? ? }

? ? ? ? }

? ? }


? ? // 如果routesMap中有 /** 的默認路由配置

? ? if (routesMap.get(DEFAULT_ROUTE) != null) {

? ? ? ? ZuulRoute defaultRoute = routesMap.get(DEFAULT_ROUTE);

? ? ? ? // Move the defaultServiceId to the end

? ? ? ? routesMap.remove(DEFAULT_ROUTE);

? ? ? ? routesMap.put(DEFAULT_ROUTE, defaultRoute);

? ? }


? ? //將routesMap中的數據微調后恋拷,放到values<String, ZuulRoute>资厉,返回

? ? LinkedHashMap<String, ZuulRoute> values = new LinkedHashMap<>();

? ? for (Entry<String, ZuulRoute> entry : routesMap.entrySet()) {

? ? ? ? String path = entry.getKey();

? ? ? ? // Prepend with slash if not already present.

? ? ? ? if (!path.startsWith("/")) {

? ? ? ? ? ? path = "/" + path;

? ? ? ? }

? ? ? ? if (StringUtils.hasText(this.properties.getPrefix())) {

? ? ? ? ? ? path = this.properties.getPrefix() + path;

? ? ? ? ? ? if (!path.startsWith("/")) {

? ? ? ? ? ? ? ? path = "/" + path;

? ? ? ? ? ? }

? ? ? ? }

? ? ? ? values.put(path, entry.getValue());

? ? }


? ? return values;

}

此方法運行后就已經加載了配置文件中所有路由信息,以及注冊中心中的服務路由信息蔬顾,有的通過URL路由宴偿,有的通過serviceId路由

只需根據本次請求的requestURI與 路由的pattern匹配找到對應的路由

匹配到路由后,PreDecorationFilter如何設置RequestContext請求上下文

//----------PreDecorationFilter前置過濾器

@Override

public Object run() {

? ? RequestContext ctx = RequestContext.getCurrentContext();

? ? final String requestURI = this.urlPathHelper.getPathWithinApplication(ctx.getRequest());

? ? Route route = this.routeLocator.getMatchingRoute(requestURI); //找到匹配的路由

? ? //----------------到上面為止是已經分析過的诀豁,根據requestURI找到匹配的Route信息


? ? // ==== 匹配到路由信息

? ? if (route != null) {

? ? ? ? String location = route.getLocation();

? ? ? ? if (location != null) {

? ? ? ? ? ? ctx.put(REQUEST_URI_KEY, route.getPath());//RequestContext設置 requestURI:路由的pattern路徑

? ? ? ? ? ? ctx.put(PROXY_KEY, route.getId());//RequestContext設置 proxy:路由id


? ? ? ? ? ? //設置需要忽略的敏感頭信息窄刘,要么用全局默認的,要么用路由自定義的

? ? ? ? ? ? if (!route.isCustomSensitiveHeaders()) {

? ? ? ? ? ? ? ? this.proxyRequestHelper

? ? ? ? ? ? ? ? ? ? ? ? .addIgnoredHeaders(this.properties.getSensitiveHeaders().toArray(new String[0]));

? ? ? ? ? ? }

? ? ? ? ? ? else {

? ? ? ? ? ? ? ? this.proxyRequestHelper.addIgnoredHeaders(route.getSensitiveHeaders().toArray(new String[0]));

? ? ? ? ? ? }

? ? ? ? ? ? //設置重試信息

? ? ? ? ? ? if (route.getRetryable() != null) {

? ? ? ? ? ? ? ? ctx.put(RETRYABLE_KEY, route.getRetryable());

? ? ? ? ? ? }

? ? ? ? ? ? //如果location是 http/https開頭的舷胜,RequestContext設置 routeHost:URL

? ? ? ? ? ? //如果location是 forward:開頭的娩践,RequestContext設置 forward信息、routeHost:null

? ? ? ? ? ? //其它 RequestContext設置 serviceId烹骨、routeHost:null欺矫、X-Zuul-ServiceId

? ? ? ? ? ? if (location.startsWith(HTTP_SCHEME+":") || location.startsWith(HTTPS_SCHEME+":")) {

? ? ? ? ? ? ? ? ctx.setRouteHost(getUrl(location));

? ? ? ? ? ? ? ? ctx.addOriginResponseHeader(SERVICE_HEADER, location);

? ? ? ? ? ? }

? ? ? ? ? ? else if (location.startsWith(FORWARD_LOCATION_PREFIX)) {

? ? ? ? ? ? ? ? ctx.set(FORWARD_TO_KEY,

? ? ? ? ? ? ? ? ? ? ? ? StringUtils.cleanPath(location.substring(FORWARD_LOCATION_PREFIX.length()) + route.getPath()));

? ? ? ? ? ? ? ? ctx.setRouteHost(null);

? ? ? ? ? ? ? ? return null;

? ? ? ? ? ? }

? ? ? ? ? ? else {

? ? ? ? ? ? ? ? // set serviceId for use in filters.route.RibbonRequest

? ? ? ? ? ? ? ? ctx.set(SERVICE_ID_KEY, location);

? ? ? ? ? ? ? ? ctx.setRouteHost(null);

? ? ? ? ? ? ? ? ctx.addOriginResponseHeader(SERVICE_ID_HEADER, location);

? ? ? ? ? ? }


? ? ? ? ? ? //是否添加代理頭信息 X-Forwarded-For

? ? ? ? ? ? if (this.properties.isAddProxyHeaders()) {

? ? ? ? ? ? ? ? addProxyHeaders(ctx, route);

? ? ? ? ? ? ? ? String xforwardedfor = ctx.getRequest().getHeader(X_FORWARDED_FOR_HEADER);

? ? ? ? ? ? ? ? String remoteAddr = ctx.getRequest().getRemoteAddr();

? ? ? ? ? ? ? ? if (xforwardedfor == null) {

? ? ? ? ? ? ? ? ? ? xforwardedfor = remoteAddr;

? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? else if (!xforwardedfor.contains(remoteAddr)) { // Prevent duplicates

? ? ? ? ? ? ? ? ? ? xforwardedfor += ", " + remoteAddr;

? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? ctx.addZuulRequestHeader(X_FORWARDED_FOR_HEADER, xforwardedfor);

? ? ? ? ? ? }


? ? ? ? ? ? //是否添加Host頭信息

? ? ? ? ? ? if (this.properties.isAddHostHeader()) {

? ? ? ? ? ? ? ? ctx.addZuulRequestHeader(HttpHeaders.HOST, toHostHeader(ctx.getRequest()));

? ? ? ? ? ? }

? ? ? ? }

? ? }

? ? // ==== 沒有匹配到路由信息

? ? else {

? ? ? ? log.warn("No route found for uri: " + requestURI);

? ? ? ? String fallBackUri = requestURI;

? ? ? ? String fallbackPrefix = this.dispatcherServletPath; // default fallback

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // servlet is

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // DispatcherServlet

? ? ? ? if (RequestUtils.isZuulServletRequest()) {

? ? ? ? ? ? // remove the Zuul servletPath from the requestUri

? ? ? ? ? ? log.debug("zuulServletPath=" + this.properties.getServletPath());

? ? ? ? ? ? fallBackUri = fallBackUri.replaceFirst(this.properties.getServletPath(), "");

? ? ? ? ? ? log.debug("Replaced Zuul servlet path:" + fallBackUri);

? ? ? ? }

? ? ? ? else {

? ? ? ? ? ? // remove the DispatcherServlet servletPath from the requestUri

? ? ? ? ? ? log.debug("dispatcherServletPath=" + this.dispatcherServletPath);

? ? ? ? ? ? fallBackUri = fallBackUri.replaceFirst(this.dispatcherServletPath, "");

? ? ? ? ? ? log.debug("Replaced DispatcherServlet servlet path:" + fallBackUri);

? ? ? ? }

? ? ? ? if (!fallBackUri.startsWith("/")) {

? ? ? ? ? ? fallBackUri = "/" + fallBackUri;

? ? ? ? }

? ? ? ? String forwardURI = fallbackPrefix + fallBackUri;

? ? ? ? forwardURI = forwardURI.replaceAll("http://", "/");

? ? ? ? ctx.set(FORWARD_TO_KEY, forwardURI);

? ? }

? ? return null;

}

總結:

只要引入了spring-cloud-starter-zuul就會間接引入Ribbon、Hystrix

路由信息可能是從配置文件中加載的展氓,也可能是通過DiscoveryClient從注冊中心加載的

zuul是通過前置過濾器PreDecorationFilter找到與當前requestURI匹配的路由信息,并在RequestContext中設置相關屬性的脸爱,后續(xù)的Route Filter會根據RequestContext中的這些屬性判斷如何路由轉發(fā)

Route Filter主要使用 SimpleHostRoutingFilter 和 RibbonRoutingFilter

當RequestContext請求上下文中存在routeHost遇汞,即URL直連信息時,使用SimpleHostRoutingFilter簡單Host路由

當RequestContext請求上下文中存在serviceId簿废,即服務id時(可能會與注冊中心關聯獲取服務列表空入,或者讀取配置文件中serviceId.ribbon.listOfServers的服務列表),使用RibbonRoutingFilter族檬,會使用Ribbon歪赢、Hystrix

歡迎工作一到五年的Java工程師朋友們加入Java架構開發(fā): 855835163

群內提供免費的Java架構學習資料(里面有高可用、高并發(fā)单料、高性能及分布式埋凯、Jvm性能調優(yōu)点楼、Spring源碼,MyBatis白对,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)合理利用自己每一分每一秒的時間來學習提升自己掠廓,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕甩恼,使勁拼蟀瞧,給未來的自己一個交代!

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末条摸,一起剝皮案震驚了整個濱河市悦污,隨后出現的幾起案子,更是在濱河造成了極大的恐慌钉蒲,老刑警劉巖切端,帶你破解...
    沈念sama閱讀 211,265評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異子巾,居然都是意外死亡帆赢,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 90,078評論 2 385
  • 文/潘曉璐 我一進店門线梗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來椰于,“玉大人,你說我怎么就攤上這事仪搔●觯” “怎么了?”我有些...
    開封第一講書人閱讀 156,852評論 0 347
  • 文/不壞的土叔 我叫張陵烤咧,是天一觀的道長偏陪。 經常有香客問我,道長煮嫌,這世上最難降的妖魔是什么贞岭? 我笑而不...
    開封第一講書人閱讀 56,408評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮驹针,結果婚禮上言疗,老公的妹妹穿的比我還像新娘。我一直安慰自己懦冰,他們只是感情好灶轰,可當我...
    茶點故事閱讀 65,445評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著刷钢,像睡著了一般笋颤。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上内地,一...
    開封第一講書人閱讀 49,772評論 1 290
  • 那天伴澄,我揣著相機與錄音赋除,去河邊找鬼。 笑死秉版,一個胖子當著我的面吹牛贤重,可吹牛的內容都是我干的。 我是一名探鬼主播清焕,決...
    沈念sama閱讀 38,921評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼并蝗,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了秸妥?” 一聲冷哼從身側響起滚停,我...
    開封第一講書人閱讀 37,688評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎粥惧,沒想到半個月后键畴,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 44,130評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡突雪,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,467評論 2 325
  • 正文 我和宋清朗相戀三年起惕,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片咏删。...
    茶點故事閱讀 38,617評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡惹想,死狀恐怖,靈堂內的尸體忽然破棺而出督函,到底是詐尸還是另有隱情嘀粱,我是刑警寧澤,帶...
    沈念sama閱讀 34,276評論 4 329
  • 正文 年R本政府宣布辰狡,位于F島的核電站锋叨,受9級特大地震影響,放射性物質發(fā)生泄漏宛篇。R本人自食惡果不足惜娃磺,卻給世界環(huán)境...
    茶點故事閱讀 39,882評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望叫倍。 院中可真熱鬧豌鸡,春花似錦、人聲如沸段标。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽逼庞。三九已至,卻和暖如春瞻赶,著一層夾襖步出監(jiān)牢的瞬間赛糟,已是汗流浹背派任。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留璧南,地道東北人掌逛。 一個月前我還...
    沈念sama閱讀 46,315評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像司倚,于是被迫代替她去往敵國和親豆混。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,486評論 2 348

推薦閱讀更多精彩內容