Spring MVC请求处理(六) - UrlPathHelpe

2019-03-23  本文已影响0人  buzzerrookie

UrlPathHelper类是Spring中的一个帮助类,有很多与URL路径有关的实用方法,在介绍该类之前先明确一些路径和编码的概念。

Servlet 3.1规范中的路径

Servlet中有三个路径容易混淆,分别是ContextPath、ServletPath和PathInfo。

在请求路径中,以下等式永远成立:requestURI = contextPath + servletPath + pathInfo。

URL路径

在收到客户端的请求后,由Web容器来决定向哪个Web应用转发该请求。所选择的Web应用一定有最长的ContextPath与请求URL从起始处开始相匹配。URL中匹配的部分就是映射到servlet时的ContextPath。
Web容器必须使用如下的路径映射规则定位处理请求的servlet。
映射到servlet时使用的路径是请求对象中的请求URL除去ContextPath和路径参数(路径参数可以参考这篇文章)。下面的URL路径映射规则需要按顺序使用,第一个匹配后便不再尝试其他匹配:

  1. 容器尝试查找请求路径与servlet的精确匹配;
  2. 容器会递归地尝试匹配最长路径前缀。以/为分隔符,在路径树中一次步进一个目录。最长的匹配会决定由哪个servlet处理;
  3. 如果URL路径中的最后一段包含扩展名(如.jsp),那么容器会尝试匹配能处理扩展名的servlet。扩展名定义为最后一段中最后的点号(.)之后的部分;
  4. 如果前三个规则没有成功匹配,容器会尝试去为所请求的资源提供服务。如果为应用定义了默认servlet,则它会被使用。许多容器都提供了隐式的默认servlet。

映射规范

Web应用部署描述符使用如下规则定义映射:

  1. 以/开头并以/*结尾的字符串用于路径映射;
  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映射改为/*,根据映射规范第一条,它可用于路径映射。

<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

解释:

实例2

将web.xml中名为dispatcher的servlet映射改为/,根据映射规范第四条,它是默认的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

URI中的编码

以Tomcat为例,它使用ISO-8859-1作为URI和查询字符串的默认编码,有两种方法可以指定解析URI和查询字符串的编码:

UrlPathHelper类

UrlPathHelper类是Spring中的一个帮助类,有很多与URL路径有关的实用方法,现逐一介绍如下。

移除分号

与移除分号有关的方法如下:

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;
}

URI解码

若设置了解码属性则decodeRequestString方法对URI解码,相关方法代码如下:

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;
}

清理斜线

getSanitizedPath方法清理斜线,将URI中连续两个斜线替换为一个斜线,其代码如下所示:

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

解码并清理

decodeAndCleanUriString方法解码并清理URI,移除分号内容、清理斜线并解码

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解码、移除分号内容并清理斜线:

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;
}

实例验证

为了加深对这几个方法的理解,我们接着使用上文使用的项目调试,将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>

发起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

这印证了上文的观点:

应用中的路径

getPathWithinApplication方法返回请求URI在web应用中的路径,返回的路径已被解码、移除分号内容并清理斜线。

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) : "");
}

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方法已经对请求URI在web应用中的路径做了解码、移除分号内容并清理斜线操作
    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;
    }
}

请求查找路径

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);
    }
}

根据alwaysUseFullPath属性(默认是true)做不同的操作:

该方法用在AbstractHandlerMethodMapping的getHandlerInternal方法中获得查找路径用于寻找匹配的HandlerMethod。

参考文献

Servlet 3.1规范
https://tomcat.apache.org/tomcat-8.5-doc/config/http.html
https://wiki.apache.org/tomcat/FAQ/CharacterEncoding

上一篇 下一篇

猜你喜欢

热点阅读