基于 4.3.12版本。

1. 概述

在使用SpringMVC来进行Web开发时,我们通常会选择让SpringMVC来代替Servlet容器来进行静态资源的请求处理(当然现在流行都是利用nginx等进行动静分离)。此时我们会进行如下的配置:

<!-- 静态资源文件; 注意这里说的是静态 -->
<mvc:resources location="/resources/" mapping="/resources/**" />

所以本篇博客要探究是在以上这句配置之后,SpringMVC底层发生的事情。

2. 细节

spring-mvc-4.3.xsd文件中可以看到<mvc:resource>的配置:
<mvc:resource>

该标签的另外一种解读方式是在 spring-webmvc-4.3.12.RELEASE.jar下的 META-INF/spring.handlers中有如下内容:

http\://www.springframework.org/schema/mvc=org.springframework.web.servlet.config.MvcNamespaceHandler

以上说明 mvc前缀的标签是由MvcNamespaceHandler来进行解析的。
MvcNamespaceHandler

由上图中我们可以看到,对<mvc:resource>的解析是被交给了ResourcesBeanDefinitionParser类。

2.1 ResourcesBeanDefinitionParser

通过观察 ResourcesBeanDefinitionParser类覆写的parse方法,我们可以发现:

  1. 其通过调用registerResourceHandler硬编码注册了ResourceHttpRequestHandler实例到Spring容器中。

    // ResourcesBeanDefinitionParser类的registerResourceHandler方法中
    String locationAttr = element.getAttribute("location");
    ManagedList<String> locations = new ManagedList<String>();
    // 这里就说明了我们在定义location的值时, 可以使用 , 分割多个地址。
    locations.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(locationAttr)));
    RootBeanDefinition resourceHandlerDef = new RootBeanDefinition(ResourceHttpRequestHandler.class);
    MutablePropertyValues values = resourceHandlerDef.getPropertyValues();
    // 之所以可以 List<String> 由转换为 List<Resource>;  是因为Spring在AbstractApplicationContext类中的prepareBeanFactory(beanFactory)方法里向容器中注册了属性解析器 ResourceArrayPropertyEditor
    values.add("locations", locations);
  2. 同样也硬编码注册了SimpleUrlHandlerMapping,目的是将上面<mvc:resources/>中配置的mapping属性值,与第一步注册的ResourceHttpRequestHandler实例组成键值对; 之后匹配mapping属性值的静态资源访问路径将交由ResourceHttpRequestHandler实例来处理。例如这里就是 访问/resources/xx下的静态资源将由本ResourceHttpRequestHandler实例来处理。

2.2 ResourceHttpRequestHandler

接下来让我们看看ResourceHttpRequestHandler类,其中比较引入注目是其实现的HttpRequestHandlerInitializingBean接口(在实现的afterPropertiesSet方法注册了PathResourceResolver, 还有ResourceHttpMessageConverter等)。

2.3 整体流程

现在让我们尝试总结整个流程
1. 我们都知道 SpringMVC的处理请求的核心类为DispatcherServlet,而核心方法为doDispatch
2. 当我们发起一个静态资源请求时, 最终在doDispatch -> getHandler 方法这里,最终筛选出我们上面注册的SimpleUrlHandlerMapping实例。
3. 然后在doDispatch -> getHandlerAdapter ,筛选出原本注册HttpRequestHandlerAdapter实例【ResourceHttpRequestHandler 就是由这个类来Adapter(适配)的】。说一句题外话,另外两个常用的Adapter分别是SimpleControllerHandlerAdapterRequestMappingHandlerAdapter【注解@RequestMapping相关的适配器】
4. HttpRequestHandlerAdapter实现的handle方法中,正是调用了ResourceHttpRequestHandler实例的handleRequest方法。
5. 所以我们的重心就是这个handleRequest方法了。我们将放在下一小节进行讲解。
6. HttpRequestHandlerAdapter实现的handle方法执行完毕后,返回的ModelAndView实例必为null。那么之后的View渲染跟本文就没啥关系了。

2.4 ResourceHttpRequestHandler.handleRequest方法
// ResourceHttpRequestHandler.handleRequest
@Override
public void handleRequest(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {

    // For very general mappings (e.g. "/") we need to check 404 first
    // 使用SpringMVC默认的方式(例如PathResourceResolver)获取Resource
    // 这里是一个扩展点, 开发者可以依据自身的需求加入自定义的ResourceResolver; 下面的部分我将给出一个例子
    Resource resource = getResource(request);
    // 没有找到就直接返回了
    if (resource == null) {
        logger.trace("No matching resource found - returning 404");
        response.sendError(HttpServletResponse.SC_NOT_FOUND);
        return;
    }

    // 如果这次的HTTP方法为 OPTIONS, 也是直接返回
    if (HttpMethod.OPTIONS.matches(request.getMethod())) {
        response.setHeader("Allow", getAllowHeader());
        return;
    }

    // Supported methods and required session
    checkRequest(request);

    // Header phase
    // 没有修改就直接返回了
    if (new ServletWebRequest(request, response).checkNotModified(resource.lastModified())) {
        logger.trace("Resource not modified - returning 304");
        return;
    }

    // Apply cache settings, if any
    prepareResponse(response);

    // Check the media type for the resource
    MediaType mediaType = getMediaType(request, resource);

    // Content phase
    if (METHOD_HEAD.equals(request.getMethod())) {
        setHeaders(response, resource, mediaType);
        logger.trace("HEAD request - skipping content");
        return;
    }

    ServletServerHttpResponse outputMessage = new ServletServerHttpResponse(response);
    // 如果这次不是分Range, 开始准备Response
    if (request.getHeader(HttpHeaders.RANGE) == null) {
        // 开始设置响应头
        // 注意这里有个细节是, 这里会设置响应头 "Content-Length"
        setHeaders(response, resource, mediaType);
        // 这里的 resourceHttpMessageConverter 实际为 ResourceHttpMessageConverter实例
        // 注意这里就会将读取到的Resource推送给响应流
        this.resourceHttpMessageConverter.write(resource, mediaType, outputMessage);
    }
    else {
        // 分Range, 略
    }
}

3. 实际案例

最近在实际项目中遇到一个向所有请求的静态资源中统一插入一个隐藏的,其值动态的INPUT的需求,最终选择了如下方式

<!-- 这个order必须比0大,因为负责处理Controller里请求的RequestMappingHandlerMapping的order正是0 -->
<mvc:resources mapping="/**" location="/" order="5">
        <mvc:resource-chain resource-cache="false"> 
            <mvc:resolvers> 
                <bean  class="com.xxx.springmvc.CustomResourceResolver">
                </bean> 
            </mvc:resolvers> 
        </mvc:resource-chain>       
</mvc:resources>

相应的CustomResourceResolver

public class CustomResourceResolver extends AbstractResourceResolver {

    private PathResourceResolver delegate = new PathResourceResolver();

    @Override
    protected Resource resolveResourceInternal(HttpServletRequest request, String requestPath,
            List<? extends Resource> locations, ResourceResolverChain chain) {

        // 最后一个参数 chain , 一看就是 Servlet中的Filter类似的职责链实现方式
        Resource resolveResource = delegate.resolveResource(request, requestPath, locations, chain);

        if (requestPath.endsWith(".html")) {
            ServletContextResource res = (ServletContextResource) resolveResource;
            return new ServletContextResourceEx(res.getServletContext(), res.getPath());
        }

        return resolveResource;
    }

}
  1. Spring4.1新特性——静态资源处理增强 (注意配置<mvc:resources/>时,要小心 xsd的版本;4.3和4.0是有相当大的区别的; 如果照着上面链接里开涛那样进行配置,报错起来玩死你。)

  2. mvc:resources拦截资源显示问题

  3. spring-mvc 3.2.12及以后配置处理的变化
  4. 《spring-framework-reference-4.2.4.pdf 》 P542
Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐