Spring源碼分析(五)SpringMVC是怎樣處理請求的放坏?

前言

上一節(jié)我們看到了SpringMVC在初始化的時候做了大量的準(zhǔn)備工作,本章節(jié)就重點跟蹤下SpringMVC的實際調(diào)用過程亿傅。

1媒峡、DispatcherServlet

記不記得,在大學(xué)期間或者剛接觸Java WEB開發(fā)的時候葵擎,前后端交互往往需要一個Servlet來接收請求谅阿,并返回信息。時至今日酬滤,Servlet仍不過時签餐。

如果是一個SpringMVC的項目,在WEB.XML里面需要配置一個DispatcherServlet盯串,它本質(zhì)就是個Servlet」谏悖或許我們還有印象,在請求到達(dá)的時候,就會調(diào)用到Servlet的service方法黄锤。那么负甸,說回到SpringMVC打月,就是FrameworkServlet類的service方法迫淹。

    protected void service(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        String method = request.getMethod();
        if (method.equalsIgnoreCase(RequestMethod.PATCH.name())) {
            processRequest(request, response);
        }
        else {
            super.service(request, response);
        }
    }

經(jīng)過一系列的判斷匹配,最后調(diào)用到DispatcherServlet類的doService方法。可以看到的是,doService方法向request里面添加了很多默認(rèn)的屬性,如果需要,我們就可以拿到。并在最后調(diào)用了實際的處理方法习霹,doDispatch()伪阶。

protected void doService(HttpServletRequest request, HttpServletResponse response) {
    // Make framework objects available to handlers and view objects.
    request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
    request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
    request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
    request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());

    FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
    if (inputFlashMap != null) {
        request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
    }
    request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
    request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
    
    doDispatch(request, response);
}

2筹误、doDispatch

實際處理的時候友存,大致分為幾個步驟膨俐。

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) {
    HttpServletRequest processedRequest = request;
    HandlerExecutionChain mappedHandler = null;
    boolean multipartRequestParsed = false;
    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
    try {
        ModelAndView mv = null;
        Exception dispatchException = null;
        try {
            processedRequest = checkMultipart(request);
            multipartRequestParsed = (processedRequest != request);
            //第一步 確定請求的處理器
            mappedHandler = getHandler(processedRequest);
            if (mappedHandler == null || mappedHandler.getHandler() == null) {
                //URL沒有找到匹配項兄淫,返回404
                noHandlerFound(processedRequest, response);
                return;
            }
            // 第二步 確定請求的處理適配器
            HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

            //第三步 調(diào)用攔截器 (方法調(diào)用前執(zhí)行)
            if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                return;
            }
            // 第四步  方法調(diào)用 
            mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
            //第五步 調(diào)用攔截器  (方法調(diào)用后執(zhí)行)
            mappedHandler.applyPostHandle(processedRequest, response, mv);
        }
        //第六步 處理方法返回 (返回頁面或者屬性)
        processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
        //第七步 調(diào)用攔截器(請求完成后執(zhí)行)
        mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
    }
}

不著急捕虽,下面我們一個一個的來看。

2.1坡脐、 獲取處理器 getHandler

獲取處理器泄私,就是根據(jù)請求的uri,匹配到Method方法。具體做法挖滤,我們在上一節(jié)Spring源碼分析(四)SpringMVC初始化做了預(yù)估崩溪,實際上Spring也確實是這樣做的。
根據(jù)uri獲取handlerMapping斩松,再以handlerMapping為key伶唯,從handlerMethods容器中拿到相應(yīng)的HandlerMethod對象。不過惧盹,它把HandlerMethod對象做了兩次封裝乳幸,源碼來看。

public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    List<Match> matches = new ArrayList<Match>();
    //lookupPath就是uri钧椰,拿到mapping粹断。以/user/index為例,mapping如下:
    //[{[/user/index],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}]
    List<T> directPathMatches = this.urlMap.get(lookupPath);
    if (directPathMatches != null) {
        //從handlerMethods容器中拿到Method對象,封裝成Match對象
        //Match對象其實就兩個屬性 mapping和handlerMethod對象
        addMatchingMappings(directPathMatches, matches, request);
    }
    HandlerMethod handler = matches.get(0).handlerMethod;

    //最后返回的是HandlerExecutionChain對象
    //里面包含兩個屬性嫡霞。handler>就是HandlerMethod對象
    //還有個攔截器列表瓶埋。interceptorList,在第三步調(diào)用攔截器就是循環(huán)這個List來調(diào)用
    return getHandlerExecutionChain(handler, request);
}

2.2诊沪、獲取適配器 getHandlerAdapter

這個比較簡單养筒。在初始化的時候,注冊了幾個適配器端姚。判斷上一步拿到的Handler是什么類型晕粪,就返回什么適配器,這里返回的是RequestMappingHandlerAdapter實例渐裸。

2.3、調(diào)用攔截器 (方法調(diào)用前執(zhí)行)

攔截器是鏈?zhǔn)秸{(diào)用尚氛,因為可能會有多個攔截器盆顾。攔截器的第一個方法怠褐,也是預(yù)處理方法preHandle是有返回值的。如果返回false您宪,整個請求就到此結(jié)束。在業(yè)務(wù)里磷杏,我們可以讓這個攔截器做一些校驗工作,不符合預(yù)期就返回false捏卓。需要注意的是interceptorIndex 這個變量极祸,它記錄當(dāng)前調(diào)用到了第幾個攔截器慈格。為什么要記錄這個呢浴捆?等看到后置攔截的時候我們就知道了稿械。

boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) {
    if (getInterceptors() != null) {
        for (int i = 0; i < getInterceptors().length; i++) {
            HandlerInterceptor interceptor = getInterceptors()[i];
            //preHandle方法如果返回false,接著就調(diào)用攔截器的后置方法页眯。
            //因為整個請求已經(jīng)結(jié)束了
            if (!interceptor.preHandle(request, response, this.handler)) {
                triggerAfterCompletion(request, response, null);
                return false;
            }
            this.interceptorIndex = i;
        }
    }
    return true;
}

2.4厢呵、方法調(diào)用

方法調(diào)用就是解析請求的參數(shù),拿到Method對象直接invoke即可碌奉。調(diào)用之后根據(jù)返回值渲染視圖寒砖。

2.4.1、參數(shù)解析

我們在上一章節(jié)已經(jīng)看到,SpringMVC初始化的時候茅逮,加載注冊了很多解析器的類判哥,其中有參數(shù)解析器和返回值類型解析器,如今就派上用場挺身。

一個HTTP請求的方法锌仅,不管有多少參數(shù),有多少種參數(shù)的類型热芹。最終伊脓,它們都是從哪來呢?沒錯株搔,就是Request。一切從Request而來纵隔。參數(shù)解析的方法最終返回的是一個Object[] args帆卓,就是參數(shù)值的數(shù)組。

  • 拿到方法上的參數(shù)列表糊啡,循環(huán)此列表
  • 調(diào)用解析器來解析參數(shù)吁津。它們的頂層接口是HandlerMethodArgumentResolver。它只有兩個方法梭依,supportsParameter典尾、resolveArgument钾埂。解析過程全靠這兩個方法,supports用來判斷是否應(yīng)該由此類解析姜性,resolve才是真正解析髓考。
  • 返回參數(shù)值
private Object[] getMethodArgumentValues(NativeWebRequest request, 
      ModelAndViewContainer mavContainer,Object... providedArgs) throws Exception {
    //獲取方法參數(shù)列表
    MethodParameter[] parameters = getMethodParameters();
    //args就是解析后的參數(shù)值的數(shù)組
    Object[] args = new Object[parameters.length];
    for (int i = 0; i < parameters.length; i++) {
        MethodParameter parameter = parameters[i];
        if (args[i] != null) {
            continue;
        }
        //判斷parameter是否能被某一個解析器所解析
        if (this.argumentResolvers.supportsParameter(parameter)) {
            try {
                //拿到上一步的解析器氨菇,解析拿到返回值放入args
                args[i] = this.argumentResolvers.resolveArgument(
                        parameter, mavContainer, request, this.dataBinderFactory);
                continue;
            }
        }
    }
    return args;
}

下面我們看一下HttpServletRequest在解析器里面具體的實現(xiàn)。這個參數(shù)會調(diào)用到ServletRequestMethodArgumentResolver解析器射赛,里面其實是一些if else奶是。

public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
        NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {

    Class<?> paramType = parameter.getParameterType();
    if (WebRequest.class.isAssignableFrom(paramType)) {
        return webRequest;
    }
    HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
    if (ServletRequest.class.isAssignableFrom(paramType) ) {
        Object nativeRequest = webRequest.getNativeRequest(paramType);
        return nativeRequest;
    }
    else if (HttpSession.class.isAssignableFrom(paramType)) {
        return request.getSession();
    }
    else if (HttpMethod.class.equals(paramType)) {
        return ((ServletWebRequest) webRequest).getHttpMethod();
    }
    else if (Principal.class.isAssignableFrom(paramType)) {
        return request.getUserPrincipal();
    }
    else if (Locale.class.equals(paramType)) {
        return RequestContextUtils.getLocale(request);
    }
    else if (InputStream.class.isAssignableFrom(paramType)) {
        return request.getInputStream();
    }
    else if (Reader.class.isAssignableFrom(paramType)) {
        return request.getReader();
    }
    //未完......
}
2.4.2、invoke

上一步通過各種解析器之后初嘹,返回一個Object類型的參數(shù)數(shù)組沮趣。有了Method對象,參數(shù)驻龟,調(diào)用就變得簡單了缸匪。

public Object invokeForRequest(NativeWebRequest request, ModelAndViewContainer mavContainer,
        Object... providedArgs) throws Exception {
    Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
    Object returnValue = doInvoke(args);
    if (logger.isTraceEnabled()) {
        logger.trace("Method [" + getMethod().getName() + "] returned [" + returnValue + "]");
    }
    return returnValue;
}
2.4.3凌蔬、返回值的解析

invoke之后,方法返回Object 類型的returnValue懈词。上面說了辩诞,解析器分為參數(shù)解析器和返回值解析器兩種译暂。So,返回值解析器就是在這里被調(diào)用秧秉。

它的解析和參數(shù)解析器套路基本一致象迎,先判斷是否該由此類解析呛踊,然后交由該類解析。記得剛工作的時候汪厨,只要是返回頁面的愉择,都是通過new ModelAndView().setName("xxx")來返回,后來發(fā)現(xiàn)直接返回視圖名字的字符串也可以衷戈,大感驚奇殖妇。原來,SpringMVC是在這里處理的疲吸。
我們來看這個類ViewNameMethodReturnValueHandler的解析方法前鹅。

public void handleReturnValue(Object returnValue, MethodParameter returnType,
        ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
    //如果返回的值是字符串類型
    //比如 /index,setViewName的工作它來搞
    if (returnValue instanceof String) {
        String viewName = (String) returnValue;
        mavContainer.setViewName(viewName);
        if (isRedirectViewName(viewName)) {
            mavContainer.setRedirectModelScenario(true);
        }
    }
}

SpringMVC比較重要的一點是支持restful烦租,是通過ResponseBody注解除盏。它又是怎么處理的呢者蠕?在注冊HandlerAdapter的時候,默認(rèn)給它添加了消息轉(zhuǎn)換器粪小。里面有7種類型抡句,其中有一個MappingJacksonHttpMessageConverter就是專門負(fù)責(zé)ResponseBody注解的。

來到RequestResponseBodyMethodProcessor類逞壁,它來負(fù)責(zé)匹配解析ResponseBody注解锐锣。判斷方法很簡單

public boolean supportsReturnType(MethodParameter returnType) {
    return (AnnotationUtils.findAnnotation(returnType.getContainingClass(), 
                            ResponseBody.class) != null ||
            returnType.getMethodAnnotation(ResponseBody.class) != null);
}

再來看它的handle方法雕憔。

public void handleReturnValue(Object returnValue, MethodParameter returnType,
        ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
        throws IOException, HttpMediaTypeNotAcceptableException {
    //這個屬性很重要,在此設(shè)置為true分瘦,先記住,下面再看蟋恬。
    mavContainer.setRequestHandled(true);
    //具體用消息轉(zhuǎn)換器來寫入趁冈,這里的消息轉(zhuǎn)換器就是
    //MappingJacksonHttpMessageConverter
    writeWithMessageConverters(returnValue, returnType, webRequest);
}

protected <T> void writeWithMessageConverters(T returnValue, MethodParameter returnType,
            ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
            throws IOException, HttpMediaTypeNotAcceptableException {

    //...........省略大部分代碼 
    //設(shè)置數(shù)據(jù)格式編碼等 application/json;charset=UTF-8
    if (selectedMediaType != null) {
        selectedMediaType = selectedMediaType.removeQualityValue();
        //messageConverters就是包含MappingJacksonHttpMessageConverter在內(nèi)的多種轉(zhuǎn)換器
        for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
            //先判斷是否可寫 判斷方式也很簡單渗勘,就是看mediaType是否是application/json
            if (messageConverter.canWrite(returnValueClass, selectedMediaType)) {
                //拿到返回值
                returnValue = this.adviceChain.invoke(returnValue, returnType, selectedMediaType,
                        (Class<HttpMessageConverter<?>>) messageConverter.getClass(), inputMessage, outputMessage);
                if (returnValue != null) {
                    //寫的過程旺坠,先設(shè)置Response的頭信息、編碼蹋肮,再調(diào)用jackson.databind包里的方法寫入
                    ((HttpMessageConverter<T>) messageConverter).write(returnValue, selectedMediaType, outputMessage);
                    //寫完之后刷新
                    //outputMessage.getBody().flush();
                }
                return;
            }
        }
    }
}
2.4.4璧疗、獲取ModelAndView

方法調(diào)用完了崩侠,返回值也都解析了。視圖需不需要返回改抡,返回到哪里系瓢?ModelAndView來決定。

private ModelAndView getModelAndView(ModelAndViewContainer mavContainer,
            ModelFactory modelFactory, NativeWebRequest webRequest) throws Exception {
    
    //在解析ResponseBody的時候阵赠,我們說有個屬性很重要肌稻。mavContainer.setRequestHandled(true);
    //在這里就用到了爹谭,說明不需要視圖
    if (mavContainer.isRequestHandled()) {
        return null;
    }
    ModelMap model = mavContainer.getModel();
    ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model);
    if (!mavContainer.isViewReference()) {
        mav.setView((View) mavContainer.getView());
    }
    return mav;
}

行文至此榛搔,關(guān)于方法調(diào)用算是都已經(jīng)處理完畢了。我們可以看出來腹泌,其實調(diào)用很簡單凉袱, 關(guān)鍵在于做好參數(shù)解析和返回值解析的工作。

2.5钟鸵、調(diào)用攔截器 (方法調(diào)用后執(zhí)行)

又到了一個攔截器的調(diào)用涤躲。這次它調(diào)用的是postHandle方法。

for (int i = getInterceptors().length - 1; i >= 0; i--) {
    HandlerInterceptor interceptor = getInterceptors()[i];
    interceptor.postHandle(request, response, this.handler, mv);
}

2.6蒙袍、處理結(jié)果

這里是真正響應(yīng)請求的地方嫩挤。先是判斷ModelAndView是否為空俐镐,如果為空說明返回的不是視圖,就沒必要往下執(zhí)行叼风。

if (mv != null && !mv.wasCleared()) {
    render(mv, request, response);
    if (errorView) {
        WebUtils.clearErrorRequestAttributes(request);
    }
}

重點在于render方法棍苹。它最終調(diào)用了renderMergedOutputModel方法枢里,渲染輸出。我們在配置文件中彬碱,都要配置一個視圖解析器viewResolver奥洼,這里就是用到的它。解析出來完整的視圖路徑后嚼沿,利用Servlet的RequestDispatcher直接做轉(zhuǎn)發(fā)就完成了這一步的工作。

protected void renderMergedOutputModel(
            Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) {
    //獲取返回的路徑 視圖解析器配置的prefix加上返回的viewName遣妥,再加上后綴p:suffix
    String dispatcherPath = prepareForRendering(requestToExpose, response);
    //獲取RequestDispatcher箫踩,
    RequestDispatcher rd = getRequestDispatcher(requestToExpose, dispatcherPath);
    //直接轉(zhuǎn)發(fā)
    rd.forward(requestToExpose, response);
}

2.7辨图、調(diào)用攔截器 (視圖渲染后執(zhí)行)

在調(diào)用攔截器的預(yù)處理方法時,提到了一個變量:interceptorIndex 吱韭。在這里就能看到它的作用鱼的。攔截器可能是多個的凑阶,它是鏈?zhǔn)秸{(diào)用的過程。比如有5個攔截器姨俩。如果在第3個攔截器的preHandler方法返回了false师郑,后兩個攔截器的After不應(yīng)該再被執(zhí)行宝冕。所以在后置攔截方法是從interceptorIndex 開始的。

void triggerAfterCompletion(HttpServletRequest request, 
        HttpServletResponse response, Exception ex)throws Exception {
    for (int i = this.interceptorIndex; i >= 0; i--) {
        HandlerInterceptor interceptor = getInterceptors()[i];
        try {
            interceptor.afterCompletion(request, response, this.handler, ex);
        }
    }
}

3菊卷、總結(jié)

以上就是SpringMVC處理一個請求的所有流程宝剖。DispatcherServlet其本質(zhì)就是個Servlet万细,一切請求經(jīng)過它來處理。它根據(jù)請求的uri找到對應(yīng)的Method對象襟雷,然后從Request中拿到參數(shù)解析成我們想要的類型仁烹,調(diào)用具體方法。通過不同的返回值解析器來確定返回的數(shù)據(jù)的類型是什么计呈,需不需要響應(yīng)視圖捌显。然后調(diào)用RequestDispatcher 直接轉(zhuǎn)發(fā)总寒。最后通過HandlerInterceptor可以讓我們有機會參與到SpringMVC處理環(huán)節(jié)中去,在具體方法執(zhí)行的不同時機加入我們自定義的業(yè)務(wù)善镰。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末炫欺,一起剝皮案震驚了整個濱河市熏兄,隨后出現(xiàn)的幾起案子摩桶,更是在濱河造成了極大的恐慌,老刑警劉巖岛宦,帶你破解...
    沈念sama閱讀 212,454評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件砾肺,死亡現(xiàn)場離奇詭異防嗡,居然都是意外死亡,警方通過查閱死者的電腦和手機裙盾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評論 3 385
  • 文/潘曉璐 我一進店門番官,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人门躯,你說我怎么就攤上這事讶凉∩娇祝” “怎么了?”我有些...
    開封第一講書人閱讀 157,921評論 0 348
  • 文/不壞的土叔 我叫張陵褐望,是天一觀的道長譬挚。 經(jīng)常有香客問我酪呻,道長,這世上最難降的妖魔是什么漆腌? 我笑而不...
    開封第一講書人閱讀 56,648評論 1 284
  • 正文 為了忘掉前任闷尿,我火速辦了婚禮女坑,結(jié)果婚禮上匆骗,老公的妹妹穿的比我還像新娘。我一直安慰自己盟广,他們只是感情好瓮钥,可當(dāng)我...
    茶點故事閱讀 65,770評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著肋拔,像睡著了一般呀酸。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,950評論 1 291
  • 那天艾栋,我揣著相機與錄音蝗砾,去河邊找鬼携冤。 笑死曾棕,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的申尤。 我是一名探鬼主播衙耕,決...
    沈念sama閱讀 39,090評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼橙喘,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了饰潜?” 一聲冷哼從身側(cè)響起磁奖,我...
    開封第一講書人閱讀 37,817評論 0 268
  • 序言:老撾萬榮一對情侶失蹤比搭,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后蜜托,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體橄务,經(jīng)...
    沈念sama閱讀 44,275評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡蜂挪,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,592評論 2 327
  • 正文 我和宋清朗相戀三年棠涮,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片史煎。...
    茶點故事閱讀 38,724評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡篇梭,死狀恐怖酝枢,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情喉磁,我是刑警寧澤官脓,帶...
    沈念sama閱讀 34,409評論 4 333
  • 正文 年R本政府宣布卑笨,位于F島的核電站赤兴,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏座舍。R本人自食惡果不足惜陨帆,卻給世界環(huán)境...
    茶點故事閱讀 40,052評論 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望榆鼠。 院中可真熱鬧亥鸠,春花似錦妆够、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至家妆,卻和暖如春灾螃,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背揩徊。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留嵌赠,地道東北人塑荒。 一個月前我還...
    沈念sama閱讀 46,503評論 2 361
  • 正文 我出身青樓姜挺,卻偏偏與公主長得像齿税,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子炊豪,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,627評論 2 350

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

  • Spring Web MVC Spring Web MVC 是包含在 Spring 框架中的 Web 框架凌箕,建立于...
    Hsinwong閱讀 22,364評論 1 92
  • 對于java中的思考的方向,1必須要看前端的頁面词渤,對于前端的頁面基本的邏輯牵舱,如果能理解最好,不理解也要知道幾點缺虐。 ...
    神尤魯?shù)婪?/span>閱讀 806評論 0 0
  • 一心向著目標(biāo)前進的人芜壁,整個世界都會為你讓路。 之前也一直聽設(shè)立目標(biāo)的重要性高氮,但從沒認(rèn)認(rèn)真真的設(shè)定目標(biāo)...
    甜甜的522閱讀 127評論 0 0
  • 作為一個花粉過敏,又比較宅的人罪裹,春天里的爭奇斗艷對我毫無吸引力饱普,油菜花运挫、桃花、櫻花费彼、杜鵑......朋友圈里賞遍了...
    雪了個雪_簡書閱讀 353評論 0 2
  • 每日橙思 我覺得一點一點的達(dá)成其實并沒有太多的成就感滑臊,因為都是量變,當(dāng)引起質(zhì)變的時候才能帶來強烈的成就感箍铲。 今天跟...
    cf85ea560887閱讀 143評論 0 0