基于
4.3.12
版本。
在使用SpringMVC来进行Web开发时,我们通常会选择让SpringMVC来代替Servlet容器来进行静态资源的请求处理(当然现在流行都是利用nginx等进行动静分离)。此时我们会进行如下的配置:
<mvc:resources location="/resources/" mapping="/resources/**" />
所以本篇博客要探究是在以上这句配置之后,SpringMVC底层发生的事情。
从spring-mvc-4.3.xsd
文件中可以看到
的配置:
该标签的另外一种解读方式是在 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
来进行解析的。
由上图中我们可以看到,对
的解析是被交给了ResourcesBeanDefinitionParser
类。
ResourcesBeanDefinitionParser
类通过观察 ResourcesBeanDefinitionParser
类覆写的parse
方法,我们可以发现:
其通过调用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 由转换为 List; 是因为Spring在AbstractApplicationContext类中的prepareBeanFactory(beanFactory)方法里向容器中注册了属性解析器 ResourceArrayPropertyEditor
values.add("locations", locations);
同样也硬编码注册了SimpleUrlHandlerMapping
,目的是将上面
中配置的mapping
属性值,与第一步注册的ResourceHttpRequestHandler
实例组成键值对; 之后匹配mapping
属性值的静态资源访问路径将交由ResourceHttpRequestHandler
实例来处理。例如这里就是 访问/resources/xx
下的静态资源将由本ResourceHttpRequestHandler
实例来处理。
ResourceHttpRequestHandler
类接下来让我们看看ResourceHttpRequestHandler
类,其中比较引入注目是其实现的HttpRequestHandler
, InitializingBean
接口(在实现的afterPropertiesSet
方法注册了PathResourceResolver
, 还有ResourceHttpMessageConverter
等)。
现在让我们尝试总结整个流程
1. 我们都知道 SpringMVC的处理请求的核心类为DispatcherServlet
,而核心方法为doDispatch
。
2. 当我们发起一个静态资源请求时, 最终在doDispatch -> getHandler
方法这里,最终筛选出我们上面注册的SimpleUrlHandlerMapping
实例。
3. 然后在doDispatch -> getHandlerAdapter
,筛选出原本注册HttpRequestHandlerAdapter
实例【ResourceHttpRequestHandler 就是由这个类来Adapter(适配)的】。说一句题外话,另外两个常用的Adapter分别是SimpleControllerHandlerAdapter
和RequestMappingHandlerAdapter
【注解@RequestMapping
相关的适配器】
4. HttpRequestHandlerAdapter
实现的handle
方法中,正是调用了ResourceHttpRequestHandler
实例的handleRequest
方法。
5. 所以我们的重心就是这个handleRequest
方法了。我们将放在下一小节进行讲解。
6. HttpRequestHandlerAdapter
实现的handle
方法执行完毕后,返回的ModelAndView
实例必为null。那么之后的View渲染跟本文就没啥关系了。
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, 略
}
}
最近在实际项目中遇到一个向所有请求的静态资源中统一插入一个隐藏的,其值动态的INPUT的需求,最终选择了如下方式
<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;
}
}
Spring4.1新特性——静态资源处理增强 (注意配置
时,要小心 xsd的版本;4.3和4.0是有相当大的区别的; 如果照着上面链接里开涛那样进行配置,报错起来玩死你。)
mvc:resources拦截资源显示问题