• 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;
      }
    }
    
  • 相关阅读:
    ubuntu共享打印机
    vc常用技巧
    SipDroid
    MonkeyRunner使用小结
    Ubuntu12.04 开机启动菜单 删除多余的内核(旧版本) 调整不同操作系统的启动顺序
    安卓Android手机上邮箱的设置通用方法
    WCDMA PPP test
    unbunt配置环境变量
    书摘《FBI教你读心术》
    ms sql server 2000 全文索引
  • 原文地址:https://www.cnblogs.com/kendoziyu/p/PathResourceResolver_ClassPathResource.html
Copyright © 2020-2023  润新知