上一篇写了使用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) { 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");
ResourceHandlerRegistry registry = new ResourceHandlerRegistry(this.applicationContext, this.servletContext, contentNegotiationManager, urlPathHelper); addResourceHandlers(registry); AbstractHandlerMapping handlerMapping = registry.getHandlerMapping(); if (handlerMapping == null) { return null; } 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; } 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); }
|
上面的配置将断点打到这里,查看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) { 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."); 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); } else if (location.startsWith(CLASSPATH_URL_PREFIX)) { return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader()); } else { try { URL url = new URL(location); return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url)); } catch (MalformedURLException ex) { 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访问静态文件的原理。