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

Posted by hcy on April 12, 2020

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

## 我们的做法

​ 我们是重写了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访问静态文件的原理。


转载请注明出处:https://www.huangchaoyu.com/2020/04/12/SpringBoot访问静态文件的原理总结/