Spring MVC請求處理(六) - UrlPathHelper類

UrlPathHelper類是Spring中的一個幫助類,有很多與URL路徑有關(guān)的實用方法,在介紹該類之前先明確一些路徑和編碼的概念度气。

Servlet 3.1規(guī)范中的路徑

Servlet中有三個路徑容易混淆聘鳞,分別是ContextPath、ServletPath和PathInfo滔蝉。

  • Context Path定義為servlet所屬ServletContext關(guān)聯(lián)的路徑前綴。若該上下文是處于服務器基地址的默認上下文塔沃,則這個路徑是空串蝠引,這種情況請參考Tomcat的Context配置文檔Naming一節(jié)。否則蛀柴,如果這個上下文不處在服務器的根螃概,則這個路徑會以/開頭但不會以/結(jié)尾。
  • Servlet Path定義為請求路徑中直接對應到映射的部分鸽疾。這個路徑以/開頭吊洼,但當請求與/*或空串兩種模式匹配時是空串。
  • Path Info定義為請求路徑中既不是Context Path也不是Servlet Path的部分制肮,它要么是null冒窍,這是沒有額外路徑的情況下,要么是以/開頭的字符串弄企。

在請求路徑中超燃,以下等式永遠成立:requestURI = contextPath + servletPath + pathInfo。

URL路徑

在收到客戶端的請求后拘领,由Web容器來決定向哪個Web應用轉(zhuǎn)發(fā)該請求意乓。所選擇的Web應用一定有最長的ContextPath與請求URL從起始處開始相匹配。URL中匹配的部分就是映射到servlet時的ContextPath。
Web容器必須使用如下的路徑映射規(guī)則定位處理請求的servlet届良。
映射到servlet時使用的路徑是請求對象中的請求URL除去ContextPath和路徑參數(shù)(路徑參數(shù)可以參考這篇文章)笆凌。下面的URL路徑映射規(guī)則需要按順序使用,第一個匹配后便不再嘗試其他匹配:

  1. 容器嘗試查找請求路徑與servlet的精確匹配士葫;
  2. 容器會遞歸地嘗試匹配最長路徑前綴乞而。以/為分隔符,在路徑樹中一次步進一個目錄慢显。最長的匹配會決定由哪個servlet處理爪模;
  3. 如果URL路徑中的最后一段包含擴展名(如.jsp),那么容器會嘗試匹配能處理擴展名的servlet荚藻。擴展名定義為最后一段中最后的點號(.)之后的部分屋灌;
  4. 如果前三個規(guī)則沒有成功匹配,容器會嘗試去為所請求的資源提供服務应狱。如果為應用定義了默認servlet共郭,則它會被使用。許多容器都提供了隱式的默認servlet疾呻。

映射規(guī)范

Web應用部署描述符使用如下規(guī)則定義映射:

  1. 以/開頭并以/*結(jié)尾的字符串用于路徑映射除嘹;
  2. 以"*."前綴開頭的字符串用于擴展名映射;
  3. 空串""是特殊的模式岸蜗,精確地映射到應用上下文的根尉咕,舉例來說,對來自http://host:port/<context-root>/的請求散吵,PathInfo是/龙考,ServletPath和ContextPath都是空串"";
  4. 只包含/的字符串表明是應用的默認servlet矾睦,這種情況下ServletPath是請求URI減去ContextPath,PathInfo是null炎功;
  5. 其他字符串只會精確匹配枚冗。

接下來以一個簡單的Spring工程和Tomcat 8.5為例說明典型情況下各路徑的值。

實例1

將web.xml中名為dispatcher的servlet映射改為/*蛇损,根據(jù)映射規(guī)范第一條赁温,它可用于路徑映射。

<servlet-mapping>
    <servlet-name>dispatcher</servlet-name>
    <url-pattern>/*</url-pattern>
</servlet-mapping>

將工程打包為spring-mvc.war淤齐,以Get方法訪問http://localhost:8080/spring-mvc/paths股囊,日志輸出:

contextPath:/spring-mvc
servletPath:""
pathInfo:/paths

解釋:

  • 根據(jù)URL路徑一節(jié),/spring-mvc的Web應用能與/spring-mvc/paths最長匹配更啄,匹配的部分/spring-mvc即是ContextPath稚疹;
  • 根據(jù)URL路徑一節(jié),映射到servlet時使用的路徑是/spring-mvc/paths減去/spring-mvc祭务,所以/paths會用于映射匹配内狗,根據(jù)URL路徑第二條匹配規(guī)則怪嫌,/paths與/*匹配,根據(jù)“Servlet Path”一節(jié)柳沙,ServletPath為""岩灭;
  • 根據(jù)等式規(guī)則可得PathInfo為/paths。

實例2

將web.xml中名為dispatcher的servlet映射改為/赂鲤,根據(jù)映射規(guī)范第四條噪径,它是默認的servlet。

<servlet-mapping>
    <servlet-name>dispatcher</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

將工程打包為spring-mvc.war数初,以Get方法訪問http://localhost:8080/spring-mvc/paths熄云,日志輸出:

contextPath:/spring-mvc
servletPath:/paths
pathInfo:null
  • 根據(jù)URL路徑一節(jié),/spring-mvc的Web應用能與/spring-mvc/paths最長匹配妙真,匹配的部分/spring-mvc即是ContextPath缴允;
  • 根據(jù)URL路徑一節(jié),映射到servlet時使用的路徑是/spring-mvc/paths減去/spring-mvc珍德,所以/paths會用于映射匹配练般,根據(jù)URL路徑第四條匹配規(guī)則,/paths與由默認servlet處理锈候,根據(jù)映射規(guī)范第四條薄料,ServletPath為/paths,PathInfo為null泵琳。

URI中的編碼

  • GET方法:RFC2396第二章指出URI只能包含部分ASCII字符摄职,除了這些字符都需要用百分號轉(zhuǎn)義,但沒有規(guī)定轉(zhuǎn)義字符時使用何種編碼获列。

  • POST方法:Servlet 3.1規(guī)范3.11節(jié)指出如果客戶端請求沒有指定編碼谷市,那么默認使用ISO-8859-1解碼POST數(shù)據(jù)。

    Currently, many browsers do not send a char encoding qualifier with the Content-Type header, leaving open the determination of the character encoding for reading HTTP requests. The default encoding of a request the container uses to create the request reader and parse POST data must be “ISO-8859-1” if none has been specified by the client request. However, in order to indicate to the developer, in this case, the failure of the client to send a character encoding, the container returns null from the getCharacterEncoding method.

    ServletRequest接口的setCharacterEncoding方法用來設置編碼击孩,該方法可以覆蓋容器的默認編碼迫悠,但必須在POST數(shù)據(jù)被解析或數(shù)據(jù)讀取之前調(diào)用,一旦數(shù)據(jù)被讀取該方法調(diào)用便不再有效巩梢。

    If the client hasn’t set character encoding and the request data is encoded with a different encoding than the default as described above, breakage can occur. To remedy this situation, a new method setCharacterEncoding(String enc) has been added to the ServletRequest interface. Developers can override the character encoding supplied by the container by calling this method. It must be called prior to parsing any post data or reading any input from the request. Calling this method once data has been read will not affect the encoding.

以Tomcat為例创泄,它使用ISO-8859-1作為URI和查詢字符串的默認編碼,有兩種方法可以指定解析URI和查詢字符串的編碼:

  • 在server.xml配置文件的<Connector>元素上配置屬性URIEncoding括蝠,如URIEncoding="UTF-8"鞠抑。請注意從Tomcat 8開始,URIEncoding屬性的默認值是UTF-8忌警;
  • 在server.xml配置文件的<Connector>元素上配置屬性useBodyEncodingForURI為true(默認為false)搁拙,Tomcat會使用Content-Type頭或ServletRequest接口的setCharacterEncoding方法指定的編碼解析查詢字符串,若編碼不被支持,那么使用默認的ISO-8859-1感混。請注意該屬性只適用于查詢字符串端幼,不適用于URI的路徑部分。

UrlPathHelper類

UrlPathHelper類是Spring中的一個幫助類弧满,有很多與URL路徑有關(guān)的實用方法婆跑,現(xiàn)逐一介紹如下。

移除分號

與移除分號有關(guān)的方法如下:

public String removeSemicolonContent(String requestUri) {
    return (this.removeSemicolonContent ?
            removeSemicolonContentInternal(requestUri) : removeJsessionid(requestUri));
}

private String removeSemicolonContentInternal(String requestUri) {
    int semicolonIndex = requestUri.indexOf(';');
    while (semicolonIndex != -1) {
        int slashIndex = requestUri.indexOf('/', semicolonIndex);
        String start = requestUri.substring(0, semicolonIndex);
        requestUri = (slashIndex != -1) ? start + requestUri.substring(slashIndex) : start;
        semicolonIndex = requestUri.indexOf(';', semicolonIndex);
    }
    return requestUri;
}

private String removeJsessionid(String requestUri) {
    int startIndex = requestUri.toLowerCase().indexOf(";jsessionid=");
    if (startIndex != -1) {
        int endIndex = requestUri.indexOf(';', startIndex + 12);
        String start = requestUri.substring(0, startIndex);
        requestUri = (endIndex != -1) ? start + requestUri.substring(endIndex) : start;
    }
    return requestUri;
}
  • removeSemicolonContent方法根據(jù)removeSemicolonContent屬性決定是移除請求URI中的所有分號內(nèi)容還是只移除jsessionid部分庭呜,默認是前者滑进,所以這兩種情況都會移除jsessionid部分;
  • removeSemicolonContentInternal方法移除請求URI中所有的分號內(nèi)容募谎,注意URI中每段都可以有分號扶关,如/users/name;v=1.1/gender;value=male等形式;
  • removeJsessionid方法只移除請求URI中;jsessionid=xxx的部分而保留URI的其余部分(包括其他分號)数冬,移除jsessionid時不區(qū)分大小寫节槐。

URI解碼

若設置了解碼屬性則decodeRequestString方法對URI解碼,相關(guān)方法代碼如下:

public String decodeRequestString(HttpServletRequest request, String source) {
    if (this.urlDecode && source != null) {
        return decodeInternal(request, source);
    }
    return source;
}

@SuppressWarnings("deprecation")
private String decodeInternal(HttpServletRequest request, String source) {
    String enc = determineEncoding(request);
    try {
        return UriUtils.decode(source, enc);
    }
    catch (UnsupportedEncodingException ex) {
        if (logger.isWarnEnabled()) {
            logger.warn("Could not decode request string [" + source + "] with encoding '" + enc +
                    "': falling back to platform default encoding; exception message: " + ex.getMessage());
        }
        return URLDecoder.decode(source);
    }
}

protected String determineEncoding(HttpServletRequest request) {
    String enc = request.getCharacterEncoding();
    if (enc == null) {
        enc = getDefaultEncoding();
    }
    return enc;
}
  • determineEncoding方法調(diào)用HttpServletRequest的getCharacterEncoding方法獲取編碼拐纱,若沒有則使用默認的ISO-8859-1編碼铜异;
  • decodeInternal方法使用上一步得到的編碼解碼URI,若不支持此編碼則使用系統(tǒng)屬性file.encoding指定的編碼(從URLDecoder.decode的源碼可得秸架,注意該編碼也是Charset類的defaultCharset方法的返回值)揍庄。

清理斜線

getSanitizedPath方法清理斜線,將URI中連續(xù)兩個斜線替換為一個斜線东抹,其代碼如下所示:

private String getSanitizedPath(final String path) {
    String sanitized = path;
    while (true) {
        int index = sanitized.indexOf("http://");
        if (index < 0) {
            break;
        }
        else {
            sanitized = sanitized.substring(0, index) + sanitized.substring(index + 1);
        }
    }
    return sanitized;
}

解碼并清理

decodeAndCleanUriString方法解碼并清理URI蚂子,移除分號內(nèi)容、清理斜線并解碼

private String decodeAndCleanUriString(HttpServletRequest request, String uri) {
    uri = removeSemicolonContent(uri);
    uri = decodeRequestString(request, uri);
    uri = getSanitizedPath(uri);
    return uri;
}

getRequestUri方法

HttpServletRequest的getRequestURI方法的返回值未被容器解碼且沒有去掉分號部分且沒有查詢字符串缭黔,而UrlPathHelper類的getRequestUri方法會對該URI解碼食茎、移除分號內(nèi)容并清理斜線:

public String getRequestUri(HttpServletRequest request) {
    String uri = (String) request.getAttribute(WebUtils.INCLUDE_REQUEST_URI_ATTRIBUTE);
    if (uri == null) {
        uri = request.getRequestURI();
    }
    return decodeAndCleanUriString(request, uri);
}

getContextPath方法

HttpServletRequest的getContextPath方法的返回值未被容器解碼且去掉了分號部分,而UrlPathHelper類的getContextPath方法會對ContextPath解碼:

public String getContextPath(HttpServletRequest request) {
    String contextPath = (String) request.getAttribute(WebUtils.INCLUDE_CONTEXT_PATH_ATTRIBUTE);
    if (contextPath == null) {
        contextPath = request.getContextPath();
    }
    if ("/".equals(contextPath)) {
        // Invalid case, but happens for includes on Jetty: silently adapt it.
        contextPath = "";
    }
    return decodeRequestString(request, contextPath);
}

getServletPath方法

HttpServletRequest的getServletPath方法的返回值已被容器解碼且去掉了分號部分试浙,所以UrlPathHelper類的getServletPath方法不再對其解碼:

public String getServletPath(HttpServletRequest request) {
    String servletPath = (String) request.getAttribute(WebUtils.INCLUDE_SERVLET_PATH_ATTRIBUTE);
    if (servletPath == null) {
        servletPath = request.getServletPath();
    }
    if (servletPath.length() > 1 && servletPath.endsWith("/") && shouldRemoveTrailingServletPathSlash(request)) {
        // On WebSphere, in non-compliant mode, for a "/foo/" case that would be "/foo"
        // on all other servlet containers: removing trailing slash, proceeding with
        // that remaining slash as final lookup path...
        servletPath = servletPath.substring(0, servletPath.length() - 1);
    }
    return servletPath;
}

實例驗證

為了加深對這幾個方法的理解董瞻,我們接著使用上文使用的項目調(diào)試,將war包文件名改為spring mvc.war田巴,接著修改web.xml中的servlet映射,將DispatcherServlet映射到/patt"ern/*

<servlet-mapping>
    <servlet-name>dispatcher</servlet-name>
    <url-pattern>/patt"ern/*</url-pattern>
</servlet-mapping>

發(fā)起Get請求:localhost:8080/spring%20mvc/patt%22ern;foo=bar/urlPathHelper;v=1.1//%E6%B5%8B%E8%AF%95?param1=val1挟秤,部分日志輸出如下:

DEBUG c.s.controller.ServletInfoController - HttpServletRequest#getRequestURI: /spring%20mvc/patt%22ern;foo=bar/urlPathHelper;v=1.1/%E6%B5%8B%E8%AF%95
DEBUG c.s.controller.ServletInfoController - UrlPathHelper#getRequestUri: /spring mvc/patt"ern/urlPathHelper/測試
DEBUG c.s.controller.ServletInfoController - HttpServletRequest#getContextPath: /spring%20mvc
DEBUG c.s.controller.ServletInfoController - UrlPathHelper#getContextPath: /spring mvc
DEBUG c.s.controller.ServletInfoController - HttpServletRequest#getServletPath: /patt"ern
DEBUG c.s.controller.ServletInfoController - UrlPathHelper#getServletPath: /patt"ern

這印證了上文的觀點:

  • HttpServletRequest的getRequestURI方法的返回值未被容器解碼且沒有去掉分號部分壹哺,只是去掉了查詢字符串,而UrlPathHelper類的getRequestUri方法會對該URI用UTF-8編碼方式解碼艘刚、移除分號內(nèi)容并清理斜線(%20是空格管宵,%22是雙引號,測試兩字的UTF-8編碼是\xe6\xb5\x8b\xe8\xaf\x95)。
  • 為什么Spring會用UTF-8解碼呢箩朴,這是因為在web.xml中添加了CharacterEncodingFilter過濾器岗喉,為HttpServletRequest使用setCharacterEncoding方法設置了UTF-8編碼,否則會使用默認的ISO-8859-1炸庞。如果不加過濾器的話則可以在Content-Type頭添加charset=UTF-8钱床,這是因為Tomcat在實現(xiàn)ServletRequest接口時getCharacterEncoding方法內(nèi)部在setCharacterEncoding沒有設值時會從Content-Type取得編碼,不知道其他容器是否有這種類似的實現(xiàn)埠居;
  • HttpServletRequest的getContextPath方法的返回值未被容器解碼且去掉了分號部分查牌,而UrlPathHelper類的getContextPath方法會對ContextPath解碼;
  • HttpServletRequest的getServletPath方法的返回值已被容器解碼且去掉了分號部分滥壕,UrlPathHelper類的getServletPath方法不再對其解碼纸颜。Tomcat解碼時采用哪種編碼方式呢?這是由Tomcat的URIEncoding屬性指定的绎橘,見上文編碼一章胁孙。

應用中的路徑

getPathWithinApplication方法返回請求URI在web應用中的路徑,返回的路徑已被解碼称鳞、移除分號內(nèi)容并清理斜線涮较。

public String getPathWithinApplication(HttpServletRequest request) {
    String contextPath = getContextPath(request);
    String requestUri = getRequestUri(request);
    String path = getRemainingPath(requestUri, contextPath, true);
    if (path != null) {
        // Normal case: URI contains context path.
        return (StringUtils.hasText(path) ? path : "/");
    }
    else {
        return requestUri;
    }
}

/**
 * Match the given "mapping" to the start of the "requestUri" and if there
 * is a match return the extra part. This method is needed because the
 * context path and the servlet path returned by the HttpServletRequest are
 * stripped of semicolon content unlike the requesUri.
 */
private String getRemainingPath(String requestUri, String mapping, boolean ignoreCase) {
    int index1 = 0;
    int index2 = 0;
    for (; (index1 < requestUri.length()) && (index2 < mapping.length()); index1++, index2++) {
        char c1 = requestUri.charAt(index1);
        char c2 = mapping.charAt(index2);
        if (c1 == ';') {
            index1 = requestUri.indexOf('/', index1);
            if (index1 == -1) {
                return null;
            }
            c1 = requestUri.charAt(index1);
        }
        if (c1 == c2 || (ignoreCase && (Character.toLowerCase(c1) == Character.toLowerCase(c2)))) {
            continue;
        }
        return null;
    }
    if (index2 != mapping.length()) {
        return null;
    }
    else if (index1 == requestUri.length()) {
        return ""; // mapping與requestUri全匹配,額外的部分當然是空串了
    }
    else if (requestUri.charAt(index1) == ';') {
        index1 = requestUri.indexOf('/', index1);
    }
    return (index1 != -1 ? requestUri.substring(index1) : "");
}
  • getRemainingPath方法將mapping字符串與requestUri字符串相匹配胡岔,匹配過程中忽略掉requestUri中的分號部分法希。如果能匹配則返回requestUri除去匹配之外的額外部分,否則返回null靶瘸。舉個例子苫亦,requestUri是/data;v=1.1/users;foo=bar/extra,mapping是/data/users怨咪,那么該方法返回的就是/extra屋剑;
  • 正常情況下,getPathWithinApplication方法將匹配之外的額外部分作為請求URI在web應用中的路徑诗眨;
  • 異常情況下唉匾,無匹配返回請求URI。

Servlet映射中的路徑

getPathWithinServletMapping方法返回請求URI在Servlet映射中的路徑,這里需要再次注意請求URI锹引、ContextPath和ServletPath的編解碼和分號击罪,見上文。

/**
 * Return the path within the servlet mapping for the given request,
 * i.e. the part of the request's URL beyond the part that called the servlet,
 * or "" if the whole URL has been used to identify the servlet.
 * <p>Detects include request URL if called within a RequestDispatcher include.
 * <p>E.g.: servlet mapping = "/*"; request URI = "/test/a" -> "/test/a".
 * <p>E.g.: servlet mapping = "/"; request URI = "/test/a" -> "/test/a".
 * <p>E.g.: servlet mapping = "/test/*"; request URI = "/test/a" -> "/a".
 * <p>E.g.: servlet mapping = "/test"; request URI = "/test" -> "".
 * <p>E.g.: servlet mapping = "/*.test"; request URI = "/a.test" -> "".
 * @param request current HTTP request
 * @return the path within the servlet mapping, or ""
 */
public String getPathWithinServletMapping(HttpServletRequest request) {
    String pathWithinApp = getPathWithinApplication(request);
    String servletPath = getServletPath(request);
    String sanitizedPathWithinApp = getSanitizedPath(pathWithinApp); // 貌似這步有些多余峡懈,因為getPathWithinApplication方法已經(jīng)對請求URI在web應用中的路徑做了解碼、移除分號內(nèi)容并清理斜線操作
    String path;

    // If the app container sanitized the servletPath, check against the sanitized version
    if (servletPath.contains(sanitizedPathWithinApp)) {
        path = getRemainingPath(sanitizedPathWithinApp, servletPath, false);
    }
    else {
        path = getRemainingPath(pathWithinApp, servletPath, false);
    }

    if (path != null) {
        // Normal case: URI contains servlet path.
        return path;
    }
    else {
        // Special case: URI is different from servlet path.
        String pathInfo = request.getPathInfo();
        if (pathInfo != null) {
            // Use path info if available. Indicates index page within a servlet mapping?
            // e.g. with index page: URI="/", servletPath="/index.html"
            return pathInfo;
        }
        if (!this.urlDecode) {
            // No path info... (not mapped by prefix, nor by extension, nor "/*")
            // For the default servlet mapping (i.e. "/"), urlDecode=false can
            // cause issues since getServletPath() returns a decoded path.
            // If decoding pathWithinApp yields a match just use pathWithinApp.
            path = getRemainingPath(decodeInternal(request, pathWithinApp), servletPath, false);
            if (path != null) {
                return pathWithinApp;
            }
        }
        // Otherwise, use the full servlet path.
        return servletPath;
    }
}
  • getPathWithinServletMapping方法與getPathWithinApplication方法相似与斤,也利用了getRemainingPath方法肪康;
  • 正常情況下荚恶,ServletPath在URI中有匹配,getPathWithinServletMapping方法返回的是應用中的路徑除去ServletPath磷支;
  • 異常情況沒太看懂谒撼,好像是處理Javadoc最后一個例子的情況,但/*.test不是一個合法的映射啊……雾狈。

請求查找路徑

getLookupPathForRequest方法返回請求的查找路徑廓潜,其代碼如下所示:

public String getLookupPathForRequest(HttpServletRequest request) {
    // Always use full path within current servlet context?
    if (this.alwaysUseFullPath) {
        return getPathWithinApplication(request);
    }
    // Else, use path within current servlet mapping if applicable
    String rest = getPathWithinServletMapping(request);
    if (!"".equals(rest)) {
        return rest;
    }
    else {
        return getPathWithinApplication(request);
    }
}

根據(jù)alwaysUseFullPath屬性(默認是true)做不同的操作:

  • 當設置為true時返回請求在應用中的路徑;
  • 當設置為false時箍邮,如果沒使用整個URL定位servlet茉帅,那么返回請求在servlet映射中的路徑,否則返回請求在應用中的路徑锭弊。

該方法用在AbstractHandlerMethodMapping的getHandlerInternal方法中獲得查找路徑用于尋找匹配的HandlerMethod堪澎。

參考文獻

Servlet 3.1規(guī)范
https://tomcat.apache.org/tomcat-8.5-doc/config/http.html
https://wiki.apache.org/tomcat/FAQ/CharacterEncoding

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市味滞,隨后出現(xiàn)的幾起案子樱蛤,更是在濱河造成了極大的恐慌,老刑警劉巖剑鞍,帶你破解...
    沈念sama閱讀 218,122評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件昨凡,死亡現(xiàn)場離奇詭異,居然都是意外死亡蚁署,警方通過查閱死者的電腦和手機便脊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來光戈,“玉大人哪痰,你說我怎么就攤上這事【米保” “怎么了晌杰?”我有些...
    開封第一講書人閱讀 164,491評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長筷弦。 經(jīng)常有香客問我肋演,道長,這世上最難降的妖魔是什么烂琴? 我笑而不...
    開封第一講書人閱讀 58,636評論 1 293
  • 正文 為了忘掉前任爹殊,我火速辦了婚禮,結(jié)果婚禮上奸绷,老公的妹妹穿的比我還像新娘边灭。我一直安慰自己,他們只是感情好健盒,可當我...
    茶點故事閱讀 67,676評論 6 392
  • 文/花漫 我一把揭開白布绒瘦。 她就那樣靜靜地躺著,像睡著了一般扣癣。 火紅的嫁衣襯著肌膚如雪惰帽。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,541評論 1 305
  • 那天父虑,我揣著相機與錄音该酗,去河邊找鬼。 笑死士嚎,一個胖子當著我的面吹牛呜魄,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播莱衩,決...
    沈念sama閱讀 40,292評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼爵嗅,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了笨蚁?” 一聲冷哼從身側(cè)響起睹晒,我...
    開封第一講書人閱讀 39,211評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎括细,沒想到半個月后伪很,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,655評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡奋单,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,846評論 3 336
  • 正文 我和宋清朗相戀三年锉试,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片览濒。...
    茶點故事閱讀 39,965評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡呆盖,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出匾七,到底是詐尸還是另有隱情絮短,我是刑警寧澤,帶...
    沈念sama閱讀 35,684評論 5 347
  • 正文 年R本政府宣布昨忆,位于F島的核電站丁频,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏邑贴。R本人自食惡果不足惜席里,卻給世界環(huán)境...
    茶點故事閱讀 41,295評論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望拢驾。 院中可真熱鬧奖磁,春花似錦、人聲如沸繁疤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至躁染,卻和暖如春鸣哀,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背吞彤。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評論 1 269
  • 我被黑心中介騙來泰國打工我衬, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人饰恕。 一個月前我還...
    沈念sama閱讀 48,126評論 3 370
  • 正文 我出身青樓挠羔,卻偏偏與公主長得像,于是被迫代替她去往敵國和親埋嵌。 傳聞我的和親對象是個殘疾皇子破加,可洞房花燭夜當晚...
    茶點故事閱讀 44,914評論 2 355

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