SpringMVC 篇(一)DispatcherServlet 初始化

引言

SpringMVC 的出現(xiàn)大大方便了 Java Web 領(lǐng)域的開發(fā)扮碧,開發(fā)一個 Web 接口幾個注解就完事了螺句。而 SpringBoot 的出現(xiàn)又進一步提升了我們的開發(fā)效率涎劈。在這一層層背后缤削,你可曾想過它們到底是如何實現(xiàn)的瘦棋?我相信肯定是有的,但是面對它龐大的代碼體系鲜棠,讓人望而卻步肌厨。不過不要緊,我來帶大家進入 SpringBoot 的源碼世界豁陆,見證一下這個 “藝術(shù)品” 是如何跑起來的柑爸。

DispatcherServlet

通過名字我們可以看出它就是一個 Servlet,而恰恰這個 Servlet 就是 SpringMVC 的核心盒音。我們從它開始入手表鳍,抽絲剝繭,看看它到底是怎么對請求進行加工處理的祥诽。

  • 繼承鏈

image.png
  • 啟動

DispatcherServlet

/**
 * This implementation calls {@link #initStrategies}.
 */ 
 @Override
 protected void onRefresh(ApplicationContext context) {
        initStrategies(context);
 }

這個方法是我們首先要關(guān)注的方法譬圣,因為它完成了 DispatcherServlet 相關(guān)資源的初始化,看它調(diào)用的方法 initStrategies(context); 也能看出端倪雄坪。我們先不著急往下看胁镐,我們想一想誰調(diào)用的 onRefresh() 方法呢蹭睡?我們通過它的注解發(fā)現(xiàn)它是一個繼承下來的方法电谣,好,我們往它的上游看去件。

FrameworkServlet

/**
 * Callback that receives refresh events from this servlet's WebApplicationContext.
 * <p>The default implementation calls {@link #onRefresh},
 * triggering a refresh of this servlet's context-dependent state.
 * @param event the incoming ApplicationContext event
 */
 public void onApplicationEvent(ContextRefreshedEvent event) {
    this.refreshEventReceived = true;
    onRefresh(event.getApplicationContext());
 }
/**
 * Template method which can be overridden to add servlet-specific refresh work.
 * Called after successful context refresh.
 * <p>This implementation is empty.
 * @param context the current WebApplicationContext
 * @see #refresh()
 */
 protected void onRefresh(ApplicationContext context) {
    // For subclasses: do nothing by default.
 }

我們發(fā)現(xiàn) onRefresh() 方法是個空方法笨农,而它的調(diào)用者就在它的上邊就缆,即:onApplicationEvent(ContextRefreshedEvent event),通過名字我們可以看出它是一個事件方法,當(dāng)上下文刷新的時候谒亦,該方法會被觸發(fā)竭宰。但是 FrameworkServlet并沒有實現(xiàn)任何相關(guān)的監(jiān)聽器,是如何完成事件監(jiān)聽的呢份招?不要緊切揭,我們大概看看這個類的結(jié)構(gòu),看看能發(fā)現(xiàn)什么端倪不锁摔。我們發(fā)現(xiàn)它有一個內(nèi)部類廓旬,而這個內(nèi)部類實現(xiàn)了監(jiān)聽器,而實現(xiàn)的監(jiān)聽方法恰恰調(diào)用了onApplicationEvent(ContextRefreshedEvent event)谐腰。

/**
 * ApplicationListener endpoint that receives events from this servlet's WebApplicationContext
 * only, delegating to {@code onApplicationEvent} on the FrameworkServlet instance.
 */
private class ContextRefreshListener implements ApplicationListener<ContextRefreshedEvent> {

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        FrameworkServlet.this.onApplicationEvent(event);
    }
}

看到這里的時候孕豹,信心滿滿的,但是當(dāng)你啟動準(zhǔn)備驗證的時候十气,發(fā)現(xiàn)励背,不對。好像并不是通過事件觸發(fā) onRefresh() 的砸西。為什么叶眉?如果沒有事件產(chǎn)生址儒,那么該方法肯定就無法觸發(fā)了。所以它肯定還有別的調(diào)用入口衅疙,我們繼續(xù)往下看莲趣。

FrameworkServlet

    /**
     * Initialize and publish the WebApplicationContext for this servlet.
     * <p>Delegates to {@link #createWebApplicationContext} for actual creation
     * of the context. Can be overridden in subclasses.
     * @return the WebApplicationContext instance
     * @see #FrameworkServlet(WebApplicationContext)
     * @see #setContextClass
     * @see #setContextConfigLocation
     */
    protected WebApplicationContext initWebApplicationContext() {
        WebApplicationContext rootContext =
                WebApplicationContextUtils.getWebApplicationContext(getServletContext());
        WebApplicationContext wac = null;

        if (this.webApplicationContext != null) {
            // A context instance was injected at construction time -> use it
            wac = this.webApplicationContext;
            if (wac instanceof ConfigurableWebApplicationContext) {
                ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
                if (!cwac.isActive()) {
                    // The context has not yet been refreshed -> provide services such as
                    // setting the parent context, setting the application context id, etc
                    if (cwac.getParent() == null) {
                        // The context instance was injected without an explicit parent -> set
                        // the root application context (if any; may be null) as the parent
                        cwac.setParent(rootContext);
                    }
                    configureAndRefreshWebApplicationContext(cwac);
                }
            }
        }
        if (wac == null) {
            // No context instance was injected at construction time -> see if one
            // has been registered in the servlet context. If one exists, it is assumed
            // that the parent context (if any) has already been set and that the
            // user has performed any initialization such as setting the context id
            wac = findWebApplicationContext();
        }
        if (wac == null) {
            // No context instance is defined for this servlet -> create a local one
            wac = createWebApplicationContext(rootContext);
        }

        if (!this.refreshEventReceived) {
            // Either the context is not a ConfigurableApplicationContext with refresh
            // support or the context injected at construction time had already been
            // refreshed -> trigger initial onRefresh manually here.
            onRefresh(wac);
        }

        if (this.publishContext) {
            // Publish the context as a servlet context attribute.
            String attrName = getServletContextAttributeName();
            getServletContext().setAttribute(attrName, wac);
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Published WebApplicationContext of servlet '" + getServletName() +
                        "' as ServletContext attribute with name [" + attrName + "]");
            }
        }

        return wac;
    }

我們把焦點放到這行代碼上:

 if (!this.refreshEventReceived) {
    // Either the context is not a ConfigurableApplicationContext with refresh
    // support or the context injected at construction time had already been
    // refreshed -> trigger initial onRefresh manually here.
    onRefresh(wac);
 }

也就是說,如果事件沒有成功發(fā)布或者發(fā)布較早炼蛤,那么需要手動調(diào)用 onRefresh(wac) 方法(也間接說明了妖爷,事件并不靠譜)蝶涩。大體的調(diào)用關(guān)系鏈可以按照如下的方式分析:

servlet.init() -> GenericServlet.init() -> HttpServletBean.init() -> HttpServletBean.initServletBean() -> FrameworkSerlvet.initServletBean() -> FrameworkSerlvet.initWebApplicationContext() -> FrameworkSerlvet.onRefresh(wac)

總結(jié)

onRefresh() 方法的調(diào)用有兩個入口理朋,一個是通過上下文事件觸發(fā),一個是手動觸發(fā)(當(dāng)事件觸發(fā)失敗的時候)绿聘。而手動觸發(fā)的頂端則是 servlet 的 init() 方法嗽上。

組件初始化

當(dāng) onRefresh() 方法被觸發(fā)的時候,組件的初始化工作就展開了熄攘。在開始之前兽愤,我們先重點關(guān)注一個配置文件,那就是跟 DispatcherServlet 同一級的 DispatcherServlet.properties 文件。該文件存放的就是組件的默認(rèn)實現(xiàn)挪圾。

DispatcherServlet.properties

# Default implementation classes for DispatcherServlet's strategy interfaces.
# Used as fallback when no matching beans are found in the DispatcherServlet context.
# Not meant to be customized by application developers.

org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver

org.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolver

org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
    org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping,\
    org.springframework.web.servlet.function.support.RouterFunctionMapping

org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
    org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
    org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter,\
    org.springframework.web.servlet.function.support.HandlerFunctionAdapter


org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver,\
    org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\
    org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver

org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator

org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver

org.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.SessionFlashMapManager

DispatcherServlet

/**
 * Initialize the strategy objects that this servlet uses.
 * <p>May be overridden in subclasses in order to initialize further strategy objects.
 */
 protected void initStrategies(ApplicationContext context) {
        // 文件上傳解析器
        initMultipartResolver(context);
        // 本地化解析器
        initLocaleResolver(context);
        // 主題解析器
        initThemeResolver(context);
        // 處理器映射器(url 和 Controller 方法的映射)
        initHandlerMappings(context);
        // 處理器適配器(實際執(zhí)行 Controller 方法)
        initHandlerAdapters(context);
        // 處理器異常解析器
        initHandlerExceptionResolvers(context);
        // RequestToViewName 解析器
        initRequestToViewNameTranslator(context);
        // 視圖解析器(視圖的匹配和渲染)
        initViewResolvers(context);
        // FlashMap 管理器
        initFlashMapManager(context);
 }

由于各個組件的加載完全一樣浅萧,所以這里我只選擇 initHandlerMappings(context) 進行詳細(xì)的介紹,該方法加載 HandlerMapping.class 的實例哲思,也就是處理 urlcontroller 的映射洼畅。

    /**
     * 實例化該類所使用的 HandlerMappings 映射器,如果該命名空間下(父子容器中的子容器)的 bean 容器中沒有該 bean 的定義棚赔,那么默認(rèn)會分配一個 BeanNameUrlHandlerMapping 映射器帝簇。
     */
    private void initHandlerMappings(ApplicationContext context) {
        this.handlerMappings = null;

        if (this.detectAllHandlerMappings) {
            // 從當(dāng)前容器及它的父容器中獲取對應(yīng)的 bean 對象。
            Map<String, HandlerMapping> matchingBeans =
                    BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
            if (!matchingBeans.isEmpty()) {
                this.handlerMappings = new ArrayList<>(matchingBeans.values());
                // 根據(jù) @Order 進行排序靠益。
                AnnotationAwareOrderComparator.sort(this.handlerMappings);
            }
        }
        else {
            try {
                // 根據(jù)名字及類型取出對應(yīng)的 bean 對象丧肴。
                HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
                this.handlerMappings = Collections.singletonList(hm);
            }
            catch (NoSuchBeanDefinitionException ex) {
                // Ignore, we'll add a default HandlerMapping later.
            }
        }

        // 如果沒有映射器被發(fā)現(xiàn),那么會從配置文件中獲取出一個默認(rèn)的映射器胧后。
        if (this.handlerMappings == null) {
            this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
            if (logger.isTraceEnabled()) {
                logger.trace("No HandlerMappings declared for servlet '" + getServletName() +
                        "': using default strategies from DispatcherServlet.properties");
            }
        }
    }

到這里 HandlerMapping 類型的對象已經(jīng)加載完畢了芋浮,為了進行進一步說明,我們以 RequestMappingHandlerMapping 的加載為例進行詳細(xì)的說明壳快。為什么講它呢途样?它就是處理 urlcontroller 映射的處理器。廢話不多說濒憋,我們開始吧(細(xì)心的話何暇,你會發(fā)現(xiàn)在進行檢索的時候,有一個父容器的存在凛驮,這就是 Web 中的父子容器裆站,至于什么時候加載的父容器,大家可以回想一下先前的文章)!
RequestMappingHandlerMapping 通過它的父類間接實現(xiàn)了 InitializingBean 接口宏胯,該接口在之前介紹容器的時候已經(jīng)知道它的具體作用羽嫡,所以這里我們不多說,直接進入接口方法看看:

RequestMappingHandlerMapping

    @Override
    @SuppressWarnings("deprecation")
    public void afterPropertiesSet() {
        this.config = new RequestMappingInfo.BuilderConfiguration();
        this.config.setUrlPathHelper(getUrlPathHelper());
        this.config.setPathMatcher(getPathMatcher());
        this.config.setSuffixPatternMatch(useSuffixPatternMatch());
        this.config.setTrailingSlashMatch(useTrailingSlashMatch());
        this.config.setRegisteredSuffixPatternMatch(useRegisteredSuffixPatternMatch());
        this.config.setContentNegotiationManager(getContentNegotiationManager());

        super.afterPropertiesSet();
    }

這里我們直接進入它的父方法看看:

AbstractHandlerMethodMapping<T>

    // Handler method detection

    /**
     * 初始化時檢測 handler
     * @see #initHandlerMethods
     */
    @Override
    public void afterPropertiesSet() {
        initHandlerMethods();
    }

    /**
     * 從容器中檢索出已經(jīng)注冊好的 handler bean肩袍。
     * @see #getCandidateBeanNames()
     * @see #processCandidateBean
     * @see #handlerMethodsInitialized
     */
    protected void initHandlerMethods() {
        for (String beanName : getCandidateBeanNames()) {
            if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
                processCandidateBean(beanName);
            }
        }
        handlerMethodsInitialized(getHandlerMethods());
    }
    /**
     *  檢測 object 類型的 bean杭棵,并將 bean 的名稱放到數(shù)組里。
     * @since 5.1
     * @see #setDetectHandlerMethodsInAncestorContexts
     * @see BeanFactoryUtils#beanNamesForTypeIncludingAncestors
     */
    protected String[] getCandidateBeanNames() {
        return (this.detectHandlerMethodsInAncestorContexts ?
                BeanFactoryUtils.beanNamesForTypeIncludingAncestors(obtainApplicationContext(), Object.class) :
                obtainApplicationContext().getBeanNamesForType(Object.class));
    }

    /**
     * 這里只處理 @Controller 或 @RequestMapping 修飾的 bean
     */
     protected void processCandidateBean(String beanName) {
        Class<?> beanType = null;
        try {
            beanType = obtainApplicationContext().getType(beanName);
        }
        catch (Throwable ex) {
            // An unresolvable bean type, probably from a lazy bean - let's ignore it.
            if (logger.isTraceEnabled()) {
                logger.trace("Could not resolve type for bean '" + beanName + "'", ex);
            }
        }
        // 這里就是用來篩選 @Controller 或 @RequestMapping 修飾的類
        if (beanType != null && isHandler(beanType)) {
            detectHandlerMethods(beanName);
        }
    }

    @Override
    protected boolean isHandler(Class<?> beanType) {
        return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
                AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
    }

通過以上代碼氛赐,我們可以看出魂爪,把 @Controller 或者 @RequestMapping 修飾的 bean 篩選出來,篩選出來之后做什么呢艰管?我們繼續(xù)往下看:

AbstractHandlerMethodMapping<T>

    // 這里的 handler 就是每個 controller 類的名字(并非全限定名)
    protected void detectHandlerMethods(Object handler) {
        Class<?> handlerType = (handler instanceof String ?
                obtainApplicationContext().getType((String) handler) : handler.getClass());

        if (handlerType != null) {
            Class<?> userType = ClassUtils.getUserClass(handlerType);
            // 這里 Method 就是具體的方法全限定名滓侍,而 T 就是該方法映射信息(HTTP 方法 + URL)
            Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
                    (MethodIntrospector.MetadataLookup<T>) method -> {
                        try {
                            return getMappingForMethod(method, userType);
                        }
                        catch (Throwable ex) {
                            throw new IllegalStateException("Invalid mapping on handler class [" +
                                    userType.getName() + "]: " + method, ex);
                        }
                    });
            if (logger.isTraceEnabled()) {
                logger.trace(formatMappings(userType, methods));
            }
            methods.forEach((method, mapping) -> {
                // 利用反射獲取方法對象
                Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
                // 進行注冊,handler 是 controller 類的簡稱牲芋,invocableMethod 目標(biāo)方法對象撩笆,mapping 是接口映射信息(HTTP 方法 + URL)
                registerHandlerMethod(handler, invocableMethod, mapping);
            });
        }
    }

       protected void registerHandlerMethod(Object handler, Method method, T mapping) {
           this.mappingRegistry.register(mapping, handler, method);
        }
        //  注冊 URL 與 Controller 方法
        public void register(T mapping, Object handler, Method method) {
            // Assert that the handler method is not a suspending one.
            if (KotlinDetector.isKotlinType(method.getDeclaringClass()) && KotlinDelegate.isSuspend(method)) {
                throw new IllegalStateException("Unsupported suspending handler method detected: " + method);
            }
            this.readWriteLock.writeLock().lock();
            try {
                HandlerMethod handlerMethod = createHandlerMethod(handler, method);
                validateMethodMapping(handlerMethod, mapping);
                // 將 mapping 信息與方法對應(yīng)起來
                this.mappingLookup.put(mapping, handlerMethod);

                List<String> directUrls = getDirectUrls(mapping);
                for (String url : directUrls) {
                    // 這里將 url 跟 mapping 信息對應(yīng)起來
                    this.urlLookup.add(url, mapping);
                }

                String name = null;
                if (getNamingStrategy() != null) {
                    name = getNamingStrategy().getName(handlerMethod, mapping);
                    addMappingName(name, handlerMethod);
                }

                CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
                if (corsConfig != null) {
                    this.corsLookup.put(handlerMethod, corsConfig);
                }

                this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directUrls, name));
            }
            finally {
                this.readWriteLock.writeLock().unlock();
            }
        }

以上代碼就是保存 URL 及對應(yīng)的 Controller 方法,這里分了兩個集合缸浦,一個是存放 URL 對應(yīng)的 mapping 信息(包括了 HTTP 方法及 URL 信息 )夕冲,一個是存放 mapping 信息及對應(yīng)的 Controller 方法。這里也用到了讀寫鎖的寫鎖裂逐,大家可以分析一下為什么要上鎖歹鱼。


尾聲

這篇文章主要講解了 Dispatcher 的初始化過程,然后以 HandlerMapping 為例講解了 Controller 方法的映射絮姆。接下來的文章【SpringMVC 篇(二)DispatcherServlet 請求】將會介紹請求發(fā)生的時候醉冤,Dispatcher 是如何處理請求的,以及各個消息轉(zhuǎn)換器篙悯、異步處理器蚁阳、攔截器之間的協(xié)作方式。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
禁止轉(zhuǎn)載鸽照,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者螺捐。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市矮燎,隨后出現(xiàn)的幾起案子定血,更是在濱河造成了極大的恐慌,老刑警劉巖诞外,帶你破解...
    沈念sama閱讀 218,284評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件澜沟,死亡現(xiàn)場離奇詭異,居然都是意外死亡峡谊,警方通過查閱死者的電腦和手機茫虽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評論 3 395
  • 文/潘曉璐 我一進店門刊苍,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人濒析,你說我怎么就攤上這事正什。” “怎么了号杏?”我有些...
    開封第一講書人閱讀 164,614評論 0 354
  • 文/不壞的土叔 我叫張陵婴氮,是天一觀的道長。 經(jīng)常有香客問我盾致,道長主经,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,671評論 1 293
  • 正文 為了忘掉前任绰上,我火速辦了婚禮旨怠,結(jié)果婚禮上渠驼,老公的妹妹穿的比我還像新娘蜈块。我一直安慰自己,他們只是感情好迷扇,可當(dāng)我...
    茶點故事閱讀 67,699評論 6 392
  • 文/花漫 我一把揭開白布百揭。 她就那樣靜靜地躺著,像睡著了一般蜓席。 火紅的嫁衣襯著肌膚如雪器一。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,562評論 1 305
  • 那天厨内,我揣著相機與錄音祈秕,去河邊找鬼。 笑死雏胃,一個胖子當(dāng)著我的面吹牛请毛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播瞭亮,決...
    沈念sama閱讀 40,309評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼方仿,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了统翩?” 一聲冷哼從身側(cè)響起仙蚜,我...
    開封第一講書人閱讀 39,223評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎厂汗,沒想到半個月后委粉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,668評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡娶桦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,859評論 3 336
  • 正文 我和宋清朗相戀三年贾节,在試婚紗的時候發(fā)現(xiàn)自己被綠了匣掸。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,981評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡氮双,死狀恐怖碰酝,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情戴差,我是刑警寧澤送爸,帶...
    沈念sama閱讀 35,705評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站暖释,受9級特大地震影響袭厂,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜球匕,卻給世界環(huán)境...
    茶點故事閱讀 41,310評論 3 330
  • 文/蒙蒙 一纹磺、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧亮曹,春花似錦橄杨、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至役耕,卻和暖如春采转,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背瞬痘。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評論 1 270
  • 我被黑心中介騙來泰國打工故慈, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人框全。 一個月前我還...
    沈念sama閱讀 48,146評論 3 370
  • 正文 我出身青樓察绷,卻偏偏與公主長得像,于是被迫代替她去往敵國和親竣况。 傳聞我的和親對象是個殘疾皇子克婶,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,933評論 2 355

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