avatar

springMVC配置二

此部分关于描述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");
//创建一个DefaultServletHttpRequestHandler
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);

//创建SimpleUrlHandlerMapping,并设置urlMap
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));

// Ensure BeanNameUrlHandlerMapping (SPR-8289) and default HandlerAdapters are not "turned off"
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 + "'");
}
//forward实现
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:包含ResourceHandlerRegistrationResourceChainRegistration
  • 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);
//开启version资源解析器
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);
//通过locationValues解析的资源
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;
//region资源转换器
@Nullable
private ResourceRegionHttpMessageConverter resourceRegionHttpMessageConverter;

@Nullable
private ContentNegotiationManager contentNegotiationManager;

@Nullable
private PathExtensionContentNegotiationStrategy contentNegotiationStrategy;

@Nullable
private CorsConfiguration corsConfiguration;

@Nullable
private UrlPathHelper urlPathHelper;

//转换#{属性},由aware接口实现
@Nullable
private StringValueResolver embeddedValueResolver;


public ResourceHttpRequestHandler() {
super(HttpMethod.GET.name(), HttpMethod.HEAD.name());
}
/==============================初始化==========================================
public void afterPropertiesSet() throws Exception {
//[1]通过location初始化文本资源
resolveResourceLocations();
//[1!]
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.");
}
//[2!]至少添加一个path处理器
if (this.resourceResolvers.isEmpty()) {
this.resourceResolvers.add(new PathResourceResolver());
}
//[2!]
//[3]
initAllowedLocations();
//[3!]

//[4]初始化其他必要变量
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();
//[4!]
}
//-------------------根据locations解析资源-------------------------
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)) { //如果带有[charset=xx]前缀,则保存一份带有编码的map
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); //带有编码的map
}
}
}
//-----------------------正确解析的文本资源添加到PathResourceResolver中-----------------------------
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 {

// For very general mappings (e.g. "/") we need to check 404 first
Resource resource = getResource(request);
if (resource == null) {
logger.debug("Resource not found");
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
//[1!]
//[2]options方法则返回mvc支持的方法
if (HttpMethod.OPTIONS.matches(request.getMethod())) {
response.setHeader("Allow", getAllowHeader());
return;
}
//[2!]
// Supported methods and required session
checkRequest(request);

// Header phase
//这里根据判断资源是否过期
if (new ServletWebRequest(request, response).checkNotModified(resource.lastModified())) {
logger.trace("Resource not modified");
return;
}

// Apply cache settings, if any
//处理缓存机制
prepareResponse(response);
//[3]head方法表示仅仅返回响应头
// Check the media type for the resource
MediaType mediaType = getMediaType(request, resource);

// Content phase
if (METHOD_HEAD.equals(request.getMethod())) {
setHeaders(response, resource, mediaType);
return;
}
//[3!]

//[4] 转化器来写出资源
ServletServerHttpResponse outputMessage = new ServletServerHttpResponse(response);
if (request.getHeader(HttpHeaders.RANGE) == null) { //非range资源
Assert.state(this.resourceHttpMessageConverter != null, "Not initialized");
setHeaders(response, resource, mediaType);
this.resourceHttpMessageConverter.write(resource, mediaType, outputMessage);
}
else {//range资源
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);
}
}
//[4!]
}
  • 总结
    • 通过解析器来处理资源
    • 通过转换器写出资源
    • 处理了缓存头,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;

//简单来说就是通过list创建一个链表,next表示下一个节点
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);
}

}
  • 总结
    • 链条顺序:和构造器resolvers顺序一致

资源解析器

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;
}
//nextChain获取
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 {
//Ant路径匹配器
private AntPathMatcher pathMatcher = new AntPathMatcher();
/** Map from path pattern -> VersionStrategy. */
//key为url路径,value为VersionStrategy
private final Map<String, VersionStrategy> versionStrategyMap = new LinkedHashMap<>();
//添加md5类型
public VersionResourceResolver addContentVersionStrategy(String... pathPatterns) {
addVersionStrategy(new ContentVersionStrategy(), pathPatterns);
return this;
}
//添加version类型
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) {
//[1]优先执行next
Resource resolved = chain.resolveResource(request, requestPath, locations);
if (resolved != null) {
return resolved;
}
//[1!]
//[2]根据路径获取VS,Ant路径匹配
VersionStrategy versionStrategy = getStrategyForPath(requestPath);
if (versionStrategy == null) {
return null;
}
//[2!]
//[3]尝试获取请求中存在的版本信息
String candidateVersion = versionStrategy.extractVersion(requestPath);
if (!StringUtils.hasLength(candidateVersion)) {
return null;
}
//[3!]
//[4]首先尝试获取基础版本,即不带版本信息的
//从路径中删除版本信息
String simplePath = versionStrategy.removeVersion(requestPath, candidateVersion);
//获取基础资源,由于ResourceHttpRequestHandler#afterPropertiesSet的实现
//因此责任链会有一个兜底的PathResourceResolver
Resource baseResource = chain.resolveResource(request, simplePath, locations);
if (baseResource == null) {
return null;
}
//[4!]
//[5]根据基础资源生成版本号
String actualVersion = versionStrategy.getResourceVersion(baseResource);
if (candidateVersion.equals(actualVersion)) {//当客户端发送的版本信息和当前资源能够匹配则返回
return new FileNameVersionedResource(baseResource, candidateVersion);//返回VersionResource,这是一个HttpResource
}
else {
if (logger.isTraceEnabled()) {
logger.trace("Found resource for \"" + requestPath + "\", but version [" +
candidateVersion + "] does not match");
}
return null;
}
//[5!]
}
//====================内部类===============================
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;
}
//这个HttpResource设置了一个etag头信息
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);
}
//用来处理`{version}/xx.资源后缀`这种类型
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) { //返回资源附带的前缀{version}部分
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
//使用正则获取-{MD5}版本信息
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
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);//生成md5摘要
}
catch (IOException ex) {
throw new IllegalStateException("Failed to calculate hash for " + resource, ex);
}
}

}
  • FixedVersionStrategy
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;


/**
* Create a new FixedVersionStrategy with the given version string.
* @param version the fixed version string to use
*/
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中的资源

文章作者: fancylight
文章链接: https://www.fancylight.top/2020/04/03/springMVC%E9%85%8D%E7%BD%AE%E4%BA%8C/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 博客
打赏
  • 微信
    微信
  • 支付寶
    支付寶

评论