Spring MVC : ResourceHttpRequestHandler

本文详细解析Spring MVC框架中静态资源处理流程,包括ResourceHttpRequestHandler类的配置与工作原理,以及如何通过DispatcherServlet和HandlerAdapter处理静态资源请求。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

概述

ResourceHttpRequestHandler是一个HttpRequestHandler实现类,用于处理静态资源请求。

配置例子 :

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {    
	/**
     * 静态资源文件映射配置
     * @param registry
     */
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {       
        // 本例子是 映射多个静态资源路径到某个URL path pattern,
        // 实际上也可以映射多个静态资源路径到多个 URL path pattern,
        registry.addResourceHandler("/my/**")
        		.addResourceLocations("classpath:/MyStatic/","file:/web_starter_repo/");

        // 映射到jar包内的静态文件(真正的静态文件,应用运行时,这些文件无业务逻辑,也不能被替换或者修改)
        registry.addResourceHandler("/my-static/**")
        		.addResourceLocations("classpath:/MyStatic/")
        		.resourceChain(true);
    }
	
	// ... 其他 Spring MVC 配置
}	

Spring MVC配置机制的bean组件HandlerMapping resourceHandlerMapping初始化时会使用以上静态资源配置。该HandlerMapping实际使用类为SimpleUrlHandlerMapping,URL path pattern所映射的Handler的实现类就是ResourceHttpRequestHandler

// WebMvcConfigurationSupport 代码片段
	@Bean
	@Nullable
	public HandlerMapping resourceHandlerMapping() {
		Assert.state(this.applicationContext != null, "No ApplicationContext set");
		Assert.state(this.servletContext != null, "No ServletContext set");

		ResourceHandlerRegistry registry = new ResourceHandlerRegistry(this.applicationContext,
				this.servletContext, mvcContentNegotiationManager(), mvcUrlPathHelper());
       // 这里调用的 addResourceHandlers 会是开发人员或者框架其他部分提供的配置逻辑                 
		addResourceHandlers(registry);

       // 生成面向静态资源的 handlerMapping,实际使用类是 SimpleUrlHandlerMapping
		AbstractHandlerMapping handlerMapping = registry.getHandlerMapping();
		if (handlerMapping == null) {
			return null;
		}
		handlerMapping.setPathMatcher(mvcPathMatcher());
		handlerMapping.setUrlPathHelper(mvcUrlPathHelper());
		handlerMapping.setInterceptors(getInterceptors());
		handlerMapping.setCorsConfigurations(getCorsConfigurations());
		return handlerMapping;
	}

关于bean组件HandlerMapping resourceHandlerMapping的生成,核心的逻辑如下 :

// ResourceHandlerRegistry#getHandlerMapping
	@Nullable
	protected AbstractHandlerMapping getHandlerMapping() {
		if (this.registrations.isEmpty()) {
			return null;
		}

		Map<String, HttpRequestHandler> urlMap = new LinkedHashMap<>();
       // 遍历配置中指定的每个注册项  ResourceHandlerRegistration registration,
       // 每个注册项可以理解为 : 多个资源路径 跟 多个 URL pattern 的映射信息 N:M
		for (ResourceHandlerRegistration registration : this.registrations) {
           // 遍历当前注册项中的每个 URL pattern
			for (String pathPattern : registration.getPathPatterns()) {
              // 针对每个注册项中的每个 URL pattern 生成一个 ResourceHttpRequestHandler,
              // 该 ResourceHttpRequestHandler 包含了向多个静态资源路径的映射 : 1 :M
				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);
				}
              
              // 添加新建的 ResourceHttpRequestHandler 到 urlMap
				urlMap.put(pathPattern, handler);
			}
		}

       // 使用 urlMap 构建  SimpleUrlHandlerMapping 对象并返回
		SimpleUrlHandlerMapping handlerMapping = new SimpleUrlHandlerMapping();
		handlerMapping.setOrder(this.order);
		handlerMapping.setUrlMap(urlMap);
		return handlerMapping;
	}

然后当客户访问配置中匹配所设置的URL pattern的请求到达时,上面的SimpleUrlHandlerMapping handlerMapping就会被DispatcherServlet使用到,进而找到的Handler就是ResourceHttpRequestHandler,相应的HandlerAdapterHttpRequestHandlerAdapterDispatcherServlet会使用HttpRequestHandlerAdapter执行ResourceHttpRequestHandler加载静态资源返回给请求者,没有后续DispatcherServlet进行视图解析和渲染的过程。具体代码如下所示:

// HttpRequestHandlerAdapter 代码片段
	@Override
	@Nullable
	public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {

		((HttpRequestHandler) handler).handleRequest(request, response);
       // 注意这里返回 null, 表示告知调用者 请求已经被完全处理,后续不需要进行视图解析和渲染 
		return null;
	}

而关于ResourceHttpRequestHandler#handleRequest方法的逻辑,可以参考下面的源代码。

源代码

源代码版本 Spring Web MVC 5.1.5.RELEASE

package org.springframework.web.servlet.resource;

// 省略 import 行

public class ResourceHttpRequestHandler extends WebContentGenerator
		implements HttpRequestHandler, EmbeddedValueResolverAware, InitializingBean, CorsConfigurationSource {

	private static final Log logger = LogFactory.getLog(ResourceHttpRequestHandler.class);

	private static final String URL_RESOURCE_CHARSET_PREFIX = "[charset=";

    // 所映射的静态资源文件的位置路径,String 列表形式
    // 对外部调用者来讲,locationValues ,locations 二者只能使用其一, 
    // ResourceHttpRequestHandler 内部会综合考虑这两个字段,最终归一为使用 locations
	private final List<String> locationValues = new ArrayList<>(4);

    // 所映射的静态资源文件的位置路径, Resource 列表形式
    // 对外部调用者来讲,locationValues ,locations 二者只能使用其一, 
    // ResourceHttpRequestHandler 内部会综合考虑这两个字段,最终归一为使用 locations    
	private final List<Resource> locations = new ArrayList<>(4);

	private final Map<Resource, Charset> locationCharsets = new HashMap<>(4);

    // 缺省会被初始化为只有一个元素 : PathResourceResolver
	private final List<ResourceResolver> resourceResolvers = new ArrayList<>(4);

	private final List<ResourceTransformer> resourceTransformers = new ArrayList<>(4);

    // 缺省会被设置成 DefaultResourceResolverChain
	@Nullable
	private ResourceResolverChain resolverChain;

	@Nullable
	private ResourceTransformerChain transformerChain;

    // 缺省会被设置成 ResourceHttpMessageConverter , 用于将静态资源内容写入到响应对象
	@Nullable
	private ResourceHttpMessageConverter resourceHttpMessageConverter;

    // 缺省会被设置成 ResourceRegionHttpMessageConverter, 用于支持 HTTP Range 头部使用时, 
    // 将静态资源的部分写入到响应对象
	@Nullable
	private ResourceRegionHttpMessageConverter resourceRegionHttpMessageConverter;

    // 缺省会被设置成 null
	@Nullable
	private ContentNegotiationManager contentNegotiationManager;

    // 从请求路径中分析文件扩展名为用于查找Media Type的key的工具,会结合属性
    // servletContext 和 contentNegotiationManager 初始化,如果 servletContext 不为 null,
    // 会是一个 ServletPathExtensionContentNegotiationStrategy,否则会是一个
    // PathExtensionContentNegotiationStrategy
    // contentNegotiationManager 如果被设置的话,会被用于获取初始化 
    // contentNegotiationStrategy 的 Map<String, MediaType> mediaTypes
	@Nullable
	private PathExtensionContentNegotiationStrategy contentNegotiationStrategy;

	@Nullable
	private CorsConfiguration corsConfiguration;

	@Nullable
	private UrlPathHelper urlPathHelper;

	@Nullable
	private StringValueResolver embeddedValueResolver;


	public ResourceHttpRequestHandler() {
		super(HttpMethod.GET.name(), HttpMethod.HEAD.name());
	}


	/**
	 * An alternative to #setLocations(List) that accepts a list of
	 * String-based location values, with support for UrlResource's
	 * (e.g. files or HTTP URLs) with a special prefix to indicate the charset
	 * to use when appending relative paths. For example
	 * "[charset=Windows-31J]http://example.org/path".
	 * @since 4.3.13
	 */
	public void setLocationValues(List<String> locationValues) {
		Assert.notNull(locationValues, "Location values list must not be null");
		this.locationValues.clear();
		this.locationValues.addAll(locationValues);
	}

	/**
	 * Set the List of Resource locations to use as sources
	 * for serving static resources.
	 * @see #setLocationValues(List)
	 */
	public void setLocations(List<Resource> locations) {
		Assert.notNull(locations, "Locations list must not be null");
		this.locations.clear();
		this.locations.addAll(locations);
	}

	/**
	 * Return the configured List of Resource locations.
	 * Note that if #setLocationValues(List) locationValues are provided,
	 * instead of loaded Resource-based locations, this method will return
	 * empty until after initialization via #afterPropertiesSet().
	 * @see #setLocationValues
	 * @see #setLocations
	 */
	public List<Resource> getLocations() {
		return this.locations;
	}

	/**
	 * Configure the list of ResourceResolver ResourceResolvers to use.
	 * By default PathResourceResolver is configured. If using this property,
	 * it is recommended to add PathResourceResolver as the last resolver.
	 */
	public void setResourceResolvers(@Nullable List<ResourceResolver> resourceResolvers) {
		this.resourceResolvers.clear();
		if (resourceResolvers != null) {
			this.resourceResolvers.addAll(resourceResolvers);
		}
	}

	/**
	 * Return the list of configured resource resolvers.
	 */
	public List<ResourceResolver> getResourceResolvers() {
		return this.resourceResolvers;
	}

	/**
	 * Configure the list of ResourceTransformer ResourceTransformers to use.
	 * By default no transformers are configured for use.
	 */
	public void setResourceTransformers(@Nullable List<ResourceTransformer> resourceTransformers) {
		this.resourceTransformers.clear();
		if (resourceTransformers != null) {
			this.resourceTransformers.addAll(resourceTransformers);
		}
	}

	/**
	 * Return the list of configured resource transformers.
	 */
	public List<ResourceTransformer> getResourceTransformers() {
		return this.resourceTransformers;
	}

	/**
	 * Configure the ResourceHttpMessageConverter to use.
	 * By default a ResourceHttpMessageConverter will be configured.
	 * @since 4.3
	 */
	public void setResourceHttpMessageConverter(@Nullable ResourceHttpMessageConverter messageConverter) {
		this.resourceHttpMessageConverter = messageConverter;
	}

	/**
	 * Return the configured resource converter.
	 * @since 4.3
	 */
	@Nullable
	public ResourceHttpMessageConverter getResourceHttpMessageConverter() {
		return this.resourceHttpMessageConverter;
	}

	/**
	 * Configure the ResourceRegionHttpMessageConverter to use.
	 * By default a ResourceRegionHttpMessageConverter will be configured.
	 * @since 4.3
	 */
	public void setResourceRegionHttpMessageConverter(
				@Nullable ResourceRegionHttpMessageConverter messageConverter) {
		this.resourceRegionHttpMessageConverter = messageConverter;
	}

	/**
	 * Return the configured resource region converter.
	 * @since 4.3
	 */
	@Nullable
	public ResourceRegionHttpMessageConverter getResourceRegionHttpMessageConverter() {
		return this.resourceRegionHttpMessageConverter;
	}

	/**
	 * Configure a ContentNegotiationManager to help determine the
	 * media types for resources being served. If the manager contains a path
	 * extension strategy it will be checked for registered file extension.
	 * @since 4.3
	 */
	public void setContentNegotiationManager(@Nullable ContentNegotiationManager contentNegotiationManager) {
		this.contentNegotiationManager = contentNegotiationManager;
	}

	/**
	 * Return the configured content negotiation manager.
	 * @since 4.3
	 */
	@Nullable
	public ContentNegotiationManager getContentNegotiationManager() {
		return this.contentNegotiationManager;
	}

	/**
	 * Specify the CORS configuration for resources served by this handler.
	 * By default this is not set in which allows cross-origin requests.
	 */
	public void setCorsConfiguration(CorsConfiguration corsConfiguration) {
		this.corsConfiguration = corsConfiguration;
	}

	/**
	 * Return the specified CORS configuration.
	 */
	@Override
	@Nullable
	public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
		return this.corsConfiguration;
	}

	/**
	 * Provide a reference to the UrlPathHelper used to map requests to
	 * static resources. This helps to derive information about the lookup path
	 * such as whether it is decoded or not.
	 * @since 4.3.13
	 */
	public void setUrlPathHelper(@Nullable UrlPathHelper urlPathHelper) {
		this.urlPathHelper = urlPathHelper;
	}

	/**
	 * The configured UrlPathHelper.
	 * @since 4.3.13
	 */
	@Nullable
	public UrlPathHelper getUrlPathHelper() {
		return this.urlPathHelper;
	}

	@Override
	public void setEmbeddedValueResolver(StringValueResolver resolver) {
		this.embeddedValueResolver = resolver;
	}


    // InitializingBean 接口定义的初始化方法
	@Override
	public void afterPropertiesSet() throws Exception {
       // 如果  locationValues 被使用,解析 locationValues 中的 字符串资源路径为 Resource,统一到
       // locations 中以备使用
		resolveResourceLocations();

		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.");
		}

		if (this.resourceResolvers.isEmpty()) {
			this.resourceResolvers.add(new PathResourceResolver());
		}

		initAllowedLocations();

		// Initialize immutable resolver and transformer chains
		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();
	}

    // 如果  locationValues 被使用,解析 locationValues 中的 字符串资源路径为 Resource,统一到
    // 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)) {
				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);
			}
		}
	}

	/**
	 * Look for a PathResourceResolver among the configured resource
	 * resolvers and set its allowedLocations property (if empty) to
	 * match the #setLocations locations configured on this class.
	 */
	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;
			}
		}
	}

	/**
	 * Initialize the content negotiation strategy depending on the ContentNegotiationManager
	 * setup and the availability of a ServletContext.
	 * @see ServletPathExtensionContentNegotiationStrategy
	 * @see PathExtensionContentNegotiationStrategy
	 */
	protected PathExtensionContentNegotiationStrategy initContentNegotiationStrategy() {
		Map<String, MediaType> mediaTypes = null;
		if (getContentNegotiationManager() != null) {
			PathExtensionContentNegotiationStrategy strategy =
					getContentNegotiationManager().getStrategy(PathExtensionContentNegotiationStrategy.class);
			if (strategy != null) {
				mediaTypes = new HashMap<>(strategy.getMediaTypes());
			}
		}
		return (getServletContext() != null ?
				new ServletPathExtensionContentNegotiationStrategy(getServletContext(), mediaTypes) :
				new PathExtensionContentNegotiationStrategy(mediaTypes));
	}


	/**
	 * Processes a resource request. 处理资源请求
	 * Checks for the existence of the requested resource in the configured list of locations.
	 * If the resource does not exist, a 404 response will be returned to the client.
	 * If the resource exists, the request will be checked for the presence of the
	 * Last-Modified header, and its value will be compared against the last-modified
	 * timestamp of the given resource, returning a 304 status code if the
	 * Last-Modified value  is greater. If the resource is newer than the
	 * Last-Modified value, or the header is not present, the content resource
	 * of the resource will be written to the response with caching headers
	 * set to expire one year in the future.
     * 在配置的资源路径中检查请求资源的存在性。
     * 如果目标资源不存在,响应 404,使用 response.sendError。
     * 如果目标资源存在,会看是否使用 Last-Modified 头部。如果使用了 Last-Modified 头部,
     * 该头部值会跟资源最近修改时间戳作比较,如果 Last-Modified 更大,也就是说资源在 
     * Last-Modified 指定的时间之后没有被修改,则返回 304, 如果 Last-Modified 较小,
     * 或者 Last-Modified 根本没有被使用,则目标资源的内容会被写入到响应,并设置缓存头部,
     * 生命资源会在1年时过期。
	 */
	@Override
	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) {
          //  目标静态资源不存在,返回错误 404
			logger.debug("Resource not found");
			response.sendError(HttpServletResponse.SC_NOT_FOUND);
			return;
		}

		if (HttpMethod.OPTIONS.matches(request.getMethod())) {
			response.setHeader("Allow", getAllowHeader());
			return;
		}

		// Supported methods and required session
       // 条件检查 :
       // 1. 如果支持HTTP 方法属性 supportedMethods 被设置,则要求请求的方法必须是被支持的
       // 2. 如果需要session属性 requireSession 被设置,则要求必须已经存在相应的session
		checkRequest(request);

		// Header phase
       // 开始对响应头部的输出阶段
        // 对 LastModified 头部的处理,如果使用了该头部,并且资源未在该头部指定的时间之后被修改,
        // 则直接返回 304        
		if (new ServletWebRequest(request, response).checkNotModified(resource.lastModified())) {
			logger.trace("Resource not modified");
			return;
		}

		// Apply cache settings, if any
       //  应用必要的缓存头部 :
       // this.cacheControl
       // this.cacheSeconds
       // this.varyByRequestHeaders
		prepareResponse(response);

		// Check the media type for the resource
       // 尝试使用 contentNegotiationStrategy 获取资源的  MediaType
		MediaType mediaType = getMediaType(request, resource);

		// Content phase
       // 开始对响应内容的输出阶段 
       // 与内容有关的头部也在这一阶段进行
		if (METHOD_HEAD.equals(request.getMethod())) {
           // 对 HEAD 请求的处理,只写头部不写内容 
          // 头部 Content-Type, Content-Length 的设置
          // 头部 Accept-Ranges : bytes
			setHeaders(response, resource, mediaType);
           // 不写入内容,因为当前请求方法是 HEAD 
			return;
		}

		ServletServerHttpResponse outputMessage = new ServletServerHttpResponse(response);
		if (request.getHeader(HttpHeaders.RANGE) == null) {
           // 最常见的静态资源访问处理 
			Assert.state(this.resourceHttpMessageConverter != null, "Not initialized");
          // 头部 Content-Type, Content-Length 的设置
          // 头部 Accept-Ranges : bytes            
			setHeaders(response, resource, mediaType);
          // 写入资源内容到响应  
			this.resourceHttpMessageConverter.write(resource, mediaType, outputMessage);
		}
		else {
          // 请求使用了头部 Range 的资源请求情况,
          
          // 确保属性 resourceRegionHttpMessageConverter 被设置
			Assert.state(this.resourceRegionHttpMessageConverter != null, "Not initialized");
			response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes");
			ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(request);
			try {
              // 获取要返回哪些Range的参数            
				List<HttpRange> httpRanges = inputMessage.getHeaders().getRange();
              // 响应状态字设置为 206  
				response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
              // 向响应对象写入目标资源数据中被请求的那些 Range 
				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);
			}
		}
	}

    // 根据请求URL路径,从配置的静态资源路径中获取目标静态资源并作相应的变换,如果路径错误或者目标静态资源找不到则返回null
	@Nullable
	protected Resource getResource(HttpServletRequest request) throws IOException {
		String path = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
		if (path == null) {
			throw new IllegalStateException("Required request attribute '" +
					HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE + "' is not set");
		}

		path = processPath(path);
		if (!StringUtils.hasText(path) || isInvalidPath(path)) {
			return null;
		}
		if (isInvalidEncodedPath(path)) {
			return null;
		}

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

	/**
	 * Process the given resource path.
	 * The default implementation replaces:
	 * 
	 * 1. Backslash with forward slash.
	 * 2. Duplicate occurrences of slash with a single slash.
	 * 3. Any combination of leading slash and control characters (00-1F and 7F)
	 * with a single "/" or "". For example "  / // foo/bar"
	 * becomes "/foo/bar".
	 * 
	 * @since 3.2.12
	 */
	protected String processPath(String path) {
		path = StringUtils.replace(path, "\\", "/");
		path = cleanDuplicateSlashes(path);
		return cleanLeadingSlash(path);
	}

	private String cleanDuplicateSlashes(String path) {
		StringBuilder sb = null;
		char prev = 0;
		for (int i = 0; i < path.length(); i++) {
			char curr = path.charAt(i);
			try {
				if ((curr == '/') && (prev == '/')) {
					if (sb == null) {
						sb = new StringBuilder(path.substring(0, i));
					}
					continue;
				}
				if (sb != null) {
					sb.append(path.charAt(i));
				}
			}
			finally {
				prev = curr;
			}
		}
		return sb != null ? sb.toString() : path;
	}

	private String cleanLeadingSlash(String path) {
		boolean slash = false;
		for (int i = 0; i < path.length(); i++) {
			if (path.charAt(i) == '/') {
				slash = true;
			}
			else if (path.charAt(i) > ' ' && path.charAt(i) != 127) {
				if (i == 0 || (i == 1 && slash)) {
					return path;
				}
				return (slash ? "/" + path.substring(i) : path.substring(i));
			}
		}
		return (slash ? "/" : "");
	}

	/**
	 * Check whether the given path contains invalid escape sequences.
	 * @param path the path to validate
	 * @return true if the path is invalid, false otherwise
	 */
	private boolean isInvalidEncodedPath(String path) {
		if (path.contains("%")) {
			try {
				// Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars
				String decodedPath = URLDecoder.decode(path, "UTF-8");
				if (isInvalidPath(decodedPath)) {
					return true;
				}
				decodedPath = processPath(decodedPath);
				if (isInvalidPath(decodedPath)) {
					return true;
				}
			}
			catch (IllegalArgumentException | UnsupportedEncodingException ex) {
				// Should never happen...
			}
		}
		return false;
	}

	/**
	 * Identifies invalid resource paths. By default rejects:
	 * 
	 * 1. Paths that contain "WEB-INF" or "META-INF"
	 * 2. Paths that contain "../" after a call to rg.springframework.util.StringUtils#cleanPath.
	 * 3. Paths that represent a org.springframework.util.ResourceUtils#isUrl
	 * valid URL or would represent one after the leading slash is removed.
	 * 
	 * Note: this method assumes that leading, duplicate '/'
	 * or control characters (e.g. white space) have been trimmed so that the
	 * path starts predictably with a single '/' or does not have one.
	 * @param path the path to validate
	 * @return true if the path is invalid, false otherwise
	 * @since 3.0.6
	 */
	protected boolean isInvalidPath(String path) {
		if (path.contains("WEB-INF") || path.contains("META-INF")) {
			if (logger.isWarnEnabled()) {
				logger.warn("Path with \"WEB-INF\" or \"META-INF\": [" + path + "]");
			}
			return true;
		}
		if (path.contains(":/")) {
			String relativePath = (path.charAt(0) == '/' ? path.substring(1) : path);
			if (ResourceUtils.isUrl(relativePath) || relativePath.startsWith("url:")) {
				if (logger.isWarnEnabled()) {
					logger.warn("Path represents URL or has \"url:\" prefix: [" + path + "]");
				}
				return true;
			}
		}
		if (path.contains("..") && StringUtils.cleanPath(path).contains("../")) {
			if (logger.isWarnEnabled()) {
				logger.warn("Path contains \"../\" after call to StringUtils#cleanPath: [" + path + "]");
			}
			return true;
		}
		return false;
	}

	/**
	 * Determine the media type for the given request and the resource matched
	 * to it. This implementation tries to determine the MediaType based on the
	 * file extension of the Resource via
	 * ServletPathExtensionContentNegotiationStrategy#getMediaTypeForResource.
	 * @param request the current request
	 * @param resource the resource to check
	 * @return the corresponding media type, or null if none found
	 */
	@Nullable
	protected MediaType getMediaType(HttpServletRequest request, Resource resource) {
		return (this.contentNegotiationStrategy != null ?
				this.contentNegotiationStrategy.getMediaTypeForResource(resource) : null);
	}

	/**
	 * Set headers on the given servlet response.
	 * Called for GET requests as well as HEAD requests.
	 * @param response current servlet response
	 * @param resource the identified resource (never null)
	 * @param mediaType the resource's media type (never null)
	 * @throws IOException in case of errors while setting the headers
	 */
	protected void setHeaders(HttpServletResponse response, Resource resource, @Nullable MediaType mediaType)
			throws IOException {

		long length = resource.contentLength();
		if (length > Integer.MAX_VALUE) {
			response.setContentLengthLong(length);
		}
		else {
			response.setContentLength((int) length);
		}

		if (mediaType != null) {
			response.setContentType(mediaType.toString());
		}
		if (resource instanceof HttpResource) {
			HttpHeaders resourceHeaders = ((HttpResource) resource).getResponseHeaders();
			resourceHeaders.forEach((headerName, headerValues) -> {
				boolean first = true;
				for (String headerValue : headerValues) {
					if (first) {
						response.setHeader(headerName, headerValue);
					}
					else {
						response.addHeader(headerName, headerValue);
					}
					first = false;
				}
			});
		}
		response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes");
	}


	@Override
	public String toString() {
		return "ResourceHttpRequestHandler " + formatLocations();
	}

	private Object formatLocations() {
		if (!this.locationValues.isEmpty()) {
			return this.locationValues.stream().collect(Collectors.joining("\", \"", "[\"", "\"]"));
		}
		else if (!this.locations.isEmpty()) {
			return this.locations;
		}
		return Collections.emptyList();
	}

}

参考文章

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值