SpringMVC源碼分析3:DispatcherServlet的初始化與請求轉(zhuǎn)發(fā)

在我們第一次學(xué)Servlet編程,學(xué)java web的時候徙缴,還沒有那么多框架伊约。我們開發(fā)一個簡單的功能要做的事情很簡單,就是繼承HttpServlet兽愤,根據(jù)需要重寫一下doGet垃它,doPost方法,跳轉(zhuǎn)到我們定義好的jsp頁面烹看。Servlet類編寫完之后在web.xml里注冊這個Servlet類国拇。

除此之外,沒有其他了惯殊。我們啟動web服務(wù)器酱吝,在瀏覽器中輸入地址,就可以看到瀏覽器上輸出我們寫好的頁面土思。為了更好的理解上面這個過程务热,你需要學(xué)習(xí)關(guān)于Servlet生命周期的三個階段,就是所謂的“init-service-destroy”己儒。

以上的知識崎岂,我覺得對于你理解SpringMVC的設(shè)計(jì)思想,已經(jīng)足夠了闪湾。SpringMVC當(dāng)然可以稱得上是一個復(fù)雜的框架冲甘,但是同時它又遵循Servlet世界里最簡單的法則,那就是“init-service-destroy”。我們要分析SpringMVC的初始化流程江醇,其實(shí)就是分析DispatcherServlet類的init()方法濒憋,讓我們帶著這種單純的觀點(diǎn)眼虱,打開DispatcherServlet的源碼一窺究竟吧拳喻。

<init-param>配置元素讀取

用Eclipse IDE打開DispatcherServlet類的源碼定页,ctrl+T看一下谨读。

DispatcherServlet類的初始化入口方法init()定義在HttpServletBean這個父類中刀疙,HttpServletBean類作為一個直接繼承于HttpServlet類的類冀偶,覆寫了HttpServlet類的init()方法帽哑,實(shí)現(xiàn)了自己的初始化行為溯祸。

@Override
    public final void init() throws ServletException {
        if (logger.isDebugEnabled()) {
            logger.debug("Initializing servlet '" + getServletName() + "'");
        }

        // Set bean properties from init parameters.
        try {
            PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
            BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
            ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
            bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, this.environment));
            initBeanWrapper(bw);
            bw.setPropertyValues(pvs, true);
        }
        catch (BeansException ex) {
            logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
            throw ex;
        }

        // Let subclasses do whatever initialization they like.
        initServletBean();

        if (logger.isDebugEnabled()) {
            logger.debug("Servlet '" + getServletName() + "' configured successfully");
        }
    }

這里的initServletBean()方法在HttpServletBean類中是一個沒有任何實(shí)現(xiàn)的空方法羽嫡,它的目的就是留待子類實(shí)現(xiàn)自己的初始化邏輯本姥,也就是我們常說的模板方法設(shè)計(jì)模式。SpringMVC在此生動的運(yùn)用了這個模式厂僧,init()方法就是模版方法模式中的模板方法扣草,SpringMVC真正的初始化過程,由子類FrameworkServlet中覆寫的initServletBean()方法觸發(fā)颜屠。

再看一下init()方法內(nèi)被try,catch塊包裹的代碼辰妙,里面涉及到BeanWrapper,PropertyValues甫窟,ResourceEditor這些Spring內(nèi)部非常底層的類密浑。要深究具體代碼實(shí)現(xiàn)上面的細(xì)節(jié),需要對Spring框架源碼具有相當(dāng)深入的了解粗井。我們這里先避繁就簡尔破,從代碼效果和設(shè)計(jì)思想上面來分析這段try,catch塊內(nèi)的代碼所做的事情:

  • 注冊一個字符串到資源文件的編輯器,讓Servlet下面的<init-param>配置元素可以使用形如“classpath:”這種方式指定SpringMVC框架bean配置文件的來源浇衬。
  • 將web.xml中在DispatcherServlet這個Servlet下面的<init-param>配置元素利用JavaBean的方式(即通過setter方法)讀取到DispatcherServlet中來懒构。

這兩點(diǎn),我想通過下面一個例子來說明一下耘擂。

我在web.xml中注冊的DispatcherServlet配置如下:

<!-- springMVC配置開始 -->
    <servlet>
        <servlet-name>appServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring/spring-servlet.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>appServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
    <!-- springMVC配置結(jié)束 -->

可以看到胆剧,我注冊了一個名為contextConfigLocation的<init-param>元素,其值為“classpath:spring/spring-servlet.xml”醉冤,這也是大家常常用來指定SpringMVC配置文件路徑的方法秩霍。上面那段try,catch塊包裹的代碼發(fā)揮的作用,一個是將“classpath:spring/spring-servlet.xml”這段字符串轉(zhuǎn)換成classpath路徑下的一個資源文件蚁阳,供框架初始化讀取配置元素铃绒。在我的工程中是在spring文件夾下面的配置文件spring-servlet.xml。

另外一個作用螺捐,就是將contextConfigLocation的值讀取出來颠悬,然后通過setContextConfigLocation()方法設(shè)置到DispatcherServlet中矮燎,這個setContextConfigLocation()方法是在FrameworkServlet類中定義的,也就是上面繼承類圖中DispatcherServlet的直接父類椿疗。

我們在setContextConfigLocation()方法上面打上一個斷點(diǎn)漏峰,啟動web工程糠悼,可以看到下面的調(diào)試結(jié)果届榄。

HttpServletBean類的作者是大名鼎鼎的Spring之父Rod Johnson。作為POJO編程哲學(xué)的大師倔喂,他在HttpServletBean這個類的設(shè)計(jì)中铝条,運(yùn)用了依賴注入思想完成了<init-param>配置元素的讀取。他抽離出HttpServletBean這個類的目的也在于此席噩,就是“以依賴注入的方式來讀取Servlet類的<init-param>配置信息”班缰,而且這里很明顯是一種setter注入。

明白了HttpServletBean類的設(shè)計(jì)思想悼枢,我們也就知道可以如何從中獲益埠忘。具體來說,我們繼承HttpServletBean類(就像DispatcherServlet做的那樣)馒索,在類中定義一個屬性莹妒,為這個屬性加上setter方法后,我們就可以在<init-param>元素中為其定義值绰上。在類被初始化后旨怠,值就會被注入進(jìn)來,我們可以直接使用它蜈块,避免了樣板式的getInitParameter()方法的使用鉴腻,而且還免費(fèi)享有Spring中資源編輯器的功能,可以在web.xml中百揭,通過“classpath:”直接指定類路徑下的資源文件爽哎。

注意,雖然SpringMVC本身為了后面初始化上下文的方便器一,使用了字符串來聲明和設(shè)置contextConfigLocation參數(shù)课锌,但是將其聲明為Resource類型,同樣能夠成功獲取盹舞。鼓勵讀者們自己繼承HttpServletBean寫一個測試用的Servlet類产镐,并設(shè)置一個參數(shù)來調(diào)試一下,這樣能夠幫助你更好的理解獲取配置參數(shù)的過程踢步。

容器上下文的建立

上一篇文章中提到過癣亚,SpringMVC使用了Spring容器來容納自己的配置元素,擁有自己的bean容器上下文获印。在SpringMVC初始化的過程中述雾,非常關(guān)鍵的一步就是要建立起這個容器上下文,而這個建立上下文的過程,發(fā)生在FrameworkServlet類中玻孟,由上面init()方法中的initServletBean()方法觸發(fā)唆缴。

@Override
    protected final void initServletBean() throws ServletException {
        getServletContext().log("Initializing Spring FrameworkServlet '" + getServletName() + "'");
        if (this.logger.isInfoEnabled()) {
            this.logger.info("FrameworkServlet '" + getServletName() + "': initialization started");
        }
        long startTime = System.currentTimeMillis();

        try {
            this.webApplicationContext = initWebApplicationContext();
            initFrameworkServlet();
        }
        catch (ServletException ex) {
            this.logger.error("Context initialization failed", ex);
            throw ex;
        }
        catch (RuntimeException ex) {
            this.logger.error("Context initialization failed", ex);
            throw ex;
        }

        if (this.logger.isInfoEnabled()) {
            long elapsedTime = System.currentTimeMillis() - startTime;
            this.logger.info("FrameworkServlet '" + getServletName() + "': initialization completed in " +
                    elapsedTime + " ms");
        }
    }

initFrameworkServlet()方法是一個沒有任何實(shí)現(xiàn)的空方法,除去一些樣板式的代碼黍翎,那么這個initServletBean()方法所做的事情已經(jīng)非常明白:

this.webApplicationContext = initWebApplicationContext();

這一句簡單直白的代碼面徽,道破了FrameworkServlet這個類,在SpringMVC類體系中的設(shè)計(jì)目的匣掸,它是 用來抽離出建立 WebApplicationContext 上下文這個過程的趟紊。

initWebApplicationContext()方法,封裝了建立Spring容器上下文的整個過程碰酝,方法內(nèi)的邏輯如下:

  1. 獲取由ContextLoaderListener初始化并注冊在ServletContext中的根上下文霎匈,記為rootContext
  2. 如果webApplicationContext已經(jīng)不為空,表示這個Servlet類是通過編程式注冊到容器中的(Servlet 3.0+中的ServletContext.addServlet() )送爸,上下文也由編程式傳入铛嘱。若這個傳入的上下文還沒被初始化,將rootContext上下文設(shè)置為它的父上下文袭厂,然后將其初始化墨吓,否則直接使用。
  3. 通過wac變量的引用是否為null嵌器,判斷第2步中是否已經(jīng)完成上下文的設(shè)置(即上下文是否已經(jīng)用編程式方式傳入)肛真,如果wac==null成立,說明該Servlet不是由編程式注冊到容器中的爽航。此時以contextAttribute屬性的值為鍵蚓让,在ServletContext中查找上下文,查找得到讥珍,說明上下文已經(jīng)以別的方式初始化并注冊在contextAttribute下历极,直接使用。
  4. 檢查wac變量的引用是否為null衷佃,如果wac==null成立趟卸,說明2、3兩步中的上下文初始化策略都沒成功氏义,此時調(diào)用createWebApplicationContext(rootContext)锄列,建立一個全新的以rootContext為父上下文的上下文,作為SpringMVC配置元素的容器上下文惯悠。大多數(shù)情況下我們所使用的上下文邻邮,就是這個新建的上下文。
  5. 以上三種初始化上下文的策略克婶,都會回調(diào)onRefresh(ApplicationContext context)方法(回調(diào)的方式根據(jù)不同策略有不同)筒严,onRefresh方法在DispatcherServlet類中被覆寫丹泉,以上面得到的上下文為依托,完成SpringMVC中默認(rèn)實(shí)現(xiàn)類的初始化鸭蛙。
  6. 最后摹恨,將這個上下文發(fā)布到ServletContext中,也就是將上下文以一個和Servlet類在web.xml中注冊名字有關(guān)的值為鍵娶视,設(shè)置為ServletContext的一個屬性晒哄。你可以通過改變publishContext的值來決定是否發(fā)布到ServletContext中,默認(rèn)為true歇万。

以上面6點(diǎn)跟蹤FrameworkServlet類中的代碼揩晴,可以比較清晰的了解到整個容器上下文的建立過程勋陪,也就能夠領(lǐng)會到FrameworkServlet類的設(shè)計(jì)目的贪磺,它是用來建立一個和Servlet關(guān)聯(lián)的Spring容器上下文,并將其注冊到ServletContext中的诅愚。跳脫開SpringMVC體系寒锚,我們也能通過繼承FrameworkServlet類,得到與Spring容器整合的好處违孝,F(xiàn)rameworkServlet和HttpServletBean一樣刹前,是一個可以獨(dú)立使用的類。整個SpringMVC設(shè)計(jì)中雌桑,處處體現(xiàn)開閉原則喇喉,這里顯然也是其中一點(diǎn)。

初始化SpringMVC默認(rèn)實(shí)現(xiàn)類

初始化流程在FrameworkServlet類中流轉(zhuǎn)校坑,建立了上下文后拣技,通過onRefresh(ApplicationContext context)方法的回調(diào),進(jìn)入到DispatcherServlet類中耍目。

@Override
    protected void onRefresh(ApplicationContext context) {
        initStrategies(context);
    }

DispatcherServlet類覆寫了父類FrameworkServlet中的onRefresh(ApplicationContext context)方法膏斤,提供了SpringMVC各種編程元素的初始化。當(dāng)然這些編程元素邪驮,都是作為容器上下文中一個個bean而存在的莫辨。具體的初始化策略,在initStrategies()方法中封裝毅访。

protected void initStrategies(ApplicationContext context) {
        initMultipartResolver(context);
        initLocaleResolver(context);
        initThemeResolver(context);
        initHandlerMappings(context);
        initHandlerAdapters(context);
        initHandlerExceptionResolvers(context);
        initRequestToViewNameTranslator(context);
        initViewResolvers(context);
        initFlashMapManager(context);
    }

我們以其中initHandlerMappings(context)方法為例沮榜,分析一下這些SpringMVC編程元素的初始化策略,其他的方法喻粹,都是以類似的策略初始化的蟆融。

private void initHandlerMappings(ApplicationContext context) {
        this.handlerMappings = null;

        if (this.detectAllHandlerMappings) {
            // Find all HandlerMappings in the ApplicationContext, including ancestor contexts.
            Map<String, HandlerMapping> matchingBeans =
                    BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
            if (!matchingBeans.isEmpty()) {
                this.handlerMappings = new ArrayList<HandlerMapping>(matchingBeans.values());
                // We keep HandlerMappings in sorted order.
                OrderComparator.sort(this.handlerMappings);
            }
        }
        else {
            try {
                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.
            }
        }

        // Ensure we have at least one HandlerMapping, by registering
        // a default HandlerMapping if no other mappings are found.
        if (this.handlerMappings == null) {
            this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
            if (logger.isDebugEnabled()) {
                logger.debug("No HandlerMappings found in servlet '" + getServletName() + "': using default");
            }
        }
    }

detectAllHandlerMappings變量默認(rèn)為true,所以在初始化HandlerMapping接口默認(rèn)實(shí)現(xiàn)類的時候磷斧,會把上下文中所有HandlerMapping類型的Bean都注冊在handlerMappings這個List變量中振愿。如果你手工將其設(shè)置為false捷犹,那么將嘗試獲取名為handlerMapping的Bean,新建一個只有一個元素的List冕末,將其賦給handlerMappings萍歉。如果經(jīng)過上面的過程,handlerMappings變量仍為空档桃,那么說明你沒有在上下文中提供自己HandlerMapping類型的Bean定義枪孩。此時,SpringMVC將采用默認(rèn)初始化策略來初始化handlerMappings藻肄。

點(diǎn)進(jìn)去getDefaultStrategies看一下蔑舞。

@SuppressWarnings("unchecked")
    protected <T> List<T> getDefaultStrategies(ApplicationContext context, Class<T> strategyInterface) {
        String key = strategyInterface.getName();
        String value = defaultStrategies.getProperty(key);
        if (value != null) {
            String[] classNames = StringUtils.commaDelimitedListToStringArray(value);
            List<T> strategies = new ArrayList<T>(classNames.length);
            for (String className : classNames) {
                try {
                    Class<?> clazz = ClassUtils.forName(className, DispatcherServlet.class.getClassLoader());
                    Object strategy = createDefaultStrategy(context, clazz);
                    strategies.add((T) strategy);
                }
                catch (ClassNotFoundException ex) {
                    throw new BeanInitializationException(
                            "Could not find DispatcherServlet's default strategy class [" + className +
                                    "] for interface [" + key + "]", ex);
                }
                catch (LinkageError err) {
                    throw new BeanInitializationException(
                            "Error loading DispatcherServlet's default strategy class [" + className +
                                    "] for interface [" + key + "]: problem with class file or dependent class", err);
                }
            }
            return strategies;
        }
        else {
            return new LinkedList<T>();
        }
    }

它是一個范型的方法,承擔(dān)所有SpringMVC編程元素的默認(rèn)初始化策略嘹屯。方法的內(nèi)容比較直白攻询,就是以傳遞類的名稱為鍵,從defaultStrategies這個Properties變量中獲取實(shí)現(xiàn)類州弟,然后反射初始化钧栖。

需要說明一下的是defaultStrategies變量的初始化,它是在DispatcherServlet的靜態(tài)初始化代碼塊中加載的婆翔。

private static final Properties defaultStrategies;

    static {
        // Load default strategy implementations from properties file.
        // This is currently strictly internal and not meant to be customized
        // by application developers.
        try {
            ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, DispatcherServlet.class);
            defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
        }
        catch (IOException ex) {
            throw new IllegalStateException("Could not load 'DispatcherServlet.properties': " + ex.getMessage());
        }
    }
private static final String DEFAULT_STRATEGIES_PATH = "DispatcherServlet.properties";

這個DispatcherServlet.properties里面拯杠,以鍵值對的方式,記錄了SpringMVC默認(rèn)實(shí)現(xiàn)類啃奴,它在spring-webmvc-3.1.3.RELEASE.jar這個jar包內(nèi)潭陪,在org.springframework.web.servlet包里面。

# 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.annotation.DefaultAnnotationHandlerMapping

org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
    org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
    org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter

org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerExceptionResolver,\
    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

至此最蕾,我們分析完了initHandlerMappings(context)方法的執(zhí)行過程依溯,其他的初始化過程與這個方法非常類似。所有初始化方法執(zhí)行完后揖膜,SpringMVC正式完成初始化誓沸,靜靜等待Web請求的到來。

總結(jié)

回顧整個SpringMVC的初始化流程壹粟,我們看到拜隧,通過HttpServletBean、FrameworkServlet趁仙、DispatcherServlet三個不同的類層次洪添,SpringMVC的設(shè)計(jì)者將三種不同的職責(zé)分別抽象,運(yùn)用模版方法設(shè)計(jì)模式分別固定在三個類層次中雀费。其中HttpServletBean完成的是<init-param>配置元素的依賴注入干奢,F(xiàn)rameworkServlet完成的是容器上下文的建立,DispatcherServlet完成的是SpringMVC具體編程元素的初始化策略盏袄。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末忿峻,一起剝皮案震驚了整個濱河市薄啥,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌逛尚,老刑警劉巖垄惧,帶你破解...
    沈念sama閱讀 211,817評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異绰寞,居然都是意外死亡到逊,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評論 3 385
  • 文/潘曉璐 我一進(jìn)店門滤钱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來觉壶,“玉大人,你說我怎么就攤上這事件缸⊥校” “怎么了?”我有些...
    開封第一講書人閱讀 157,354評論 0 348
  • 文/不壞的土叔 我叫張陵停团,是天一觀的道長旷坦。 經(jīng)常有香客問我,道長佑稠,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,498評論 1 284
  • 正文 為了忘掉前任旗芬,我火速辦了婚禮舌胶,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘疮丛。我一直安慰自己幔嫂,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,600評論 6 386
  • 文/花漫 我一把揭開白布誊薄。 她就那樣靜靜地躺著履恩,像睡著了一般。 火紅的嫁衣襯著肌膚如雪呢蔫。 梳的紋絲不亂的頭發(fā)上切心,一...
    開封第一講書人閱讀 49,829評論 1 290
  • 那天,我揣著相機(jī)與錄音片吊,去河邊找鬼绽昏。 笑死,一個胖子當(dāng)著我的面吹牛俏脊,可吹牛的內(nèi)容都是我干的全谤。 我是一名探鬼主播,決...
    沈念sama閱讀 38,979評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼爷贫,長吁一口氣:“原來是場噩夢啊……” “哼认然!你這毒婦竟也來了补憾?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,722評論 0 266
  • 序言:老撾萬榮一對情侶失蹤卷员,失蹤者是張志新(化名)和其女友劉穎余蟹,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體子刮,經(jīng)...
    沈念sama閱讀 44,189評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡威酒,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,519評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了挺峡。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片葵孤。...
    茶點(diǎn)故事閱讀 38,654評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖橱赠,靈堂內(nèi)的尸體忽然破棺而出尤仍,到底是詐尸還是另有隱情,我是刑警寧澤狭姨,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布宰啦,位于F島的核電站,受9級特大地震影響饼拍,放射性物質(zhì)發(fā)生泄漏赡模。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,940評論 3 313
  • 文/蒙蒙 一师抄、第九天 我趴在偏房一處隱蔽的房頂上張望漓柑。 院中可真熱鬧,春花似錦叨吮、人聲如沸辆布。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽锋玲。三九已至,卻和暖如春涵叮,著一層夾襖步出監(jiān)牢的瞬間惭蹂,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評論 1 266
  • 我被黑心中介騙來泰國打工围肥, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留剿干,地道東北人。 一個月前我還...
    沈念sama閱讀 46,382評論 2 360
  • 正文 我出身青樓穆刻,卻偏偏與公主長得像置尔,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子氢伟,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,543評論 2 349

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