上一篇写了使用SpringBoot访问静态文件的几种方法 ,这篇文章来讲一讲配置Springboot访问静态文件的原理,为何简单配置就能实现静态文件的加载。
## 我们的做法
我们是重写了WebMvcConfigurationSupport
的addResourceHandlers
方法,在registry
内配置映射关系来实现静态文件映射的。
1
2
3
4
5
6
7
8
@Configuration
public class WebConfig extends WebMvcConfigurationSupport {
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
//访问文件夹写法,注意文件夹路径要以file开头,以 / 结尾
registry.addResourceHandler("/static/**", "/download/**").addResourceLocations("file:F:/娱乐/","classpath:/static/");
}
}
追踪addResourceHandlers
的父类调用关系。是一个加了@Bean
注解的方法调用了此方法。
此方法会使用我们配置的registry
创建一个HandlerMapping
处理,如果没配置则不会构造这个HandlerMapping
。
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
@Bean
@Nullable
public HandlerMapping resourceHandlerMapping(
@Qualifier("mvcUrlPathHelper") UrlPathHelper urlPathHelper,
@Qualifier("mvcPathMatcher") PathMatcher pathMatcher,
@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
@Qualifier("mvcConversionService") FormattingConversionService conversionService,
@Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider) {
Assert.state(this.applicationContext != null, "No ApplicationContext set");
Assert.state(this.servletContext != null, "No ServletContext set");
//这个registry作参数
ResourceHandlerRegistry registry = new ResourceHandlerRegistry(this.applicationContext,
this.servletContext, contentNegotiationManager, urlPathHelper);
//回掉我们重写的方法
addResourceHandlers(registry);
//如果我们没重写,直接返回null
AbstractHandlerMapping handlerMapping = registry.getHandlerMapping();
if (handlerMapping == null) {
return null;
}
//如果重写了,能获取到HandlerMapping返回
handlerMapping.setPathMatcher(pathMatcher);
handlerMapping.setUrlPathHelper(urlPathHelper);
handlerMapping.setInterceptors(getInterceptors(conversionService, resourceUrlProvider));
handlerMapping.setCorsConfigurations(getCorsConfigurations());
return handlerMapping;
}
构造过程,查看上面代码的registry.getHandlerMapping()
调用。
他会根据我们的配置构造SimpleUrlHandlerMapping
,这个SimpleUrlHandlerMapping
内部保存多个HttpRequestHandler
,将每个请求路径映射成一个HttpRequestHandler
。
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
protected AbstractHandlerMapping getHandlerMapping() {
if (this.registrations.isEmpty()) {
return null;
}
//根据registrations,构造多个HttpRequestHandler存入urlMap里面
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);
}
}
//根据urlMap里面的配置,构造SimpleUrlHandlerMapping
return new SimpleUrlHandlerMapping(urlMap, this.order);
}
上面的配置将断点打到这里,查看urlMap
里面的值,他的key
是拦截的路径,value
是构造的ResourceHttpRequestHandler
,这个Handler
里面配置的是两个路径分别是/static/**
和/download/**
。
值都是ResourceHttpRequestHandler实例,里面保存两个路径是file:F:/娱乐/
,classpath:/static/
。这样浏览器发送的请求如何匹配/static/**
就会被下面的handler
处理,去配置的两个存储位置查找资源。
何时调用HandlerMapping的
将断点打在Dispathervlet
的doDispatch
方法上,访问 http://localhost:8080/static/2.jpg,程序进入getHandler()
方法里。
1
2
3
4
5
6
7
8
9
10
11
12
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
//遍历handlerMappings列表,找到合适的HandlerMapping
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}
此方法内遍历的handlerMapping
如下,他会逐个调用他们的getHandler()
方法,这样就能找到对应的ResourceHttpRequestHandler
处理请求了。
ResourceHttpRequestHandler如何处理请求的
查看ResourceHttpRequestHandler
的handleRequest
方法。它调用了getResource(request)
方法获取资源。
1
2
3
4
5
6
7
8
9
10
11
12
@Override
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;
}
..... 省略
getResource
方法使用了责任模式,将多个location
封装到resolverChain
中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Nullable
protected Resource getResource(HttpServletRequest request) throws IOException {
String path = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
path = processPath(path);
Assert.notNull(this.resolverChain, "ResourceResolverChain not initialized.");
Assert.notNull(this.transformerChain, "ResourceTransformerChain not initialized.");
//resolverChain就是责任链模式,将多个locations封装成责任链
Resource resource = this.resolverChain.resolveResource(request, path, getLocations());
if (resource != null) {
resource = this.transformerChain.transform(request, resource);
}
return resource;
}
location的分类
org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry#getHandlerMapping
方法内调用了handler.afterPropertiesSet()
方法,再调用resolveResourceLocations()
方法,此方法将配置的多个路径解析成不同的Location。
1
Resource resource = applicationContext.getResource(location);
请看下面代码,location
有四种分别是
ClassPathResource
从classpath获取资源FileUrlResource
从文件获取资源UrlResource
从url获取资源ClassPathContextResource
委托给context获取资源
下面是将字符串解析成不同Location
的方法。
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
@Override
public Resource getResource(String location) {
Assert.notNull(location, "Location must not be null");
for (ProtocolResolver protocolResolver : getProtocolResolvers()) {
Resource resource = protocolResolver.resolve(location, this);
if (resource != null) {
return resource;
}
}
if (location.startsWith("/")) {
return getResourceByPath(location);
}
//以classpath开头的,创建ClassPathResource
else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
}
else {
try {
//符合URL规范的返回FileUrlResource或者UrlResource
// Try to parse the location as a URL...
URL url = new URL(location);
return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url));
}
catch (MalformedURLException ex) {
// 否则返回 ClassPathContextResource
return getResourceByPath(location);
}
}
}
总结
Springboot
将我们配置的映射:
1
registry.addResourceHandler("/static/**", "/download/**").addResourceLocations("file:F:/娱乐/","classpath:/static/");
将addResourceLocations
根据前缀解析成上面四种Location
对象,在加上配置的路径封装成ResourceHttpRequestHandler
用于处理请求,多个ResourceHttpRequestHandler
封装成SimpleUrlHandlerMapping
对象。
DispatchServlet
内能查找到SimpleUrlHandlerMapping
内的ResourceHttpRequestHandler
处理请求。
ResourceHttpRequestHandler
将内部的Location
组成责任链,按顺序查找资源。
以上就是SpringBoot
访问静态文件的原理。