PathResourceResolver和ClassPathResource源码解析

先来看一下 org.springframework.web.servlet.resource.PathResourceResolver 的继承关系图:

  • ResourceResolver 接口需要实现 resolveUrlPath(返回值为String) 和 resolveResource(返回值为 org.springframework.core.io.Resource
  • AbstractResourceResolver 则是实现了 ResourceResolver,并声明了继承类需要实现的对应方法 resolveUrlPathInternalresolveResourceInternal
  • PathResourceResolver 继承了 AbstractResourceResolver,在实现 resolveUrlPathInternalresolveResourceInternal 都调用了核心方法 getResource

PathResourceResolver源码解析

点开查看PathResourceResolverresolveUrlPathInternal & resolveResourceInternal 方法源码

protected Resource resolveResourceInternal(@Nullable HttpServletRequest request, String requestPath,
    List locations, ResourceResolverChain chain) {
  return getResource(requestPath, request, locations);
}
protected String resolveUrlPathInternal(String resourcePath, List locations,
    ResourceResolverChain chain) {
  // 调用 getResource 方法,就是想查询一个对应路径下,是否存在该资源?
  // 如果存在,则返回该 resourcePath,如果不存在,就返回 null
  return (StringUtils.hasText(resourcePath) &&
      getResource(resourcePath, null, locations) != null ? resourcePath : null);
}

getResource源码解析

这里调用的 locations 是个数组,因此逐一查看是否某个 location 下。

encodeIfNecessary 这个方法,功能也比较简单,就是先用分隔符/进行分词,然后对分词后的每一部分进行编码,最后再用/拼接还原。
目前主要研究访问静态资源时用到PathResourceResolver的场景。在这种场景下, encodeIfNecessary 常常是在执行 shouldEncodeRelativePath 方法直接放回 false,即不编码直接返回了,所以不重点分析了~

// PathResourceResolver.java
protected Resource getResource(String resourcePath, Resource location) throws IOException {
  // 第一步创建相对路径
  Resource resource = location.createRelative(resourcePath);
  // 第二步判断是否存在,是否可读
  if (resource.isReadable()) {
    // 第三步进一步验证资源是在相对于它被发现的位置下,以及是否在 {@link #setAllowedLocations 允许的位置} 之下。
    if (checkResource(resource, location)) {
      return resource;
    }
    else if (logger.isWarnEnabled()) {
      Resource[] allowedLocations = getAllowedLocations();
      logger.warn("Resource path \"" + resourcePath + "\" was successfully resolved " +
	"but resource \"" +	resource.getURL() + "\" is neither under the " +
	"current location \"" + location.getURL() + "\" nor under any of the " +
	"allowed locations " + (allowedLocations != null ? Arrays.asList(allowedLocations) : "[]"));
    }
  }
  return null;
}

checkResource源码解析

// PathResourceResolver.java
protected boolean checkResource(Resource resource, Resource location) throws IOException {
  // 确认resource确实在location之下
  // 也就是location.path就是resource.path的前缀部分
  if (isResourceUnderLocation(resource, location)) {
    return true;
  }
  Resource[] allowedLocations = getAllowedLocations();
  if (allowedLocations != null) {
    for (Resource current : allowedLocations) {
      if (isResourceUnderLocation(resource, current)) {
        return true;
      }
    }
  }
  return false;
}

allowedLocations来源解析

WebMvcConfigurer=>ResourceHandlerRegistration

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

  /**
    * 静态资源处理
    */
  @Override
  public void addResourceHandlers(ResourceHandlerRegistry registry) {
    // 从WebMvcConfigurer设置到ResourceHandlerRegistration
    registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
  }
}

ResourceHandlerRegistration=>ResourceHttpRequestHandler

// ResourceHandlerRegistration.java
public class ResourceHandlerRegistration {
  private final List<String> locationValues = new ArrayList<>();

  // 对应上一段代码addResourceLocations的方法
  public ResourceHandlerRegistration addResourceLocations(String... resourceLocations) {
    this.locationValues.addAll(Arrays.asList(resourceLocations));
    return this;
  }

  protected ResourceHttpRequestHandler getRequestHandler() {
    ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler();
    if (this.resourceChainRegistration != null) {
      handler.setResourceResolvers(this.resourceChainRegistration.getResourceResolvers());
      handler.setResourceTransformers(this.resourceChainRegistration.getResourceTransformers());
    }
    // 设置到 ResourceHttpRequestHandler 中
    handler.setLocationValues(this.locationValues);
    if (this.cacheControl != null) {
      handler.setCacheControl(this.cacheControl);
    }
    else if (this.cachePeriod != null) {
      handler.setCacheSeconds(this.cachePeriod);
    }
    return handler;
  }
}

ResourceHttpRequestHandler=>PathResourceResolver

// ResourceHttpRequestHandler.java
public void setLocationValues(List<String> locationValues) {
  Assert.notNull(locationValues, "Location values list must not be null");
  this.locationValues.clear();
  // 数据来自于ResourceHandlerRegistration设置 
  this.locationValues.addAll(locationValues);
}

// 把List<String>类型的locationValues转化为
// List<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);
    }
  }
}

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())) {
        // 从ResourceHttpRequestHandler.locations设置到PathResourceResolver.allowLocations中去
        pathResolver.setAllowedLocations(getLocations().toArray(new Resource[0]));
      }
      if (this.urlPathHelper != null) {
        pathResolver.setLocationCharsets(this.locationCharsets);
        pathResolver.setUrlPathHelper(this.urlPathHelper);
      }
      break;
    }
  }
}

ClassPathResource源码解析

接下来主要针对 Resource 的子类 ClassPathLocation 进行分析:

ClassPathResource 是类路径资源的 {@link Resource} 实现。
使用给定的 {@link ClassLoader} 或给定的 {@link Class} 来加载资源。
如果类路径资源位于文件系统中,则支持解析为 {@code java.io.File},但不支持 JAR 中的资源(解析为 File)。始终支持解析为 URL。

createRelative源码解析

该方法作用就是解析出文件在 classpath 下的相对路径:

比如这个文件 classes/static/lib/jquery-3.6.0.min.js 在类路径下的相对路径就是 static/lib/jquery-3.6.0.min.js

// ClassPathResource.java
public Resource createRelative(String relativePath) {
  String pathToUse = StringUtils.applyRelativePath(this.path, relativePath);
  return (this.clazz != null ? new ClassPathResource(pathToUse, this.clazz) :
    new ClassPathResource(pathToUse, this.classLoader));
}
// StringUtils.java
public static String applyRelativePath(String path, String relativePath) {
  // 比如参数 path="static/",relativePath="lib/jquery-3.6.0.min.js"
  int separatorIndex = path.lastIndexOf("/");
  if (separatorIndex != -1) {
    String newPath = path.substring(0, separatorIndex); // 结果为 newPath="static",去掉了结尾的"/"
    if (!relativePath.startsWith("/")) {
      newPath += FOLDER_SEPARATOR; // newPath="static/",看似无意义,其实是考虑到 relativePath 和父文件夹之间还是要有一个"/"作为分隔
    }
    return newPath + relativePath; 返回值为 static/lib/jquery-3.6.0.min.js
  }
  else {
    return relativePath;
  }
}

isReadable&exists源码解析

// 接口 Resource.java
public interface Resource extends InputStreamSource {
  boolean exists();

  // isReadable默认和exists是同一个实现,ClassPathResource没有对此方法进行覆写
  default boolean isReadable() {
    return exists();
  }
}

继续来看 exists 方法

// ClassPathResource.java
@Override
public boolean exists() {
  return (resolveURL() != null);
}

protected URL resolveURL() {
  // 到了这一层,再往下就是调用 Jdk 的方法了
  // this.path 的类型是String,所以以下三个方法的入参类型也是String
  // 输入一个字符串url,返回一个URL对象
  try {
    if (this.clazz != null) {
      return this.clazz.getResource(this.path);
    }
    else if (this.classLoader != null) {
      return this.classLoader.getResource(this.path);
    }
    else {
      return ClassLoader.getSystemResource(this.path);
    }
  }
  catch (IllegalArgumentException ex) {
    // Should not happen according to the JDK's contract:
    // see https://github.com/openjdk/jdk/pull/2662
    return null;
  }
}
posted @ 2022-04-16 17:17  极客子羽  阅读(351)  评论(0编辑  收藏  举报