此部分关于描述springmvc处理静态资源的配置,关于资源参考springio
default-servlet-handler
用来处理defaultServlet
的访问
xml
xml解析代码分析
DefaultServletHandlerBeanDefinitionParser 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 class DefaultServletHandlerBeanDefinitionParser implements BeanDefinitionParser { @Override @Nullable public BeanDefinition parse (Element element, ParserContext parserContext) { Object source = parserContext.extractSource(element); String defaultServletName = element.getAttribute("default-servlet-name" ); RootBeanDefinition defaultServletHandlerDef = new RootBeanDefinition(DefaultServletHttpRequestHandler.class ) ; defaultServletHandlerDef.setSource(source); defaultServletHandlerDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); if (StringUtils.hasText(defaultServletName)) { defaultServletHandlerDef.getPropertyValues().add("defaultServletName" , defaultServletName); } String defaultServletHandlerName = parserContext.getReaderContext().generateBeanName(defaultServletHandlerDef); parserContext.getRegistry().registerBeanDefinition(defaultServletHandlerName, defaultServletHandlerDef); parserContext.registerComponent(new BeanComponentDefinition(defaultServletHandlerDef, defaultServletHandlerName)); Map<String, String> urlMap = new ManagedMap<>(); urlMap.put("/**" , defaultServletHandlerName); RootBeanDefinition handlerMappingDef = new RootBeanDefinition(SimpleUrlHandlerMapping.class ) ; handlerMappingDef.setSource(source); handlerMappingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); handlerMappingDef.getPropertyValues().add("urlMap" , urlMap); String handlerMappingBeanName = parserContext.getReaderContext().generateBeanName(handlerMappingDef); parserContext.getRegistry().registerBeanDefinition(handlerMappingBeanName, handlerMappingDef); parserContext.registerComponent(new BeanComponentDefinition(handlerMappingDef, handlerMappingBeanName)); MvcNamespaceUtils.registerDefaultComponents(parserContext, source); return null ; } }
注解
实现源码
根据default-servlet-handler
创建一个SimpleUrlHandlerMapping
并包含DefaultServletHttpRequestHandler
DefaultServletHttpRequestHandler:是HttpRequestHandler
的实现之一,访问默认servlet,根据不同的web环境,defualtServlet也不同
DefaultServletHttpRequestHandler 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 public class DefaultServletHttpRequestHandler implements HttpRequestHandler , ServletContextAware { public void setServletContext (ServletContext servletContext) { this .servletContext = servletContext; if (!StringUtils.hasText(this .defaultServletName)) { if (this .servletContext.getNamedDispatcher(COMMON_DEFAULT_SERVLET_NAME) != null ) { this .defaultServletName = COMMON_DEFAULT_SERVLET_NAME; } else if (this .servletContext.getNamedDispatcher(GAE_DEFAULT_SERVLET_NAME) != null ) { this .defaultServletName = GAE_DEFAULT_SERVLET_NAME; } else if (this .servletContext.getNamedDispatcher(RESIN_DEFAULT_SERVLET_NAME) != null ) { this .defaultServletName = RESIN_DEFAULT_SERVLET_NAME; } else if (this .servletContext.getNamedDispatcher(WEBLOGIC_DEFAULT_SERVLET_NAME) != null ) { this .defaultServletName = WEBLOGIC_DEFAULT_SERVLET_NAME; } else if (this .servletContext.getNamedDispatcher(WEBSPHERE_DEFAULT_SERVLET_NAME) != null ) { this .defaultServletName = WEBSPHERE_DEFAULT_SERVLET_NAME; } else { throw new IllegalStateException("Unable to locate the default servlet for serving static content. " + "Please set the 'defaultServletName' property explicitly." ); } } } @Override public void handleRequest (HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Assert.state(this .servletContext != null , "No ServletContext set" ); RequestDispatcher rd = this .servletContext.getNamedDispatcher(this .defaultServletName); if (rd == null ) { throw new IllegalStateException("A RequestDispatcher could not be located for the default servlet '" + this .defaultServletName + "'" ); } rd.forward(request, response); } }
resources
xml
xml配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns ="http://www.springframework.org/schema/beans" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc ="http://www.springframework.org/schema/mvc" xsi:schemaLocation ="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd" > <mvc:resources mapping ="/**" location ="/**" > <mvc:resource-chain resource-cache ="true" > <mvc:resolvers > <mvc:version-resolver > <mvc:content-version-strategy patterns ="/**" /> <mvc:fixed-version-strategy version ="1.0" patterns ="/fiexedVersion/**" /> </mvc:version-resolver > </mvc:resolvers > </mvc:resource-chain > </mvc:resources > </beans >
总结
mvc:cache-control
:mapping表示url映射,location表示本地资源路径
cache-control
:控制CacheControl
属性,该类的使用参考mvc缓存处理
resource-chain
:设置内部的资源处理链
version-resolver
:用来设置VersionResourceResolver
的配置
resource-cache
:开启CachingResourceResolver
xml解析源码分析
java配置
ResourceHandlerRegistry
:包含ResourceHandlerRegistration
和ResourceChainRegistration
ResourceChainRegistration
:设置ResourceHttpRequestHandler
的资源处理链
ResourceChainRegistration
:对应资源处理器
1 2 3 4 5 6 7 8 9 10 11 public void addResourceHandlers (ResourceHandlerRegistry registry) { ResourceHandlerRegistration resourceHandlerRegistration = registry.addResourceHandler("/**" ); resourceHandlerRegistration.addResourceLocations( "/" ); ResourceChainRegistration resourceChainRegistration= resourceHandlerRegistration.resourceChain(true ); VersionResourceResolver versionResourceResolver = new VersionResourceResolver(); versionResourceResolver.addFixedVersionStrategy("1.0" , "/fixedVersion/**" ); versionResourceResolver.addContentVersionStrategy("/md5Version/**" ); resourceChainRegistration.addResolver(versionResourceResolver); }
这种代码可以写成连续的语法registry.addResourceLocations( "/").resourceChain(true).addFixedVersionStrategy("1.0"."fiexedVersion")
实现原理
ResourceHttpRequestHandler
ResourceHttpRequestHandler 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 private static final Log logger = LogFactory.getLog(ResourceHttpRequestHandler.class ) ; private static final String URL_RESOURCE_CHARSET_PREFIX = "[charset=" ; private final List<String> locationValues = new ArrayList<>(4 ); private final List<Resource> locations = new ArrayList<>(4 ); private final Map<Resource, Charset> locationCharsets = new HashMap<>(4 ); private final List<ResourceResolver> resourceResolvers = new ArrayList<>(4 ); private final List<ResourceTransformer> resourceTransformers = new ArrayList<>(4 ); @Nullable private ResourceResolverChain resolverChain; @Nullable private ResourceTransformerChain transformerChain; @Nullable private ResourceHttpMessageConverter resourceHttpMessageConverter; @Nullable private ResourceRegionHttpMessageConverter resourceRegionHttpMessageConverter; @Nullable private ContentNegotiationManager contentNegotiationManager; @Nullable private PathExtensionContentNegotiationStrategy contentNegotiationStrategy; @Nullable private CorsConfiguration corsConfiguration; @Nullable private UrlPathHelper urlPathHelper; @Nullable private StringValueResolver embeddedValueResolver; public ResourceHttpRequestHandler () { super (HttpMethod.GET.name(), HttpMethod.HEAD.name()); } /==============================初始化========================================== public void afterPropertiesSet () throws Exception { 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." ); } if (this .resourceResolvers.isEmpty()) { this .resourceResolvers.add(new PathResourceResolver()); } initAllowedLocations(); 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(); } this .contentNegotiationStrategy = initContentNegotiationStrategy(); } private void resolveResourceLocations () { 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(); for (String location : this .locationValues) { 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(); 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 ); } 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); } } } protected void initAllowedLocations () { if (CollectionUtils.isEmpty(this .locations)) { return ; } for (int i = getResourceResolvers().size() - 1 ; i >= 0 ; i--) { if (getResourceResolvers().get(i) instanceof PathResourceResolver) { PathResourceResolver pathResolver = (PathResourceResolver) getResourceResolvers().get(i); if (ObjectUtils.isEmpty(pathResolver.getAllowedLocations())) { pathResolver.setAllowedLocations(getLocations().toArray(new Resource[0 ])); } if (this .urlPathHelper != null ) { pathResolver.setLocationCharsets(this .locationCharsets); pathResolver.setUrlPathHelper(this .urlPathHelper); } break ; } } } public void handleRequest (HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Resource resource = getResource(request); if (resource == null ) { logger.debug("Resource not found" ); response.sendError(HttpServletResponse.SC_NOT_FOUND); return ; } if (HttpMethod.OPTIONS.matches(request.getMethod())) { response.setHeader("Allow" , getAllowHeader()); return ; } checkRequest(request); if (new ServletWebRequest(request, response).checkNotModified(resource.lastModified())) { logger.trace("Resource not modified" ); return ; } prepareResponse(response); MediaType mediaType = getMediaType(request, resource); if (METHOD_HEAD.equals(request.getMethod())) { setHeaders(response, resource, mediaType); return ; } ServletServerHttpResponse outputMessage = new ServletServerHttpResponse(response); if (request.getHeader(HttpHeaders.RANGE) == null ) { Assert.state(this .resourceHttpMessageConverter != null , "Not initialized" ); setHeaders(response, resource, mediaType); this .resourceHttpMessageConverter.write(resource, mediaType, outputMessage); } else { Assert.state(this .resourceRegionHttpMessageConverter != null , "Not initialized" ); response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes" ); 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("Content-Range" , "bytes */" + resource.contentLength()); response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); } } }
总结
通过解析器来处理资源
通过转换器写出资源
处理了缓存头,options,head方法,关于处理缓存头部分参考mvc缓存处理
locations变量用来表示本地资源路径
资源处理责任链
实际内部采取责任链模式实际获取资源Resource
责任链实现
DefaultResourceResolverChain
是典型的责任链模型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 class DefaultResourceResolverChain implements ResourceResolverChain { @Nullable private final ResourceResolver resolver; @Nullable private final ResourceResolverChain nextChain; public DefaultResourceResolverChain (@Nullable List<? extends ResourceResolver> resolvers) { resolvers = (resolvers != null ? resolvers : Collections.emptyList()); DefaultResourceResolverChain chain = initChain(new ArrayList<>(resolvers)); this .resolver = chain.resolver; this .nextChain = chain.nextChain; } private static DefaultResourceResolverChain initChain (ArrayList<? extends ResourceResolver> resolvers) { DefaultResourceResolverChain chain = new DefaultResourceResolverChain(null , null ); ListIterator<? extends ResourceResolver> it = resolvers.listIterator(resolvers.size()); while (it.hasPrevious()) { chain = new DefaultResourceResolverChain(it.previous(), chain); } return chain; } private DefaultResourceResolverChain (@Nullable ResourceResolver resolver, @Nullable ResourceResolverChain chain) { Assert.isTrue((resolver == null && chain == null ) || (resolver != null && chain != null ), "Both resolver and resolver chain must be null, or neither is" ); this .resolver = resolver; this .nextChain = chain; } @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 ); } }
资源解析器
ResourceResolver
spring内一贯的Resolver
AbstractResourceResolver 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public abstract class AbstractResourceResolver implements ResourceResolver { protected final Log logger = LogFactory.getLog(getClass()); @Override @Nullable public Resource resolveResource (@Nullable HttpServletRequest request, String requestPath, List<? extends Resource> locations, ResourceResolverChain chain) { return resolveResourceInternal(request, requestPath, locations, chain); } @Override @Nullable public String resolveUrlPath (String resourceUrlPath, List<? extends Resource> locations, ResourceResolverChain chain) { return resolveUrlPathInternal(resourceUrlPath, locations, chain); } @Nullable protected abstract Resource resolveResourceInternal (@Nullable HttpServletRequest request, String requestPath, List<? extends Resource> locations, ResourceResolverChain chain) ; @Nullable protected abstract String resolveUrlPathInternal (String resourceUrlPath, List<? extends Resource> locations, ResourceResolverChain chain) ;}
CachingResourceResolver
通过spring缓存机制实现,参考spring缓存
根据Accpect-Encoding
和请求路径判断是否命中缓存,内部Cache
缓存Resource
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 protected Resource resolveResourceInternal (@Nullable HttpServletRequest request, String requestPath, List<? extends Resource> locations, ResourceResolverChain chain) { String key = computeKey(request, requestPath); Resource resource = this .cache.get(key, Resource.class ) ; if (resource != null ) { if (logger.isTraceEnabled()) { logger.trace("Resource resolved from cache" ); } return resource; } resource = chain.resolveResource(request, requestPath, locations); if (resource != null ) { this .cache.put(key, resource); } return resource; }
VersionResourceResolver
通过url带上版本信息,以及Etag
来实现新鲜度,本身ResourceHttpRequestHandler
已经有根据修改时间来判断新鲜度了,
这个解析器实际上就是通过按照Etag来判断的功能,etag来自于固定版本和md5.
version url格式
{version}/static/myresource.js
,我把前者叫做version类型
static/myresource-{MD5}.js
,后者为md5类型
缓存逻辑
固定版本如1.0/static/test.js
,当第一次客户端访问,如果VersionResourceResolver
能够返回那么,客户端就会
对该资源带上Etag:1.0
,以后访问的时候ResourceHttpRequestHandler
会判断一次etag
,决定是否继续解析.
当出现版本更新时,服务器应该改变{vserion}为新的,至于资源文件则由服务器更新,页面url也要更新
MD5实际逻辑是一样
代码逻辑
VersionResourceResolver 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 public class VersionResourceResolver extends AbstractResourceResolver { private AntPathMatcher pathMatcher = new AntPathMatcher(); private final Map<String, VersionStrategy> versionStrategyMap = new LinkedHashMap<>(); public VersionResourceResolver addContentVersionStrategy (String... pathPatterns) { addVersionStrategy(new ContentVersionStrategy(), pathPatterns); return this ; } public VersionResourceResolver addFixedVersionStrategy (String version, String... pathPatterns) { List<String> patternsList = Arrays.asList(pathPatterns); List<String> prefixedPatterns = new ArrayList<>(pathPatterns.length); String versionPrefix = "/" + version; for (String pattern : patternsList) { prefixedPatterns.add(pattern); if (!pattern.startsWith(versionPrefix) && !patternsList.contains(versionPrefix + pattern)) { prefixedPatterns.add(versionPrefix + pattern); } } return addVersionStrategy(new FixedVersionStrategy(version), StringUtils.toStringArray(prefixedPatterns)); } protected Resource resolveResourceInternal (@Nullable HttpServletRequest request, String requestPath, List<? extends Resource> locations, ResourceResolverChain chain) { Resource resolved = chain.resolveResource(request, requestPath, locations); if (resolved != null ) { return resolved; } VersionStrategy versionStrategy = getStrategyForPath(requestPath); if (versionStrategy == null ) { return null ; } String candidateVersion = versionStrategy.extractVersion(requestPath); if (!StringUtils.hasLength(candidateVersion)) { return null ; } String simplePath = versionStrategy.removeVersion(requestPath, candidateVersion); Resource baseResource = chain.resolveResource(request, simplePath, locations); 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 ; } } private class FileNameVersionedResource extends AbstractResource implements HttpResource { private final Resource original; private final String version; public FileNameVersionedResource (Resource original, String version) { this .original = original; this .version = version; } public HttpHeaders getResponseHeaders () { HttpHeaders headers = (this .original instanceof HttpResource ? ((HttpResource) this .original).getResponseHeaders() : new HttpHeaders()); headers.setETag("\"" + this .version + "\"" ); return headers; } }
VersionStrategy
此类为VersionResourceResolver
中的实现核心,用来返回version
字符串,默认实现为两种
AbstractVersionStrategy 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 public String extractVersion (String requestPath) { private final VersionPathStrategy pathStrategy; return this .pathStrategy.extractVersion(requestPath); } @Override public String removeVersion (String requestPath, String version) { return this .pathStrategy.removeVersion(requestPath, version); } @Override public String addVersion (String requestPath, String version) { return this .pathStrategy.addVersion(requestPath, version); } protected static class PrefixVersionPathStrategy implements VersionPathStrategy { private final String prefix; public PrefixVersionPathStrategy (String version) { Assert.hasText(version, "Version must not be empty" ); this .prefix = version; } @Override @Nullable public String extractVersion (String requestPath) { return (requestPath.startsWith(this .prefix) ? this .prefix : null ); } @Override public String removeVersion (String requestPath, String version) { return requestPath.substring(this .prefix.length()); } @Override public String addVersion (String path, String version) { if (path.startsWith("." )) { return path; } else { return (this .prefix.endsWith("/" ) || path.startsWith("/" ) ? this .prefix + path : this .prefix + '/' + path); } } } protected static class FileNameVersionPathStrategy implements VersionPathStrategy { private static final Pattern pattern = Pattern.compile("-(\\S*)\\." ); @Override @Nullable public String extractVersion (String requestPath) { Matcher matcher = pattern.matcher(requestPath); if (matcher.find()) { String match = matcher.group(1 ); return (match.contains("-" ) ? match.substring(match.lastIndexOf('-' ) + 1 ) : match); } else { return null ; } } @Override public String removeVersion (String requestPath, String version) { return StringUtils.delete(requestPath, "-" + version); } @Override public String addVersion (String requestPath, String version) { String baseFilename = StringUtils.stripFilenameExtension(requestPath); String extension = StringUtils.getFilenameExtension(requestPath); return (baseFilename + '-' + version + '.' + extension); } }
ContentVersionStrategy 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class ContentVersionStrategy extends AbstractVersionStrategy { public ContentVersionStrategy () { super (new FileNameVersionPathStrategy()); } @Override public String getResourceVersion (Resource resource) { try { byte [] content = FileCopyUtils.copyToByteArray(resource.getInputStream()); return DigestUtils.md5DigestAsHex(content); } catch (IOException ex) { throw new IllegalStateException("Failed to calculate hash for " + resource, ex); } } }
FixedVersionStrategy 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class FixedVersionStrategy extends AbstractVersionStrategy { private final String version; public FixedVersionStrategy (String version) { super (new PrefixVersionPathStrategy(version)); this .version = version; } @Override public String getResourceVersion (Resource resource) { return this .version; } }
GzipResourceResolver
用来处理带有.gz
的资源拓展,并且Accept-Encoding
必须是gzip
PathResourceResolver
这是比较核心实现,用来根据url获取location
中的资源