[Spring][Ngbatis源码学习] Spring 的资源管理 ResourceLoader

发布时间 2023-12-17 02:09:11作者: knqiufan

在学习Ngbatis的源码时,看到了有关xml文件的加载,涉及到了资源的加载,对相关知识进行总结与整理。

1. 相关类

  • Resource
  • AbstractResource
  • ResourceLoader
  • DefaultResourceLoader
  • ResourcePatternResolver
  • PathMatchingResourcePatternResolver

以下逐一说明。

2. Resource

Resource 是 Spring 框架资源的抽象接口。用于表示应用程序中的各种资源,比如文件、类路径资源、URL等。

Resource 提供了同意的方式来访问这些资源,无论资源处于何处都可以通过 Resource 接口进行操作。

定义的接口如下:

public interface Resource extends InputStreamSource {
    // 某个资源是否以物理形式存在
    boolean exists();
    // 资源的目录读取是否可通过getInputStream()进行读取
    default boolean isReadable() {
    return this.exists();
    }
    // 判断资源是否具有开放流的句柄
    default boolean isOpen() {
    return false;
    }
    // 是否是一个文件
    default boolean isFile() {
    return false;
    }
    // 返回一个URL句柄,如果资源不能够被解析为URL,将抛出IOException
    URL getURL() throws IOException;
    // 返回一个URI句柄,如果资源不能够被解析为URL,将抛出IOException
    URI getURI() throws IOException;
    // 返回文件
    File getFile() throws IOException;
    // 获取可读取的字节通道。在任何时间可读通道上只能有一个读操作正在进行
    default ReadableByteChannel readableChannel() throws IOException {
    return Channels.newChannel(this.getInputStream());
    }
    // 获取content长度
    long contentLength() throws IOException;
    // 资源最后一次修改的时间戳
    long lastModified() throws IOException;
    // 创建此资源的相关资源
    Resource createRelative(String relativePath) throws IOException;
    // 获取资源的文件名
    @Nullable
    String getFilename();
    // 获取资源描述
    String getDescription();
}

Resource 接口的实现类有很多,比如 UrlResource、ClassPathResource、InputStreamResource、FileSystemResource等等,看名称可知道作用,不多赘述。

3. AbstractResource

AbstractResource 是 Resource 的默认实现类,实现了 Resource 大部分方法。

如果需要实现自定义的 Resource 接口,直接去继承 AbstractResource 抽象类,并根据需求重写相关方法就好。

4. ResourceLoader

ResourceLoader 接口提供了一个资源加载策略,是 Spring 资源加载的同一抽象,具体的资源加载由相应的实现类完成。默认实现类是 DafultResourceLoader.。

public interface ResourceLoader {
    String CLASSPATH_URL_PREFIX = "classpath:";
    // 根据所提供的路径 location 获取 Resource 实例,但是不确保 Resource 一定存在。
    // 支持 URL、ClassPath、相对路径资源
    Resource getResource(String location);
    // 返回 ClassLoader 实例
    @Nullable
    ClassLoader getClassLoader();
}

可以看到 ResourceLoader 提供了两个接口:getResource、getClassLoader。

5. DefaultResourceLoader

DefaultResourceLoader 是 ResourceLoader 的默认实现。代码如下:

public class DefaultResourceLoader implements ResourceLoader {
  @Nullable
  private ClassLoader classLoader;
  private final Set<ProtocolResolver> protocolResolvers = new LinkedHashSet(4);
  private final Map<Class<?>, Map<Resource, ?>> resourceCaches = new ConcurrentHashMap(4);

  public DefaultResourceLoader() {
  }

  public DefaultResourceLoader(@Nullable ClassLoader classLoader) {
    this.classLoader = classLoader;
  }

  public void setClassLoader(@Nullable ClassLoader classLoader) {
    this.classLoader = classLoader;
  }

  @Nullable
  public ClassLoader getClassLoader() {
    return this.classLoader != null ? this.classLoader : ClassUtils.getDefaultClassLoader();
  }
  // 添加 ProtocolResolver
  public void addProtocolResolver(ProtocolResolver resolver) {
    Assert.notNull(resolver, "ProtocolResolver must not be null");
    this.protocolResolvers.add(resolver);
  }
  // 获取 ProtocolResolver 集合
  public Collection<ProtocolResolver> getProtocolResolvers() {
    return this.protocolResolvers;
  }

  public <T> Map<Resource, T> getResourceCache(Class<T> valueType) {
    return (Map)this.resourceCaches.computeIfAbsent(valueType, (key) -> {
      return new ConcurrentHashMap();
    });
  }

  public void clearResourceCaches() {
    this.resourceCaches.clear();
  }
  // 根据 location 获取 Resource
  public Resource getResource(String location) {
    Assert.notNull(location, "Location must not be null");
    Iterator var2 = this.getProtocolResolvers().iterator();

    Resource resource;
    do {
      if (!var2.hasNext()) {
        if (location.startsWith("/")) {
          return this.getResourceByPath(location);
        }

        if (location.startsWith("classpath:")) {
          return new ClassPathResource(location.substring("classpath:".length()), this.getClassLoader());
        }

        try {
          URL url = new URL(location);
          return (Resource)(ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url));
        } catch (MalformedURLException var5) {
          return this.getResourceByPath(location);
        }
      }

      ProtocolResolver protocolResolver = (ProtocolResolver)var2.next();
      resource = protocolResolver.resolve(location, this);
    } while(resource == null);

    return resource;
  }

  protected Resource getResourceByPath(String path) {
    return new ClassPathContextResource(path, this.getClassLoader());
  }

  protected static class ClassPathContextResource extends ClassPathResource implements ContextResource {
    public ClassPathContextResource(String path, @Nullable ClassLoader classLoader) {
      super(path, classLoader);
    }

    public String getPathWithinContext() {
      return this.getPath();
    }

    public Resource createRelative(String relativePath) {
      String pathToUse = StringUtils.applyRelativePath(this.getPath(), relativePath);
      return new ClassPathContextResource(pathToUse, this.getClassLoader());
    }
  }
}

可以看到代码中涉及到了 ProtocolResolver。重点关注一下 ProtocolResolver 这个类。ProtocolResolver 与 DefaultResourceLoader 密不可分。

ProtocolResolver 翻译一下叫协议解析器,它允许用户自定义协议资源解析策略,作为 DefaultResourceLoader 的SPI,而不需要继承 ResourceLoader 的子类。

浅看一下 ProtocolResolver 的源码:

@FunctionalInterface
public interface ProtocolResolver {
  @Nullable
  Resource resolve(String location, ResourceLoader resourceLoader);
}

可以看到是个函数式接口,根据传入的 location 和自定义的 ResourceLoader 加载器解析出对应的 Resource 资源。

一般来说如果要实现自定义的 Resource,只需要继承 AbstractResource 就好。但有了 ProtocolResolver 就不需要直接继承 DefaultResourceLoader,实现了 ProtocolResolver 接口也可以实现自定义的 ResourceLoader。

回头看 DefaultResourceLoader 里的 getResource 方法,我们单独拿出来:

  // 根据 location 获取 Resource
  public Resource getResource(String location) {
    Assert.notNull(location, "Location must not be null");
	// 获取 ProtocolResolver 集合迭代器
    Iterator var2 = this.getProtocolResolvers().iterator();

    Resource resource;
    do {
      // 如果获取不到 ProtocolResolver 元素
      if (!var2.hasNext()) {
        // 如果是以 / 开头
        if (location.startsWith("/")) {
          return this.getResourceByPath(location);
        }
    	// 如果是以 classpath: 开头
        if (location.startsWith("classpath:")) {
          return new ClassPathResource(location.substring("classpath:".length()), this.getClassLoader());
        }

        try {
          // 构造 URL 进行资源定位
          URL url = new URL(location);
          return (Resource)(ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url));
        } catch (MalformedURLException var5) {
          return this.getResourceByPath(location);
        }
      }
	  // 获取 ProtocolResolver 并调用 resolve 接口获取 Resource
      ProtocolResolver protocolResolver = (ProtocolResolver)var2.next();
      resource = protocolResolver.resolve(location, this);
    } while(resource == null);

    return resource;
  }

6. ResourcePatternResolver

ResourcePatternResolver 是 ResourceLoader 的扩展。

代码如下:

public interface ResourcePatternResolver extends ResourceLoader {
  String CLASSPATH_ALL_URL_PREFIX = "classpath*:";

  Resource[] getResources(String locationPattern) throws IOException;
}

对比一下 ResourceLoader 的 getResource 方法就会发现,ResourceLoader 的 getResource 通过传入 location 来返回一个 Resource,当需要加载多个资源的时候就必须多次调用 getResourece。

而 ResourcePatternResolver 作为 ResourceLoader 的扩展定义了可以根据指定的资源路径匹配模式每次返回多个 Resource 实例。

可以看到新增了新的协议前缀 classpath*: 。

7. PathMatchingResourcePatternResolver

PathMatchingResourcePatternResolver 是 ResourcePatternResolver 很常用的子类,它新增了 Ant 风格的路径匹配模式。

扩展:Ant 风格匹配模式

Ant 风格的路径匹配规则是一种常用的路径模式匹配规则

包括以下几种模式:

  • ? : 匹配任意单个字符
  • * : 匹配任意数量,包括零个的字符
  • ** : 匹配任意数量,包括零个的目录路径

举例说明:

  • **/*.xml
  • /user/**
  • /user/*/profile

PathMatchingResourcePatternResolver 有三种构造方法:

  public PathMatchingResourcePatternResolver() {
    this.resourceLoader = new DefaultResourceLoader();
  }

  public PathMatchingResourcePatternResolver(ResourceLoader resourceLoader) {
    Assert.notNull(resourceLoader, "ResourceLoader must not be null");
    this.resourceLoader = resourceLoader;
  }

  public PathMatchingResourcePatternResolver(@Nullable ClassLoader classLoader) {
    this.resourceLoader = new DefaultResourceLoader(classLoader);
  }

可以看到,如果不指定 ResourceLoader 资源加载器的话,默认都是使用 DefaultResourceLoader。

关于获取 Resource 提供了两种方法:

  // 委托给相应的 ResourceLoader 获取
  public Resource getResource(String location) {
    return this.getResourceLoader().getResource(location);
  }

  public Resource[] getResources(String locationPattern) throws IOException {
    Assert.notNull(locationPattern, "Location pattern must not be null");
    // 是否以 classpath*: 开头
    if (locationPattern.startsWith("classpath*:")) {
      return this.getPathMatcher().isPattern(locationPattern.substring("classpath*:".length())) ? this.findPathMatchingResources(locationPattern) : this.findAllClassPathResources(locationPattern.substring("classpath*:".length()));
    } else {
      int prefixEnd = locationPattern.startsWith("war:") ? locationPattern.indexOf("*/") + 1 : locationPattern.indexOf(58) + 1;
      return this.getPathMatcher().isPattern(locationPattern.substring(prefixEnd)) ? this.findPathMatchingResources(locationPattern) : new Resource[]{this.getResourceLoader().getResource(locationPattern)};
    }
  }

getResource 方法很好理解,不多做赘述。

getResources 方法,逻辑的流程图如下:

getResources 中调用了 findAllClassPathResources 方法,该方法返回 classes 路径下和所有 jar 包中相匹配的资源。

  protected Resource[] findAllClassPathResources(String location) throws IOException {
    String path = location;
    if (location.startsWith("/")) {
      path = location.substring(1);
    }

    Set<Resource> result = this.doFindAllClassPathResources(path);
    if (logger.isTraceEnabled()) {
      logger.trace("Resolved classpath location [" + location + "] to resources " + result);
    }

    return (Resource[])result.toArray(new Resource[0]);
  }

  // 获取当前路径下的所有资源
  protected Set<Resource> doFindAllClassPathResources(String path) throws IOException {
    Set<Resource> result = new LinkedHashSet(16);
    ClassLoader cl = this.getClassLoader();
    Enumeration<URL> resourceUrls = cl != null ? cl.getResources(path) : ClassLoader.getSystemResources(path);

    while(resourceUrls.hasMoreElements()) {
      URL url = (URL)resourceUrls.nextElement();
      result.add(this.convertClassLoaderURL(url));
    }

    if (!StringUtils.hasLength(path)) {
      this.addAllClassLoaderJarRoots(cl, result);
    }

    return result;
  }

getResources 中还调用了另一个方法 findPathMatchingResources,主要分两步:

  1. 确定目录并获取目录下所有资源
  2. 在所有获取的资源中进行迭代匹配获取所需资源

代码有点长,就不贴了,有兴趣的小伙伴可以自行查看,位置是:

org.springframework.core.io.support.PathMatchingResourcePatternResolver#findPathMatchingResources

最后放一张 UML 图: