在web應用中,對於靜態資源,一般不會經常變化,所以通常會使用緩存,以提高效率。在spring-mvc中提供了幾種方式來處理靜態資源,其中一種是通過<mvc:resource>配置資源的位置和映射的路徑等信息。但是在3.2.12(包含3.2.12)以後,在配置的處理上有變化,這就是location屬性不能使用通配符模式,比如<mvc:resource location=“res/**” mapping=“res/**”/>這種方式,在3.2.12以前可以正常工作,但是在3.2.12及以後,不能正常工作,如請求/res/jquery.js,在判斷指定location下是否存在該資源時,解析成了/res/**/,導致在該位置下找不到資源,從而返回404錯誤。
下面從源碼來看一下這個處理過程
首先從spring-mvc的xsd文件中可以看到<mvc:resource>的配置:
<xsd:element name="resources">
<xsd:annotation>
<xsd:documentation
source="java:org.springframework.web.servlet.resource.ResourceHttpRequestHandler"><![CDATA[
Configures a handler for serving static resources such as images, js, and, css files with cache headers optimized for efficient
loading in a web browser. Allows resources to be served out of any path that is reachable via Spring's Resource handling.
]]></xsd:documentation>
</xsd:annotation>
<xsd:complexType>
<xsd:attribute name="mapping" use="required" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[
The URL mapping pattern, within the current Servlet context, to use for serving resources from this handler, such as "/resources/**"
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="location" use="required" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[
The resource location from which to serve static content, specified at a Spring Resource pattern.
Each location must point to a valid directory. Multiple locations may be specified as a comma-separated list,
and the locations will be checked for a given resource in the order specified. For example, a value of
"/, classpath:/META-INF/public-web-resources/" will allow resources to be served both from the web app
root and from any JAR on the classpath that contains a /META-INF/public-web-resources/ directory,
with resources in the web app root taking precedence.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="cache-period" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
<![CDATA[
Specifies the cache period for the resources served by this resource handler, in seconds.
The default is to not send any cache headers but rather to rely on last-modified timestamps only.
Set this to 0 in order to send cache headers that prevent caching, or to a positive number of
seconds in order to send cache headers with the given max-age value.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="order" type="xsd:int">
<xsd:annotation>
<xsd:documentation>
<![CDATA[
Specifies the order of the HandlerMapping for the resource handler. The default order is Ordered.LOWEST_PRECEDENCE - 1.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
</xsd:element>
可以看到其中處理靜態資源的類是org.springframework.web.servlet.resource.ResourceHttpRequestHandler,而且在location的描述中說明Each location must point to a valid directory. 即每個location都必須指向一個有效的目錄。
下面看一下org.springframework.web.servlet.resource.ResourceHttpRequestHandler中是如何處理靜態資源請求的:
首先它實現了org.springframework.web.HttpRequestHandler這個接口的handleRequest方法:
public void handleRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 這裏根據配置來設置緩存的header
checkAndPrepare(request, response, true);
// 獲取要獲取的資源,如果不存在,直接返回404錯誤
Resource resource = getResource(request);
if (resource == null) {
logger.debug("No matching resource found - returning 404");
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
// 省略部分代碼……
// 返回相應的資源,即把資源文件寫到響應中
writeContent(response, resource);
}
而getResource方法實現如下: protected Resource getResource(HttpServletRequest request) {
String path = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
// 省略部分處理和檢驗路徑的代碼……
// 這裏循環從配置的location下查找請求的靜態資源
for (Resource location : this.locations) {
try {
if (logger.isDebugEnabled()) {
logger.debug("Trying relative path [" + path + "] against base location: " + location);
}
Resource resource = location.createRelative(path);
// 判斷資源存在而且能夠讀取
if (resource.exists() && resource.isReadable()) {
// 這裏就是3.2.12及以後處理上的差別,在3.2.12前,不會判斷該資源是否在指定的路徑下,直接就返回了resource,而3.2.12及以後做了如下判斷
if (isResourceUnderLocation(resource, location)) {
if (logger.isDebugEnabled()) {
logger.debug("Found matching resource: " + resource);
}
return resource;
}
else {
if (logger.isTraceEnabled()) {
logger.trace("resource=\"" + resource + "\" was successfully resolved " +
"but is not under the location=\"" + location);
}
return null;
}
}
else if (logger.isTraceEnabled()) {
logger.trace("Relative resource doesn't exist or isn't readable: " + resource);
}
}
catch (IOException ex) {
logger.debug("Failed to create relative resource - trying next resource location", ex);
}
}
return null;
}
下面看看isResourceUnderLocation的實現:
private boolean isResourceUnderLocation(Resource resource, Resource location) throws IOException {
if (!resource.getClass().equals(location.getClass())) {
return false;
}
String resourcePath;
String locationPath;
if (resource instanceof UrlResource) {
resourcePath = resource.getURL().toExternalForm();
locationPath = location.getURL().toExternalForm();
}
else if (resource instanceof ClassPathResource) {
resourcePath = ((ClassPathResource) resource).getPath();
locationPath = ((ClassPathResource) location).getPath();
}
else if (resource instanceof ServletContextResource) {
resourcePath = ((ServletContextResource) resource).getPath();
locationPath = ((ServletContextResource) location).getPath();
}
else {
resourcePath = resource.getURL().getPath();
locationPath = location.getURL().getPath();
}
// 這裏是對路徑的處理,如果我們配置的是/res/**,那麼直接拼接成了/res/**/,如果請求資源爲/res/jquery.js,那麼會判斷res/jquery.js是否以/res/**/開頭,如果不是,則返回該location下沒有該資源,導致不能404錯誤
locationPath = (locationPath.endsWith("/") ||
!StringUtils.hasLength(locationPath) ? locationPath : locationPath + "/");
if (!resourcePath.startsWith(locationPath)) {
return false;
}
if (resourcePath.contains("%")) {
// Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars...
if (URLDecoder.decode(resourcePath, "UTF-8").contains("../")) {
if (logger.isTraceEnabled()) {
logger.trace("Resolved resource path contains \"../\" after decoding: " + resourcePath);
}
return false;
}
}
return true;
}
基於上面的源碼分析,可以看出,這種處理靜態資源的方式不是向前兼容的,所以在升級spring-mvc時需要特別注意這點。