SpringBoot访问静态文件的原理总结

​ 上一篇写了使用SpringBoot访问静态文件的几种方法 ,这篇文章来讲一讲配置Springboot访问静态文件的原理,为何简单配置就能实现静态文件的加载。

  • TOC
    {:toc}

我们的做法

​ 我们是重写了WebMvcConfigurationSupportaddResourceHandlers方法,在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处理,去配置的两个存储位置查找资源。

image-20200412144041449

何时调用HandlerMapping的

​ 将断点打在DispathervletdoDispatch方法上,访问 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处理请求了。

image-20200412145156338

ResourceHttpRequestHandler如何处理请求的

​ 查看ResourceHttpRequestHandlerhandleRequest方法。它调用了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访问静态文件的原理。


SpringBoot访问静态文件的原理总结
https://www.huangchaoyu.com/3949548109.html
作者
hcy
发布于
2020年4月12日
更新于
2024年8月17日
许可协议