在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时需要特别注意这点。