概述
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
,相应的HandlerAdapter
为HttpRequestHandlerAdapter
。DispatcherServlet
会使用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();
}
}