SpringMVC 中的静态资源处理
SpringMVC 中的静态资源处理
官方文档
官方文档:
参考博客:
Serve Static Resources with Spring | Baeldung
Cachable Static Assets with Spring MVC | Baeldung
关于 Resource
和 ResourceLoader
得源码分析请看《Spring 中的资源获取.md》
注册/配置静态资源处理器
《《SpringMVC》- 第十篇:基于注解配置 SpringMVC》中的
addResourceHandlers
小节
静态资源处理器 - ResourceHttpRequestHandler
HttpRequestHandler
接口的子类,处理对静态资源的请求。
ResourceHttpRequestHandler#locations
属性为一个 Resource
类型的列表,该 handler 允许从这些位置提供静态资源。资源可以从类路径位置提供,例如 classpath:/public-web-resources/
",这样就允许提供打包在 jar 文件中的诸如 .js
、.css
等资源。
此 handler 还可以配置 ResourcesResolver
资源解析链和 ResourceTransformer
资源转换链,以支持所服务静态资源的任意解析和转换。默认情况下,PathResourceResolver
只是根据配置的位置信息(Resource
对象)查找静态资源。应用可以配置附加的 ResourcesResolver
解析器和 ResourceTransformer
转换器,例如 VersionResourceResolver
,它可以为静态资源解析和准备包含版本信息的 URL。
这些我们后面的实践都会涉及到
此 handler 还正确地评估请求中的 Last-Modified
消息头 (如果存在),以便适当地返回 304
状态码,表示服务器资源未更新,不需要返回新的资源,避免对客户端已经缓存的资源造成不必要的服务器开销。
接下来简单分析一下 ResourceHttpRequestHandler
中的代码,精力有限,我们这里只分析 ResourceHandlerRegistration
和 ResourceHandlerRegistry
中注册的 ResourceHttpRequestHandler
相关的源码分析
首先 ResourceHandlerRegistration#getRequestHandler
的源码
protected ResourceHttpRequestHandler getRequestHandler() {
ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler();
if (this.resourceChainRegistration != null) {
handler.setResourceResolvers(this.resourceChainRegistration.getResourceResolvers());
handler.setResourceTransformers(this.resourceChainRegistration.getResourceTransformers());
}
handler.setLocationValues(this.locationValues);
if (this.cacheControl != null) {
handler.setCacheControl(this.cacheControl);
}
else if (this.cachePeriod != null) {
handler.setCacheSeconds(this.cachePeriod);
}
handler.setUseLastModified(this.useLastModified);
return handler;
}
ResourceHandlerRegistration#getRequestHandler
中涉及到的对 ResourceHttpRequestHandler
的自定义。即
-
ResourceHttpRequestHandler#setResourceResolvers
- 重点 -
ResourceHttpRequestHandler#setResourceTransformers
- 重点 -
ResourceHttpRequestHandler#setLocationValues
- 重点 -
ResourceHttpRequestHandler#setCacheControl
- 常用 -
ResourceHttpRequestHandler#setCacheSeconds
- 不常用 -
ResourceHttpRequestHandler#setUseLastModified
- 常用
再看 ResourceHandlerRegistry#getHandlerMapping
的源码
@Nullable
@SuppressWarnings("deprecation")
protected AbstractHandlerMapping getHandlerMapping() {
if (this.registrations.isEmpty()) {
return null;
}
Map<String, HttpRequestHandler> urlMap = new LinkedHashMap<>();
for (ResourceHandlerRegistration registration : this.registrations) {
for (String pathPattern : registration.getPathPatterns()) {
ResourceHttpRequestHandler handler = registration.getRequestHandler();
if (this.pathHelper != null) {
handler.setUrlPathHelper(this.pathHelper);
}
if (this.contentNegotiationManager != null) {
handler.setContentNegotiationManager(this.contentNegotiationManager);
}
handler.setServletContext(this.servletContext);
handler.setApplicationContext(this.applicationContext);
try {
handler.afterPropertiesSet();
}
catch (Throwable ex) {
throw new BeanInitializationException("Failed to init ResourceHttpRequestHandler", ex);
}
urlMap.put(pathPattern, handler);
}
}
return new SimpleUrlHandlerMapping(urlMap, this.order);
}
ResourceHandlerRegistry#getHandlerMapping
中对 ResourceHttpRequestHandler
的自定义
-
ResourceHttpRequestHandler#setUrlPathHelper
- 不常用 -
ResourceHttpRequestHandler#setContentNegotiationManager
不常用 -
ResourceHttpRequestHandler#setServletContext
不常用 -
ResourceHttpRequestHandler#setApplicationContext
不常用
在 ResourceHandlerRegistry#getHandlerMapping
中,调用 ResourceHttpRequestHandler#afterPropertiesSet
之前,以及设置了这么多属性
接下来,我们看 ResourceHttpRequestHandler#afterPropertiesSet
接口做了什么
除了 useLastModified 是 ResourceHttpRequestHandler 中的字段,
CacheControl
、cacheSeconds
都是 ResourceHttpRequestHandler 继承的WebContentGenerator
中的字段,用于设置响应的跟缓存相关的消息头,直到是什么就可以了,这里不深入了解
实现了 InitializingBean 接口
主要做三件事:
-
将
List<String> locationValues
字段中路径字符串转化为Resource
对象保存到List<Resource> locations
字段中locationValues
中的路径必须以/
结尾。不然,在PathResourceResolver#getResource
方法中的Resource resource = location.createRelative(resourcePath);
语句会执行失败,导致无法找到文件。原因是路径拼接的时候少了一个/
。
-
根据
List<ResourceResolver> resourceResolvers
中保存的解析器列表,构造ResourceResolverChain resolverChain
资源解析链 -
根据
List<ResourceTransformer> resourceTransformers
中保存的转换器列表,构造ResourceTransformerChain transformerChain
资源转换链
源码分析
先是将资源路径字符串列表(locationValues
)转换 Resource 列表(locations
),然后开始初始化资源解析器列表(主要是准备 PathResourceResolver 类型的解析器)和资源转换器列表,最后将这两个列表分别转化为资源解析链对象和资源转换链对象。
public void afterPropertiesSet() throws Exception {
// locationValues -> locations
// 将资源路径字符串列表转换Resource列表
resolveResourceLocations();
if (logger.isWarnEnabled() && CollectionUtils.isEmpty(this.locations)) {
logger.warn("Locations list is empty. No resources will be served unless a " +
"custom ResourceResolver is configured as an alternative to PathResourceResolver.");
}
// 如果资源解析器列表为空,添加默认的资源解析器 PathResourceResolver,
// 不得不说,PathResourceResolver 的出镜率是真的高
if (this.resourceResolvers.isEmpty()) {
this.resourceResolvers.add(new PathResourceResolver());
}
// 只用于处理 PathResourceResolver类型的解析器
// 将转化出来的Resource列表初始化到资源解析器列表中所有的 allowedLocations字段为空的 PathResourceResolver类型的解析器 的allowedLocations字段中
// 资源解析器列表中一般只有一个 PathResourceResolver类型的解析器,因为也只需要一个
initAllowedLocations();
// Initialize immutable resolver and transformer chains
// 将资源解析器列表转化为资源解析链对象
this.resolverChain = new DefaultResourceResolverChain(this.resourceResolvers);
// 将资源转换器列表转化为资源转换链对象,同时还传入了资源解析链对象作为参数
this.transformerChain = new DefaultResourceTransformerChain(this.resolverChain, this.resourceTransformers);
if (this.resourceHttpMessageConverter == null) {
this.resourceHttpMessageConverter = new ResourceHttpMessageConverter();
}
if (this.resourceRegionHttpMessageConverter == null) {
this.resourceRegionHttpMessageConverter = new ResourceRegionHttpMessageConverter();
}
ContentNegotiationManager manager = getContentNegotiationManager();
if (manager != null) {
setMediaTypes(manager.getMediaTypeMappings());
}
@SuppressWarnings("deprecation")
org.springframework.web.accept.PathExtensionContentNegotiationStrategy strategy =
initContentNegotiationStrategy();
if (strategy != null) {
setMediaTypes(strategy.getMediaTypes());
}
}
其中,我们先看看 resolveResourceLocations
,具体是怎么转换的,其实主要还是调用 ApplicationContext
的 getResource
方法获取 Resource
对象, 而ApplicationContext 的 getResource 方法本身就支持三种格式的路径:web 应用程序根目录、编译输出路径、系统文件路径,
Web 应用中
ApplicationContext
的具体实现类为AnnotationConfigWebApplicationContext
,getResource
方法继承自DefaultResourceLoader
转换成功后,可以通过 getLocations
方法获取查找目的地资源列表,getLocations
方法会在后面的 ResourceHttpRequestHandler#getResource
中用到。
private void resolveResourceLocations() {
// 判断 locationValues 路径字符串是否为空
if (CollectionUtils.isEmpty(this.locationValues)) {
return;
}
else if (!CollectionUtils.isEmpty(this.locations)) {
throw new IllegalArgumentException("Please set either Resource-based \"locations\" or " +
"String-based \"locationValues\", but not both.");
}
ApplicationContext applicationContext = obtainApplicationContext();
// 依次遍历路径字符串,转换成Resource资源对象
for (String location : this.locationValues) {
// 一般未配置 embeddedValueResolver
if (this.embeddedValueResolver != null) {
String resolvedLocation = this.embeddedValueResolver.resolveStringValue(location);
if (resolvedLocation == null) {
throw new IllegalArgumentException("Location resolved to null: " + location);
}
location = resolvedLocation;
}
Charset charset = null;
location = location.trim();
// 一般路径也不会以URL_RESOURCE_CHARSET_PREFIX开头 PS:URL_RESOURCE_CHARSET_PREFIX = "[charset="
if (location.startsWith(URL_RESOURCE_CHARSET_PREFIX)) {
int endIndex = location.indexOf(']', URL_RESOURCE_CHARSET_PREFIX.length());
if (endIndex == -1) {
throw new IllegalArgumentException("Invalid charset syntax in location: " + location);
}
String value = location.substring(URL_RESOURCE_CHARSET_PREFIX.length(), endIndex);
charset = Charset.forName(value);
location = location.substring(endIndex + 1);
}
// 所以最终,还是通过 ApplicationContext的getResource 方法获取资源对象
// 而 ApplicationContext 的 getResource 方法本身就支持三种格式的路径:web应用程序根目录、编译输出路径、系统文件路径
// Web应用中 ApplicationContext 的具体实现类为 AnnotationConfigWebApplicationContext, getResource 方法继承自 DefaultResourceLoader
Resource resource = applicationContext.getResource(location);
this.locations.add(resource);
if (charset != null) {
if (!(resource instanceof UrlResource)) {
throw new IllegalArgumentException("Unexpected charset for non-UrlResource: " + resource);
}
this.locationCharsets.put(resource, charset);
}
}
}
ResourceResolverChain 和 ResourceTransformerChain
现在,我们专门来看看 ResourceResolverChain
和 ResourceTransformerChain
到底是什么东西。
ResourceResolverChain
接口:
public interface ResourceResolverChain {
// 根据请求对象和URL中包含的资源路径和服务器上的可供查找资源路径列表(即指定查找范围)来查找资源
@Nullable
Resource resolveResource(@Nullable HttpServletRequest request, String requestPath, List<? extends Resource> locations);
// 将给定的内部资源路径解析成面向外部的公共URL路径,以便客户机用于访问位于给定内部资源路径上的资源。
// 这在渲染到客户端的URL链接时非常有用。
// 在 ResourceUrlProvider 中有所使用
@Nullable
String resolveUrlPath(String resourcePath, List<? extends Resource> locations);
}
ResourceResolverChain
只有一个实现 DefaultResourceResolverChain
,DefaultResourceResolverChain
实际上是一个单向链表的节点类,字段 nextChain
保存着下一个节点(DefaultResourceResolverChain
)的指针。
class DefaultResourceResolverChain implements ResourceResolverChain {
// 当前链表节点的资源解析器
@Nullable
private final ResourceResolver resolver;
// 当前链表节点的下一个链表节点
@Nullable
private final ResourceResolverChain nextChain;
// 用于通过 List<? extends ResourceResolver> resolvers 初始化 DefaultResourceResolverChain节点组成的链表 的三个方法
// 其中,initChain 方法主要通过倒叙迭代构建 DefaultResourceResolverChain节点组成的链表的方法很棒
......
@Override
@Nullable
public Resource resolveResource(
@Nullable HttpServletRequest request, String requestPath, List<? extends Resource> locations) {
// 直接将解析工作委托给当前链表节点中的资源解析器,同时增加一个参数:下一个资源处理链节点,方便资源解析器在解析失败的时候调用下一个资源处理链节点中的资源解析器进行处理
return (this.resolver != null && this.nextChain != null ?
this.resolver.resolveResource(request, requestPath, locations, this.nextChain) : null);
}
@Override
@Nullable
public String resolveUrlPath(String resourcePath, List<? extends Resource> locations) {
// 直接将解析工作委托给当前链表节点中的资源解析器,同时增加一个参数:下一个资源处理链节点,方便资源解析器在解析失败的时候调用下一个资源处理链节点中的资源解析器进行处理
return (this.resolver != null && this.nextChain != null ?
this.resolver.resolveUrlPath(resourcePath, locations, this.nextChain) : null);
}
}
ResourceTransformerChain
接口:
public interface ResourceTransformerChain {
// 返回相关的资源解析链对象,用于解析被转换的资源,
// 当需要被转换的资源中有对其他资源的链接的时候,这个其他资源就需要用资源解析链对象解析一下
ResourceResolverChain getResolverChain();
// 传入请求对象和带转换的资源对象返回转换过后的资源对象
Resource transform(HttpServletRequest request, Resource resource) throws IOException;
}
ResourceTransformerChain
只有一个实现 DefaultResourceTransformerChain
,DefaultResourceTransformerChain
实际上是一个单向链表的节点,字段 nextChain
保存着下一个节点(DefaultResourceTransformerChain
)的指针。
class DefaultResourceTransformerChain implements ResourceTransformerChain {
// 资源解析链对象
// 注意,资源转换链中所有节点使用的都是同一个资源解析对象,即调用构造函数的的时候传入的资源解析链对象
private final ResourceResolverChain resolverChain;
// 当前资源转换链节点的资源转换器
@Nullable
private final ResourceTransformer transformer;
// 当前资源转换链节点的下一个资源转换链节点
@Nullable
private final ResourceTransformerChain nextChain;
// 用于通过 ResourceResolverChain resolverChain 和 List<? extends ResourceTransformer> resolvers 初始化 DefaultResourceTransformerChain节点组成的链表 的三个方法
// initTransformerChain 方法主要通过倒叙迭代构建 DefaultResourceTransformerChain节点组成的链表的方法很棒
......
@Override
public ResourceResolverChain getResolverChain() {
return this.resolverChain;
}
@Override
public Resource transform(HttpServletRequest request, Resource resource) throws IOException {
// 直接将转换工作委托给当前链表节点中的资源转换器,同时增加一个参数:下一个资源钻换链节点,方便资源转换器在转换完成之后调用下一个资源转换链节点中的资源转换器进行处理
return (this.transformer != null && this.nextChain != null ?
this.transformer.transform(request, resource, this.nextChain) : resource);
}
}
ResourceResolver
跟 ResourceResolverChain
接口方法基本上一模一样,只是两个方法都多了一个 ResourceResolverChain
参数,用于在当前节点的资源解析器解析失败的时候调用下一个 ResourceResolverChain
节点的资源解析器继续解析。
public interface ResourceResolver {
// 根据请求对象和URL中包含的资源路径和服务器上的可供查找资源路径列表(即指定查找范围)来查找资源
@Nullable
Resource resolveResource(@Nullable HttpServletRequest request, String requestPath, List<? extends Resource> locations, ResourceResolverChain chain);
// 将给定的内部资源路径解析成面向外部的公共URL路径,以便客户机用于访问位于给定内部资源路径上的资源。
// 这在渲染到客户端的URL链接时非常有用。
// 在 ResourceUrlProvider 中有所使用
@Nullable
String resolveUrlPath(String resourcePath, List<? extends Resource> locations, ResourceResolverChain chain);
}
ResourceResolver
的实现类,也不多,就这几个
-
CachingResourceResolver
:从缓存中解析静态资源的ResourceResolver
实现,如果没有找到就会委托给资源解析链中的下一个节点,并将最终解析结果保存在缓存中,方便下次使用。 -
VersionResourceResolver
:用于解析包含版本字符串的请求路径的资源解析器,该版本字符串可以用作 HTTP 缓存策略的一部分,默认的 HTTP 缓存策略是过期时间,在这个时间之后,缓存有效,可以使用保存在浏览器中的缓存,缓存过期之后,则需要访问服务器获取资源,现在,我们可以根据文件内容来使缓存过期,同时,访问资源的 URL 也会更新,因为 URL 中的版本信息也更新了。-
本身不解析资源,依赖后续的资源解析链节点中的资源解析器(一般都是
PathResourceResolver
)来解析资源 -
VersionResourceResolver
中可以使用各种不同的生成版本信息的策略VersionStrategy
,只有两种ContentVersionStrategy
(基于资源内容生成 MD5 哈希值作为版本字符串)或者FixedVersionStrategy
(固定版本字符串),一般是ContentVersionStrategy
,除非它不能被使用。一个常见的场景使ContentVersionStrategy
不能与JavaScript module loaders
一起使用。对于这种情况,FixedVersionStrategy
是更好的选择。VersionResourceResolver
将路径模板和与路径模板匹配的版本生成策略存在versionStrategyMap
字段中。 -
实践看后文。
-
简单看下源码解析
-
@Override protected Resource resolveResourceInternal(@Nullable HttpServletRequest request, String requestPath, List<? extends Resource> locations, ResourceResolverChain chain) { // 本身不解析资源,依赖后续的资源解析链节点中的资源解析器(一般都是`PathResourceResolver`)来解析资源 // 如果可以直接找到资源,说明请求路径没有带版本信息,直接返回即可 // 如果是带版本信息的,这里肯定找不到,那就需要后续处理 Resource resolved = chain.resolveResource(request, requestPath, locations); if (resolved != null) { return resolved; } // 1. 现根据当前路径(带版本信息)中的版本信息获取版本生成策略 VersionStrategy versionStrategy = getStrategyForPath(requestPath); if (versionStrategy == null) { return null; } // 2. 提取版本字符串 String candidateVersion = versionStrategy.extractVersion(requestPath); if (!StringUtils.hasLength(candidateVersion)) { return null; } // 3. 去除版本字符串获取原本的请求路径 String simplePath = versionStrategy.removeVersion(requestPath, candidateVersion); // 4. 用不带版本信息的请求路径解析资源 Resource baseResource = chain.resolveResource(request, simplePath, locations); // 这个时候找不到资源,说明请求对应的资源是真的不存在,直接返回null if (baseResource == null) { return null; } // 根据版本策略为找到的资源生成版本字符串 String actualVersion = versionStrategy.getResourceVersion(baseResource); // 服务器上的文件的版本,跟请求中包含的版本相同,才返回 // 否则要么就是服务器上的资源的版本更新了,但是请求的版本没有更新 if (candidateVersion.equals(actualVersion)) { return new FileNameVersionedResource(baseResource, candidateVersion); } else { if (logger.isTraceEnabled()) { logger.trace("Found resource for \"" + requestPath + "\", but version [" + candidateVersion + "] does not match"); } return null; } } @Override protected String resolveUrlPathInternal(String resourceUrlPath, List<? extends Resource> locations, ResourceResolverChain chain) { // 注意,根据接口ResourceResolver中resolveUrlPath方法的定义,此时 resourceUrlPath 是不带 版本信息的 // 委托给下一个资源解析器节点的资源解析器来解析此路径 String baseUrl = chain.resolveUrlPath(resourceUrlPath, locations); if (StringUtils.hasText(baseUrl)) { // 获取使用的版本生成策略 VersionStrategy versionStrategy = getStrategyForPath(resourceUrlPath); if (versionStrategy == null) { return baseUrl; } // 通过给下一个资源解析器节点的资源解析器来解析资源 Resource resource = chain.resolveResource(null, baseUrl, locations); Assert.state(resource != null, "Unresolvable resource"); // 根据版本生成策略生成版本字符串 String version = versionStrategy.getResourceVersion(resource); // 添加到URL中,返回 return versionStrategy.addVersion(baseUrl, version); } return baseUrl; }
-
-
GzipResourceResolver
:用于解析请求的消息头,如果Accept-Encoding
消息头包含gzip
的请求,跟VersionResourceResolver
一样,也是委托后续的资源解析链来定位资源。注意只有当Accept-Encoding
消息头包含值gzip
即表示客户端接受gzip
的响应时,GzipResourceResolver
才会生效,已弃用 - 改用EncodedResourceResolver
-
EncodedResourceResolver
,跟GzipResourceResolver
功能一样,只不过提供了更加可拓展的实现方式,GzipResourceResolver
只支持Accept-Encoding
消息头包含gzip
的请求,EncodedResourceResolver
将所有支持的格式放在一个列表中进行管理EncodedResourceResolver#contentCodings
,支持拓展,默认支持gzip
和br
,注意,EncodedResourceResolver
必须在使用ContentVersionStrategy
版本策略的VersionResourceResolver
的前面,以确保版本计算不受影响。 -
WebJarsResourceResolver
,首先需要引入第三方库org.webjars:webjars-locator-core
,以后再了解 -
PathResourceResolver
:一个简单的ResourceResolver
实现,它尝试在给定位置下查找与请求路径匹配的资源。跟前面的那些资源解析器不一样,该解析器不会将资源的查找委托给后面的资源解析链节点,实际上,前面的那些资源解析器将资源的查找委托给后面的资源解析链节点的时候,最终都是委托给了PathResourceResolver
,所以PathResourceResolver
一般都处在资源解析链的末尾。必须掌握。简单的源码分析:
resolveUrlPathInternal
的实现和resolveResourceInternal
的实现都委托给getResource
方法。@Override protected Resource resolveResourceInternal(@Nullable HttpServletRequest request, String requestPath, List<? extends Resource> locations, ResourceResolverChain chain) { // 委托给 getResource 方法 return getResource(requestPath, request, locations); } @Override protected String resolveUrlPathInternal(String resourcePath, List<? extends Resource> locations, ResourceResolverChain chain) { // 委托给 getResource 方法 return (StringUtils.hasText(resourcePath) && getResource(resourcePath, null, locations) != null ? resourcePath : null); } @Nullable private Resource getResource(String resourcePath, @Nullable HttpServletRequest request, List<? extends Resource> locations) { for (Resource location : locations) { try { String pathToUse = encodeIfNecessary(resourcePath, request, location); // 核心使委托给 getResource 方法, 这个方法的逻辑很简单: // 1. 检查 "location/pathToUse" 是否是可读的Resource。同时此方法还对 // 2. 判断查到的资源是否在 allowedLocations 下 Resource resource = getResource(pathToUse, location); if (resource != null) { return resource; } } catch (IOException ex) { if (logger.isDebugEnabled()) { String error = "Skip location [" + location + "] due to error"; if (logger.isTraceEnabled()) { logger.trace(error, ex); } else { logger.debug(error + ": " + ex.getMessage()); } } } } return null; }
ResourceTransformer
ResourceTransformer
是函数式接口,跟 ResourceTransformerChain
接口相比,少了一个 getResolverChain()
方法,此外 transform
方法多了一个 ResourceTransformerChain
参数,用于在当前节点的资源转换器转换完成之后调用下一个 ResourceTransformerChain
节点的资源转换器继续进行处理。
@FunctionalInterface
public interface ResourceTransformer {
// 传入请求对象和带转换的资源对象返回转换过后的资源对象
Resource transform(HttpServletRequest request, Resource resource, ResourceTransformerChain transformerChain) throws IOException;
}
ResourceTransformer
的实现类也不多,就这几个,ResourceTransformerSupport
很重要,帮助快速搭建 ResourceTransformer 实现类
-
CachingResourceTransformer
:缓存转换器,之前转化的结果会在这里缓存,下一次同样的一个资源需要转换,可以直接从缓存中提取 -
CssLinkResourceTransformer
:修改 CSS 文件中的链接,以生成应该提供给用户的 URL 路径 (例如,在 URL 中插入一个基于内容的 MD5 散列)。该实现在 CSS 的@import
语句中查找链接,也在 CSSurl()
函数中查找链接。然后,所有链接都传入ResourceResolverChain
,并相对于包含 CSS 文件的位置进行解析。如果解析成功,则修改该链路,否则保留原有链路。一般,配合
VersionResourceResolver
一起使用懒得分析源码了
用到了
ResourceUrlProvider
。 -
AppCacheManifestTransformer
:用到了再说。
实现 HttpRequestHandler 接口
HttpRequestHandler 接口
只有一个方法,处理请求,返回响应,简单粗暴
@FunctionalInterface
public interface HttpRequestHandler {
// 处理请求,返回响应
void handleRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException;
}
调用链
HttpRequestHandler
类型的 handler(处理器)的专用适配器是 HttpRequestHandlerAdapter
,HttpRequestHandlerAdapter
直接将对 HandlerAdapter
接口的实现委托给 HttpRequestHandler
public class HttpRequestHandlerAdapter implements HandlerAdapter {
@Override
public boolean supports(Object handler) {
// 仅支持 HttpRequestHandler 类型的 handler
return (handler instanceof HttpRequestHandler);
}
@Override
@Nullable
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
// 将 handler 强制转化为 HttpRequestHandler ,然后转手就是委托给 HttpRequestHandler#handleRequest
((HttpRequestHandler) handler).handleRequest(request, response);
return null;
}
@Override
public long getLastModified(HttpServletRequest request, Object handler) {
if (handler instanceof LastModified) {
return ((LastModified) handler).getLastModified(request);
}
return -1L;
}
}
在 DispatcherServlet#doDispatch
中,HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
通过此适配器 HttpRequestHandlerAdapter
调用 HttpRequestHandler
接口的实现类也就是 ResourceHttpRequestHandler
的 handleRequest
方法。
ResourceHttpRequestHandler 实现
实际执行流程也很简单:ResourceHttpRequestHandler#handleRequest
一开始就调用 ResourceHttpRequestHandler#getResource
,ResourceHttpRequestHandler#getResource
中调用资源解析链 resolverChain
和资源转换链 transformerChain
来查找资源,解析资源。然后开始进行 HTTP 缓存验证,主要是 Last-Modified
,最后如果需要返回新的资源,设置响应的消息头。
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
// 如果使用了 VersionResourceResolver,在这里生效
// 因此如果版本信息更新了,但是客户端访问老版本的资源,那么会找不到,即这里就会404
Resource resource = getResource(request);
if (resource == null) {
logger.debug("Resource not found");
// HttpServletResponse.SC_NOT_FOUND 就是我们常见的404
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
// 处理 HTTP请求方法为 HttpMethod.OPTIONS 的请求
if (HttpMethod.OPTIONS.matches(request.getMethod())) {
response.setHeader("Allow", getAllowHeader());
return;
}
// Supported methods and required session
// 检查HTTP请求方法是否是指定的方法,已经当前请求是否处于服务器已经存在的session中
// 不满足条件的话就报错
// 跟WebContentGenerator有关,不深入了解
checkRequest(request);
// Header phase
// 如果使用 Last-Modified 消息头,则判断资源时间
if (isUseLastModified() && new ServletWebRequest(request, response).checkNotModified(resource.lastModified())) {
logger.trace("Resource not modified");
return;
}
// Apply cache settings, if any
// 做完了所有的检查,准备返回新静态资源
// 先进行跟缓存相关的消息头的设置,跟WebContentGenerator有关,不深入了解
prepareResponse(response);
// Check the media type for the resource
MediaType mediaType = getMediaType(request, resource);
setHeaders(response, resource, mediaType);
// Content phase
ServletServerHttpResponse outputMessage = new ServletServerHttpResponse(response);
if (request.getHeader(HttpHeaders.RANGE) == null) {
Assert.state(this.resourceHttpMessageConverter != null, "Not initialized");
this.resourceHttpMessageConverter.write(resource, mediaType, outputMessage);
}
else {
Assert.state(this.resourceRegionHttpMessageConverter != null, "Not initialized");
ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(request);
try {
List<HttpRange> httpRanges = inputMessage.getHeaders().getRange();
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
this.resourceRegionHttpMessageConverter.write(
HttpRange.toResourceRegions(httpRanges, resource), mediaType, outputMessage);
}
catch (IllegalArgumentException ex) {
response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes */" + resource.contentLength());
response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
}
}
}
再仔细看看 ResourceHttpRequestHandler#getResource
@Nullable
protected Resource getResource(HttpServletRequest request) throws IOException {
// 从请求域的属性找到路径
// AbstractHandlerMapping的initLookupPath方法中初始化的,这个我们在研究
// 1. UrlPathHelper+PathMatcher简单解析
// 2. PathContainer+PathPatternParser+PatternParser系列接口简单解析
// 的时候都研究过
// 也可能是由 PathExposingHandlerInterceptor 暴露的
String path = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
if (path == null) {
throw new IllegalStateException("Required request attribute '" +
HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE + "' is not set");
}
// 清除路径中的不规则元素,剔除非法的URL
path = processPath(path);
if (!StringUtils.hasText(path) || isInvalidPath(path)) {
return null;
}
if (isInvalidEncodedPath(path)) {
return null;
}
Assert.notNull(this.resolverChain, "ResourceResolverChain not initialized.");
Assert.notNull(this.transformerChain, "ResourceTransformerChain not initialized.");
// 先按照资源解析链解析,解析完了,获取了最终的资源对象
// 再开始资源转换链的转化
// 会用到前面 afterPropertiesSet 方法中 resolveResourceLocations 从路径列表 locationValues 中转化而来的 Resource 列表
Resource resource = this.resolverChain.resolveResource(request, path, getLocations());
if (resource != null) {
// 开始资源转换链的转化
resource = this.transformerChain.transform(request, resource);
}
return resource;
}
实践
跟 Web 配置相关的源码细节,请看
《《SpringMVC》- 第十篇:基于注解配置 SpringMVC》中的
addResourceHandlers
小节
常规使用
新建 Web 资源
在 src/main/webapp
下新建 resourcesSimple
文件夹,在 src/main/resources
下新建 staticSimple
文件夹,下面放一下静态资源
Web 配置
addResourceHandler
方法也可以添加多个路径,只不过我们一般只添加一个。此返回 ResourceHandlerRegistration
addResourceLocations
添加资源位置,可以添加 war 包的根目录中的位置(默认)、编译输出路径(classpath:
开头)、系统文件路径(file:
开头),如果有多个路径匹配,排在前面的先匹配,这是 PathResourceResolver
的 getResource
方法决定的。 注意,路径要以 /
结尾,不然报错。此返回 ResourceHandlerRegistration
setCacheControl
设置响应的的消息头的 Cache-Control
,用于设置第一次获取此资源的时候的响应的消息头的 Cache-Control
,下文中的配置对应的消息头为 Cache-Control:max-age=20
单位是秒,表示此静态资源在浏览器缓存中的保留时间是 20s,20s 内浏览器访问此资源(新开一个 tab 页,将 URL 复制过来然后回车,不按 F5 或者 Ctrl+F5),不会请求服务器,20s 后,浏览器缓存过期,才会请求服务器获取新的资源。此返回 ResourceHandlerRegistration
一般我们设置这三个方法,就够用了。
当然你还可以调用 resourceChain
,启用或者不启用资源解析缓存或者资源转换缓存。一般生产环境建议开启,调试环境不建议开启
@Configuration
//扫描组件
@ComponentScan("xyz.xiashuo.springmvcannotationconfig")
//开启MVC注解驱动
@EnableWebMvc
@PropertySource("classpath:application.properties")
@Order(1)
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 常规的静态资源处理器映射
// 效果等同于 xml中的配置
//<mvc:resources mapping="/resources/**" location="/resources, classpath:/static/" cache-period="20" />
// 添加静态资源映射路径
// addResourceHandler 方法可以添加多个路径,只不过我们一般只添加一个。
registry.addResourceHandler("/resourcesSimple/**")
// addResourceLocations 添加资源位置,可以添加war包的根目录中的位置(默认)、编译输出路径(classpath:开头)、系统文件路径(file:开头)
// 如果有多个路径匹配,排在前面的先匹配,这是 PathResourceResolver 的 getResource 方法决定的
// 注意,路径要以 / 结尾,不然报错
.addResourceLocations("/resourcesSimple/", "classpath:/staticSimple/", "file:/C:\\test/")
// setCacheControl 设置响应的的消息头的Cache-Control
.setCacheControl(CacheControl.maxAge(20, TimeUnit.SECONDS));
WebMvcConfigurer.super.addResourceHandlers(registry);
}
}
访问链接 http://localhost:8080/SpringMVC_AnnotationConfig/resourcesSimple/images/aaa.jpg
这个资源在 src/main/webapp/resourcesSimple
和 src/main/resources/staticSimple
下都有,但是 src/main/webapp/resourcesSimple
排在前面,所以返回 src/main/webapp/resourcesSimple
中的资源,
第一次在 15:58:30
请求之后,因为我们配置了缓存时间为 20s,所以虽然在 15:58:30
到 15:58:50
之间我请求了多次(新开一个 tab 页,将 URL 复制过来然后回车,不按 F5 或者 Ctrl+F5),但是使用的都是浏览器缓存,服务器并没有被请求,浏览器缓存失效后,访问服务器,同时由于资源没有改变,因此最终服务器返回的也是 304,表示资源并没有改变,继续用之前的即可,然后浏览器再次缓存 20s,接下来的 20s 如果再请求,仍然会使用浏览器资源,直到缓存超时,再次访问服务器,如此循环。
15:58:30.114 [http-nio-8080-exec-4] DEBUG org.springframework.web.servlet.DispatcherServlet - GET "/SpringMVC_AnnotationConfig/resourcesSimple/images/aaa.jpg", parameters={}
15:58:30.115 [http-nio-8080-exec-4] DEBUG org.springframework.web.servlet.handler.SimpleUrlHandlerMapping - Mapped to ResourceHttpRequestHandler ["/resourcesSimple/", "classpath:/staticSimple/", "file:/C:\test/"]
15:58:30.117 [http-nio-8080-exec-4] DEBUG org.springframework.web.servlet.DispatcherServlet - Completed 200 OK
15:58:50.378 [http-nio-8080-exec-8] DEBUG org.springframework.web.servlet.DispatcherServlet - GET "/SpringMVC_AnnotationConfig/resourcesSimple/images/aaa.jpg", parameters={}
15:58:50.379 [http-nio-8080-exec-8] DEBUG org.springframework.web.servlet.handler.SimpleUrlHandlerMapping - Mapped to ResourceHttpRequestHandler ["/resourcesSimple/", "classpath:/staticSimple/", "file:/C:\test/"]
15:58:50.381 [http-nio-8080-exec-8] DEBUG org.springframework.web.servlet.DispatcherServlet - Completed 304 NOT_MODIFIED
如果文件有所更改,就会返回新文件
19:19:53.684 [http-nio-8080-exec-1] DEBUG org.springframework.web.servlet.DispatcherServlet - GET "/SpringMVC_AnnotationConfig/resourcesSimple/images/aaa.jpg", parameters={}
19:19:53.684 [http-nio-8080-exec-1] DEBUG org.springframework.web.servlet.handler.SimpleUrlHandlerMapping - Mapped to ResourceHttpRequestHandler ["/resourcesSimple/", "classpath:/staticSimple/", "file:/C:\test/"]
19:19:53.684 [http-nio-8080-exec-1] DEBUG org.springframework.web.servlet.DispatcherServlet - Completed 304 NOT_MODIFIED
19:20:56.361 [http-nio-8080-exec-6] DEBUG org.springframework.web.servlet.DispatcherServlet - GET "/SpringMVC_AnnotationConfig/resourcesSimple/images/aaa.jpg", parameters={}
19:20:56.362 [http-nio-8080-exec-6] DEBUG org.springframework.web.servlet.handler.SimpleUrlHandlerMapping - Mapped to ResourceHttpRequestHandler ["/resourcesSimple/", "classpath:/staticSimple/", "file:/C:\test/"]
19:20:56.363 [http-nio-8080-exec-6] DEBUG org.springframework.web.servlet.DispatcherServlet - Completed 200 OK
新开一个 tab 页,将 URL 复制过来然后回车和直接按 F5 或者 Ctrl+F5 有什么区别?
背景知识:HTTP 缓存
Cache-Control: 在 HTTP 消息头中,用于指示代理和 UA 使用何种缓存策略, 表示了资源是否可以被缓存,以及缓存的有效期。
no-cache 为本次响应不可直接用于后续请求(在没有向服务器进行校验的情况下)
no-store 为禁止缓存(不得存储到非易失性介质,如果有的话尽量移除,用于敏感信息)
private 为仅 UA 可缓存
public 为大家都可以缓存。
当 Cache-Control 为可缓存时,同时可指定缓存时间(比如 public, max-age:86400)。 这意味着在 1 天(60x60x24=86400)时间内,浏览器都可以直接使用该缓存(此时服务器收不到任何请求)。
Last-Modified :表示资源的最后修改时间,如果请求资源的请求的响应的消息头中带有 Last-Modified ,下一次请求此资源的时候就会带上 If-modified-since 的消息头 。与服务器上的资源的最新的修改时间进行比较,如果请求消息头中的时间在资源最新的需改时间之前,则表示浏览器缓存过期,服务器返回最新的资源,如果请求消息头中的时间和资源最新的需改时间相同或者在其之后,则表示浏览器缓存还是最新的,不需要更新,服务器会直接返回 304,意思是没事儿,让浏览器继续用之前的缓存。
这部分代码,在
ResourceHttpRequestHandler#handleRequest
方法中写的清清楚楚Cache-Control 和 Last-Modified 一起生效,当对静态资源发起请求的时候,浏览器首先会检查 Cache-Control,看浏览器本地是否有缓存,同时缓存有没有过期,如果缓存没有过期,那么直接使用缓存,这个时候不会发起对服务器的请求,如果 Cache-Control 已经过期,则直接发起对服务器的请求,同时带上 If-modified-since 的消息头 。与服务器上的资源的最新的修改时间进行比较,如果请求消息头中的时间在资源最新的需改时间之前,则表示浏览器缓存过期,服务器返回最新的资源,如果请求消息头中的时间和资源最新的需改时间相同或者在其之后,则表示浏览器缓存还是最新的,不需要更新,服务器会直接返回 304,意思是没事儿,让浏览器继续用之前的缓存。
这里需要注意
当使用 F5 刷新请求资源的 URL 的时候,请求头会带上
Cache-Control: max-age=0
表示浏览器缓存强制过期(强制 Cache-Control 失效),直接发起对服务器的请求,此时 Last-Modified 还是有效的,如果请求消息头中的 If-modified-since 时间和资源最新的需改时间相同或者在其之后,依然会返回 304
当使用 Ctrl+F5 刷新请求资源的 URL 的时候,请求头会带上
Cache-Control: no-cache
表示不使用缓存,此时 Cache-Control 和 Last-Modified 都失效,服务器直接返回服务器中的资源内容。
勾选 Network 下的 Disable cache 也是一样的效果:
其他跟浏览器缓存相关的 HTTP 消息头
Etag : 消息头中代表资源的唯一标识标签,在服务器端生成。如果响应的消息头中带有 Etag ,下一次请求同一个资源的时候在消息头带 Etag ,如果 Etag 没有变化,将收到 304 的响应,从缓存中读取。
Etag 在使用时要注意相同资源多台 Web 服务器的 Etag 的一致性。
Expire 是消息头中代表资源的过期时间,由服务器段设置。如果响应的消息头带有 Expire ,则在 Expire 过期前不会发生 Http 请求,直接从缓存中读取。用户强制 F5 或者 Ctrl+F5 例外。
Last-Modified,Etag,Expires 三个同时使用时。先判断 Expire ,然后发送 Http 请求,服务器先判断 last-modified ,再判断 Etag ,必须都没有过期,才能返回 304 响应。
注意,此时的 ResourceHttpRequestHandler#resolverChain
,只有一个 PathResourceResolver
,
ResourceHttpRequestHandler#transformerChain
,为空
带版本信息的资源请求
在 src/main/resources
下新建 staticVersion
文件夹,下面放一下静态资源
在代替 Web.xml
的 Java 配置中添加过滤器 ResourceUrlEncodingFilter
,用于自动更新页面上对静态资源的访问。
@Override
protected Filter[] getServletFilters() {
CharacterEncodingFilter encodingFilter = new CharacterEncodingFilter();
encodingFilter.setEncoding("UTF-8");
encodingFilter.setForceRequestEncoding(true);
encodingFilter.setForceResponseEncoding(true);
HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();
// 用于自动更新资源URL后面的版本信息
ResourceUrlEncodingFilter resourceUrlEncodingFilter = new ResourceUrlEncodingFilter();
return new Filter[]{encodingFilter, hiddenHttpMethodFilter,resourceUrlEncodingFilter};
}
Web 配置
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 常规的静态资源处理器映射
// 效果等同于 xml中的配置
//<mvc:resources mapping="/resources/**" location="/resources, classpath:/static/" cache-period="20" />
// 添加静态资源映射路径
// addResourceHandler 方法可以添加多个路径,只不过我们一般只添加一个。
registry.addResourceHandler("/resourcesSimple/**")
// addResourceLocations 添加资源位置,可以添加war包的根目录中的位置(默认)、编译输出路径(classpath:开头)、系统文件路径(file:开头)
// 如果有多个路径匹配,排在前面的先匹配,这是 PathResourceResolver 的 getResource 方法决定的
//注意,路径要以 / 结尾,不然报错
.addResourceLocations("/resourcesSimple/", "classpath:/staticSimple/", "file:/C:\\test/")
// setCacheControl 设置响应的的消息头的Cache-Control
.setCacheControl(CacheControl.maxAge(20, TimeUnit.SECONDS));
// 为资源添加版本信息
registry.addResourceHandler("/resourcesVersion/**")
.addResourceLocations("classpath:/staticVersion/")
.setCacheControl(CacheControl.maxAge(20, TimeUnit.SECONDS))
// resourceChain 唯一的参数 表示是否添加缓存,将静态资源解析的结果缓存起来,生产环境建议设置为true,开发环境设置为false
.resourceChain(false)
// 添加版本信息解析器
// 为所有路径指定基于内容的版本字符串生成器
.addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"))
// 添加 Css 转换器,专门用于处理,在开启静态资源的URL版本的时候,css文件中通过 @import "another.css" 的情况,可以将 @import 的css的路径也假设版本信息
// 也可以不添加,系统会默认添加。
.addTransformer(new CssLinkResourceTransformer());
WebMvcConfigurer.super.addResourceHandlers(registry);
}
启动项目之后,刷新页面,你会发现,页面上对静态资源的访问链接,都加上了版本信息后缀,例如 /SpringMVC_AnnotationConfig/resourcesVersion/js/echarts.js
变成了 /SpringMVC_AnnotationConfig/resourcesVersion/js/echarts-205ce38d25df47b43cc2e1f4fa4ef354.js
。
第一次获取静态资源之后,20s 内新开一个 tab 页,将 URL 复制过来然后回车,仍然也是不会访问服务器的,缓存失效之后,再次访问,仍然会返回 304,然后浏览器再次缓存 20s,接下来的 20s 如果再请求,仍然会使用浏览器资源,直到缓存超时,再次访问服务器,如此循环。
19:21:42.789 [http-nio-8080-exec-4] DEBUG org.springframework.web.servlet.DispatcherServlet - GET "/SpringMVC_AnnotationConfig/resourcesVersion/js/echarts-205ce38d25df47b43cc2e1f4fa4ef354.js", parameters={}
19:21:42.789 [http-nio-8080-exec-4] DEBUG org.springframework.web.servlet.handler.SimpleUrlHandlerMapping - Mapped to ResourceHttpRequestHandler ["classpath:/staticVersion/"]
19:21:42.798 [http-nio-8080-exec-4] DEBUG org.springframework.web.servlet.DispatcherServlet - Completed 304 NOT_MODIFIED
19:22:02.793 [http-nio-8080-exec-1] DEBUG org.springframework.web.servlet.DispatcherServlet - GET "/SpringMVC_AnnotationConfig/resourcesVersion/js/echarts-205ce38d25df47b43cc2e1f4fa4ef354.js", parameters={}
19:22:02.794 [http-nio-8080-exec-1] DEBUG org.springframework.web.servlet.handler.SimpleUrlHandlerMapping - Mapped to ResourceHttpRequestHandler ["classpath:/staticVersion/"]
19:22:02.806 [http-nio-8080-exec-1] DEBUG org.springframework.web.servlet.DispatcherServlet - Completed 304 NOT_MODIFIED
但是跟常规使用不一样的是,如果在访问服务器的时候,服务器的静态资源已经更改,服务器不会返回新的资源,而是直接报错,需要刷新请求资源的 URL 所在的页面获取新的 URL,即获取新的版本号,才能正常访问。
修改资源,不刷新 URL 所在的页面,访问服务器
19:22:39.147 [http-nio-8080-exec-6] DEBUG org.springframework.web.servlet.DispatcherServlet - GET "/SpringMVC_AnnotationConfig/resourcesVersion/js/echarts-205ce38d25df47b43cc2e1f4fa4ef354.js", parameters={}
19:22:39.147 [http-nio-8080-exec-6] DEBUG org.springframework.web.servlet.handler.SimpleUrlHandlerMapping - Mapped to ResourceHttpRequestHandler ["classpath:/staticVersion/"]
19:22:39.161 [http-nio-8080-exec-6] DEBUG org.springframework.web.servlet.DispatcherServlet - Completed 304 NOT_MODIFIED
19:23:05.204 [http-nio-8080-exec-8] DEBUG org.springframework.web.servlet.DispatcherServlet - GET "/SpringMVC_AnnotationConfig/resourcesVersion/js/echarts-205ce38d25df47b43cc2e1f4fa4ef354.js", parameters={}
19:23:05.205 [http-nio-8080-exec-8] DEBUG org.springframework.web.servlet.handler.SimpleUrlHandlerMapping - Mapped to ResourceHttpRequestHandler ["classpath:/staticVersion/"]
19:23:05.224 [http-nio-8080-exec-8] DEBUG org.springframework.web.servlet.resource.ResourceHttpRequestHandler - Resource not found
19:23:05.225 [http-nio-8080-exec-8] DEBUG org.springframework.web.servlet.DispatcherServlet - Completed 404 NOT_FOUND
刷新 URL 所在的页面,并访问服务器
19:24:34.812 [http-nio-8080-exec-10] DEBUG org.springframework.web.servlet.DispatcherServlet - GET "/SpringMVC_AnnotationConfig/", parameters={}
19:24:34.812 [http-nio-8080-exec-10] DEBUG org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping - Mapped to xyz.xiashuo.springmvcannotationconfig.controller.HelloController#index()
19:24:34.850 [http-nio-8080-exec-10] DEBUG org.springframework.web.servlet.DispatcherServlet - Completed 200 OK
19:24:36.103 [http-nio-8080-exec-1] DEBUG org.springframework.web.servlet.DispatcherServlet - GET "/SpringMVC_AnnotationConfig/resourcesVersion/js/echarts-b1706f4f6701fb8f28dd37a8f8330095.js", parameters={}
19:24:36.104 [http-nio-8080-exec-1] DEBUG org.springframework.web.servlet.handler.SimpleUrlHandlerMapping - Mapped to ResourceHttpRequestHandler ["classpath:/staticVersion/"]
19:24:36.140 [http-nio-8080-exec-1] DEBUG org.springframework.web.servlet.DispatcherServlet - Completed 200 OK
对比一下可以发现,刷新访问静态资源的 URL 所在的页面之后,URL 中包含的静态资源的版本号也更新了。这个时候才能正常访问资源。
这个是由于
ResourceHttpRequestHandler#handleRequest
决定的,这个方法一上来就解析请求的 URL,版本号变了,带有原来版本号的 URL 是查不到资源的,所以直接报错.
直接在新 tab 栏输入不带版本信息的链接,始终返回最新的版本的内容,这在
VersionResourceResolver
的resolveResourceInternal
的源码分析的一开始就说明了这一点
所以判断流程是,浏览器本地缓存是否过期 -> 文件版本是否更新 -> 资源更新时间(LastModified)
此时的 ResourceHttpRequestHandler#resolverChain
,有两个资源解析器,VersionResourceResolver
和 PathResourceResolver
ResourceHttpRequestHandler#transformerChain
,只有一个资源转换器 CssLinkResourceTransformer

简单应用 - 静态资源的大版本区分
在配置文件 application.properties
中引入版本变量
resource.version = 4
Web 配置
@Configuration
//扫描组件
@ComponentScan("xyz.xiashuo.springmvcannotationconfig")
//开启MVC注解驱动
@EnableWebMvc
@PropertySource("classpath:application.properties")
@Order(1)
public class WebConfig implements WebMvcConfigurer {
@Value("${resource.version}")
private int resourceVersion;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 常规的静态资源处理器映射
// 效果等同于 xml中的配置
//<mvc:resources mapping="/resources/**" location="/resources, classpath:/static/" cache-period="20" />
// 添加静态资源映射路径
// addResourceHandler 方法可以添加多个路径,只不过我们一般只添加一个。
registry.addResourceHandler("/resourcesSimple/**")
// addResourceLocations 添加资源位置,可以添加war包的根目录中的位置(默认)、编译输出路径(classpath:开头)、系统文件路径(file:开头)
// 如果有多个路径匹配,排在前面的先匹配,这是 PathResourceResolver 的 getResource 方法决定的
//注意,路径要以 / 结尾,不然报错
.addResourceLocations("/resourcesSimple/", "classpath:/staticSimple/", "file:/C:\\test/")
// setCacheControl 设置响应的的消息头的Cache-Control
.setCacheControl(CacheControl.maxAge(20, TimeUnit.SECONDS));
// 为资源添加版本信息
registry.addResourceHandler("/resourcesVersion/**")
.addResourceLocations("classpath:/staticVersion/")
.setCacheControl(CacheControl.maxAge(20, TimeUnit.SECONDS))
// resourceChain 唯一的参数 表示是否添加缓存,将静态资源解析的结果缓存起来,生产环境建议设置为true,开发环境设置为false
.resourceChain(false)
// 添加版本信息解析器
// 为所有路径指定基于内容的版本字符串生成器
.addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"))
// 添加 Css 转换器,专门用于处理,在开启静态资源的URL版本的时候,css文件中通过 @import "another.css" 的情况,可以将 @import 的css的路径也假设版本信息
// 也可以不添加,系统会默认添加。
.addTransformer(new CssLinkResourceTransformer());
// 添加大版本切换
registry.addResourceHandler("/resources/**")
// resourceVersion 为静态资源大版本
.addResourceLocations("classpath:/resourcesAllVersion/V" + resourceVersion + "/")
.setCacheControl(CacheControl.maxAge(20, TimeUnit.SECONDS));
WebMvcConfigurer.super.addResourceHandlers(registry);
}
}
在 src/main/resources
下新建 staticVersion
文件夹,下面放一下静态资源
添加 html 页面
<h1>大版本静态资源切换</h1>
<a target="_blank" th:href="@{/resources/images/aaa.jpg}">查看图片</a>
启动项目,可以看到,访问的是 V4 文件夹下的图片,修改 application.properties
中的版本为 5,就可以访问 V4 文件夹下的图片
总结
静态资源处理链是一个典型的链式处理设计模式。
静态资源处理是一个很大的话题,但是有了链式处理结构(ResourceHttpRequestHandler+ 解析链 + 转换链)之后,灵活度就很高了。
之前学习的内容协商等策略的处理,也都可以看做是一个简单的链式处理设计模式,
用一个 lis 存储所有的处理节点,处理的时候,用一个 for 循环依次遍历所有节点进行处理,其中,如果一个节点返回无效值,比如 null,或者空集合,则表示此节点处理失败,继续遍历,如果节点返回的是非无效值,则直接返回,链式处理的结果跟处理节点的顺序高度相关。