美文网首页IT@程序员猿媛
【Spring 笔记】资源加载策略相关整理

【Spring 笔记】资源加载策略相关整理

作者: 58bc06151329 | 来源:发表于2019-09-14 12:17 被阅读0次

文前说明

作为码农中的一员,需要不断的学习,我工作之余将一些分析总结和学习笔记写成博客与大家一起交流,也希望采用这种方式记录自己的学习之旅。

本文仅供学习交流使用,侵权必删。
不用于商业目的,转载请注明出处。

1. 概述

  • Java SE 中有一个标准类 java.net.URL,该类定位为统一资源定位器(Uniform Resource Locator),但是它的实现基本只限于网络形式发布的资源的查找和定位。
    • 实际上资源的定义比较广泛,除了网络形式的资源,还有以二进制形式存在的、以文件形式存在的、以字节流形式存在的等。
    • 实际上资源可以存在于任何场所,比如网络、文件系统、应用程序中。
  • 因为 java.net.URL 的局限性,所以 Spring 实现了自己的资源加载策略,满足了如下要求。
    • 职能划分清晰,资源的定义资源的加载 有一个清晰的界限。
    • 有着统一的资源定义和资源加载策略,资源加载后返回统一的抽象给客户端,客户端对资源的处理由抽象资源接口界定。

2. 原理

2.1 统一的资源定义(Resource)

  • org.springframework.core.io.Resource 是 Spring 框架所有资源的抽象和访问接口,它继承 org.springframework.core.io.InputStreamSource 接口。
    • 作为所有资源的统一抽象,Resource 定义了一些通用的方法,由子类 AbstractResource 提供统一的默认实现。
public interface Resource extends InputStreamSource {
    /**
     * 资源是否存在
     */
    boolean exists();
    /**
     * 资源是否可读
     */
    boolean isReadable();
    /**
     * 资源所代表的句柄是否被一个 stream 打开了
     */
    boolean isOpen();
    /**
     * 返回资源的 URL 的句柄
     */
    URL getURL() throws IOException;
    /**
     * 返回资源的 URI 的句柄
     */
    URI getURI() throws IOException;
    /**
     * 返回资源的 File 的句柄
     */
    File getFile() throws IOException;
    /**
     * 资源内容的长度
     */
    long contentLength() throws IOException;
    /**
     * 资源最后的修改时间
     */
    long lastModified() throws IOException;
    /**
     * 根据资源的相对路径创建新资源
     */
    Resource createRelative(String var1) throws IOException;
    /**
     * 资源的文件名
     */
    String getFilename();
    /**
     * 资源的描述
     */
    String getDescription();
}

2.1.1 类结构图

Resource 体系
  • 根据资源的不同类型提供不同的具体实现。
    • FileSystemResourcejava.io.File 类型资源的封装,支持文件和 URL 的形式,实现 WritableResource 接口,从 Spring Framework 5.0 开始,FileSystemResource 使用 NIO2 API进行读/写交互。
    • ByteArrayResource 对字节数组提供的数据的封装。通过 InputStream 形式访问该类型的资源,该实现会根据字节数组的数据构造一个相应的 ByteArrayInputStream
    • UrlResourcejava.net.URL 类型资源的封装。内部委派 URL 进行具体的资源操作。
    • ClassPathResource class path 类型资源的实现。使用给定的 ClassLoader 或者给定的 Class 来加载资源。
    • InputStreamResource 将给定的 InputStream 作为一种资源的 Resource 的实现类。

ServletContextResource

  • org.springframework.web.context.support.ServletContextResource 类继承 AbstractFileResolvingResource 类,并实现 ContextResource 接口。
    • 是为了访问 web 容器上下文中的资源而设计的类,负责以相对于 web 应用程序根目录的路径加载资源,它支持以流和 URL 的方式访问。
    • 在 war 解包的情况下,也可以通过 File 的方式访问,还可以直接从 jar 包中访问资源。

2.1.2 AbstractResource

  • org.springframework.core.io.AbstractResourceResource 接口的默认抽象实现,实现了 Resource 接口的大部分的 公共实现
public abstract class AbstractResource implements Resource {

    public AbstractResource() {
    }
    /**
     * 判断文件是否存在,若判断过程产生异常(因为会调用 SecurityManager 来判断),就关闭对应的流
     */
    public boolean exists() {
        try {
          // 基于 File 进行判断
            return getFile().exists();
        }
        catch (IOException ex) {
            // Fall back to stream existence: can we open the stream?
            // 基于 InputStream 进行判断
            try {
                InputStream is = getInputStream();
                is.close();
                return true;
            } catch (Throwable isEx) {
                return false;
            }
        }
    }
    /**
     * 直接返回 true,表示可读
     */
    public boolean isReadable() {
        return true;
    }
    /**
     * 直接返回 false,表示未被打开
     */
    public boolean isOpen() {
        return false;
    }
    /**
     * 抛出 FileNotFoundException 异常,交给子类实现
     */
    public URL getURL() throws IOException {
        throw new FileNotFoundException(getDescription() + " cannot be resolved to URL");

    }
    /**
     * 基于 getURL() 返回的 URL 构建 URI
     */
    public URI getURI() throws IOException {
        URL url = getURL();
        try {
            return ResourceUtils.toURI(url);
        } catch (URISyntaxException ex) {
            throw new NestedIOException("Invalid URI [" + url + "]", ex);
        }
    }
    /**
     * 抛出 FileNotFoundException 异常,交给子类实现
     */
    public File getFile() throws IOException {
        throw new FileNotFoundException(getDescription() + " cannot be resolved to absolute file path");
    }
    /**
     * 获取资源的长度
     *
     * 这个资源内容长度实际就是资源的字节长度,通过全部读取一遍来判断
     */
    public long contentLength() throws IOException {
        InputStream is = getInputStream();
        try {
            long size = 0;
            byte[] buf = new byte[255]; // 每次最多读取 255 字节
            int read;
            while ((read = is.read(buf)) != -1) {
                size += read;
            }
            return size;
        } finally {
            try {
                is.close();
            } catch (IOException ex) {
            }
        }
    }
    /**
     * 返回资源最后的修改时间
     */
    public long lastModified() throws IOException {
        long lastModified = getFileForLastModifiedCheck().lastModified();
        if (lastModified == 0L) {
            throw new FileNotFoundException(getDescription() +
                    " cannot be resolved in the file system for resolving its last-modified timestamp");
        }
        return lastModified;
    }
    /**
     * 抛出 FileNotFoundException 异常,交给子类实现
     */
    public Resource createRelative(String relativePath) throws IOException {
        throw new FileNotFoundException("Cannot create a relative resource for " + getDescription());
    }
    /**
     * 获取资源名称,默认返回 null ,交给子类实现
     */
    public String getFilename() {
        return null;
    }
    /**
     * 返回资源的描述
     */
    public String toString() {
        return getDescription();
    }
    public boolean equals(Object obj) {
        return obj == this || obj instanceof Resource && ((Resource)obj).getDescription().equals(this.getDescription());
    }

    public int hashCode() {
        return this.getDescription().hashCode();
    }
}

// java.io.File.java
public boolean exists() {
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        security.checkRead(path);
    }
    if (isInvalid()) {
        return false;
    }
    return ((fs.getBooleanAttributes(this) & FileSystem.BA_EXISTS) != 0);
}
  • 想要实现自定义的 Resource ,继承 AbstractResource 抽象类,根据当前的具体资源特性覆盖相应的方法即可。

FileSystemResource(文件类型资源)的定义。

public class FileSystemResource extends AbstractResource implements WritableResource {
    private final File file;
    private final String path;

    public FileSystemResource(File file) {
        Assert.notNull(file, "File must not be null");
        this.file = file;
        this.path = StringUtils.cleanPath(file.getPath());
    }

    public FileSystemResource(String path) {
        Assert.notNull(path, "Path must not be null");
        this.file = new File(path);
        this.path = StringUtils.cleanPath(path);
    }
    /**
     * 返回文件路径
     */
    public final String getPath() {
        return this.path;
    }
    /**
     * 判断文件是否存在,若判断过程产生异常(因为会调用 SecurityManager 来判断),就关闭对应的流
     */
    public boolean exists() {
        return this.file.exists();
    }
    /**
     * 返回 true,表示可读
     */
    public boolean isReadable() {
        return this.file.canRead() && !this.file.isDirectory();
    }
    /**
     * 返回文件输入流
     */
    public InputStream getInputStream() throws IOException {
        return new FileInputStream(this.file);
    }
    /**
     * 返回 true,表示可写
     */
    public boolean isWritable() {
        return this.file.canWrite() && !this.file.isDirectory();
    }
    /**
     * 返回文件输出流
     */
    public OutputStream getOutputStream() throws IOException {
        return new FileOutputStream(this.file);
    }
    /**
     * 返回资源的 URL 的句柄
     */
    public URL getURL() throws IOException {
        return this.file.toURI().toURL();
    }
    /**
     * 返回资源的 URI 的句柄
     */
    public URI getURI() throws IOException {
        return this.file.toURI();
    }
    /**
     * 返回资源的 File 的句柄
     */
    public File getFile() {
        return this.file;
    }
    /**
     * 资源内容的长度
     */
    public long contentLength() throws IOException {
        return this.file.length();
    }
    /**
     * 根据资源的相对路径创建新资源
     */
    public Resource createRelative(String relativePath) {
        String pathToUse = StringUtils.applyRelativePath(this.path, relativePath);
        return new FileSystemResource(pathToUse);
    }
    /**
     * 资源的文件名
     */
    public String getFilename() {
        return this.file.getName();
    }
    /**
     * 资源的描述
     */
    public String getDescription() {
        return "file [" + this.file.getAbsolutePath() + "]";
    }

    public boolean equals(Object obj) {
        return obj == this || obj instanceof FileSystemResource && this.path.equals(((FileSystemResource)obj).path);
    }

    public int hashCode() {
        return this.path.hashCode();
    }
}
  • ByteArrayResourceUrlResourceClassPathResourceInputStreamResource 不同类型资源的定义与其类似。

2.2 统一的资源加载(ResourceLoader)

  • 资源的加载则由 ResourceLoader 来统一定义,org.springframework.core.io.ResourceLoader 为 Spring 资源加载的统一抽象,具体的资源加载则由相应的实现类来完成,可以将 ResourceLoader 称作为 统一资源定位器
  • ResourceLoader(定义资源加载器)主要应用于根据给定的资源文件地址,返回对应的 Resource
public interface ResourceLoader {
    String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX; // CLASSPATH URL 前缀。默认为:"classpath:"
    Resource getResource(String location);
    ClassLoader getClassLoader();
}
  • getResource(String location) 方法根据所提供资源的路径 location 返回 Resource 实例,但是不确保该 Resource 一定存在,需要调用 Resourceexist() 方法来判断。
    • 该方法支持 URL 位置资源(如:file:C:/test.dat),ClassPath 位置资源(如:classpath:test.dat),相对路径资源(如:WEB-INF/test.dat)几种模式的资源加载。
    • 该方法的主要实现是在其子类 DefaultResourceLoader 中实现。
  • getClassLoader() 方法,返回 ClassLoader 实例,可以直接获取 ResourceLoader 中使用的 ClassLoader
    • ClassPathResource 这个类就可以根据指定的 ClassLoader 来加载资源。

2.2.1 类结构图

ResourceLoader 体系
  • Spring 统一的资源加载器,提供了统一的抽象,具体的实现则由相应的子类来负责实现。

2.2.2 DefaultResourceLoader

  • org.springframework.core.io.DefaultResourceLoaderResourceLoader 的默认实现。
  • 在使用无参构造函数时,使用默认的 ClassLoaderClassUtils.getDefaultClassLoader())。
@Nullable
private ClassLoader classLoader;
public DefaultResourceLoader() { // 无参构造函数
    this.classLoader = ClassUtils.getDefaultClassLoader();
}
public DefaultResourceLoader(@Nullable ClassLoader classLoader) { // 带 ClassLoader 参数的构造函数
    this.classLoader = classLoader;
}
public void setClassLoader(@Nullable ClassLoader classLoader) {
    this.classLoader = classLoader;
}
@Override
@Nullable
public ClassLoader getClassLoader() {
    return (this.classLoader != null ? this.classLoader : ClassUtils.getDefaultClassLoader());
}
  • 也可以调用 setClassLoader() 方法进行设置。

getResource 方法实现

// DefaultResourceLoader.java

@Override
public Resource getResource(String location) {
    Assert.notNull(location, "Location must not be null");

    // 1. 通过 ProtocolResolver 加载资源
    for (ProtocolResolver protocolResolver : this.protocolResolvers) {
        Resource resource = protocolResolver.resolve(location, this);
        if (resource != null) {
            return resource;
        }
    }
    // 2. 以 / 开头,返回 ClassPathContextResource 类型的资源
    if (location.startsWith("/")) {
        return getResourceByPath(location);
    // 3. 以 classpath: 开头,返回 ClassPathResource 类型的资源
    } else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
        return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
    // 4. 根据是否为文件 URL ,是则返回 FileUrlResource 类型的资源,否则返回 UrlResource 类型的资源
    } else {
        try {
            // Try to parse the location as a URL...
            URL url = new URL(location);
            return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url));
        } catch (MalformedURLException ex) {
            // 5. 返回 ClassPathContextResource 类型的资源
            // No URL -> resolve as resource path.
            return getResourceByPath(location);
        }
    }
}
  • 方法的调用流程。
    • 步骤 1,通过 ProtocolResolver 加载资源,成功返回 Resource
    • 步骤 2,如果 location 以 " / " 开头,则调用 getResourceByPath() 方法,构造 ClassPathContextResource 类型资源并返回。
    • 步骤 3,如果 location 以 " classpath: " 开头,则构造 ClassPathResource 类型资源并返回。
      • 在构造该资源时,通过 getClassLoader() 方法获取当前 ClassLoader
    • 步骤 4,构造 URL ,尝试通过它进行资源定位,若没有抛出 MalformedURLException 异常,则判断是否为 FileURL , 如果是则构造 FileUrlResource 类型的资源,否则构造 UrlResource 类型的资源。
    • 步骤 5,如果在加载过程中抛出 MalformedURLException 异常,则委派 getResourceByPath() 方法,实现资源定位加载,与 <步骤 2> 的调用相同。

ProtocolResolver(用户自定义协议资源解决策略)

  • org.springframework.core.io.ProtocolResolver 允许用户自定义资源加载协议。
  • ProtocolResolver 接口,仅有一个方法 Resource resolve(String location, ResourceLoader resourceLoader)
/**
 * 使用指定的 ResourceLoader ,解析指定的 location 。
 * 若成功,则返回对应的 Resource 。
 *
 * Resolve the given location against the given resource loader
 * if this implementation's protocol matches.
 * @param location the user-specified resource location 资源路径
 * @param resourceLoader the associated resource loader 指定的加载器 ResourceLoader
 * @return a corresponding {@code Resource} handle if the given location
 * matches this resolver's protocol, or {@code null} otherwise 返回为相应的 Resource
 */
@Nullable
Resource resolve(String location, ResourceLoader resourceLoader);
  • 自定义的 Resolver 调用 DefaultResourceLoader#addProtocolResolver(ProtocolResolver)方法即可使用该策略。
/**
 * ProtocolResolver 集合
 */
private final Set<ProtocolResolver> protocolResolvers = new LinkedHashSet<>(4);

public void addProtocolResolver(ProtocolResolver resolver) {
    Assert.notNull(resolver, "ProtocolResolver must not be null");
    this.protocolResolvers.add(resolver);
}

2.2.3 FileSystemResourceLoader

  • 继承 DefaultResourceLoader,且覆写了 getResourceByPath(String) 方法,使之从文件系统加载资源并以 FileSystemResource 类型返回,获得文件系统资源类型。
  • FileSystemContextResource,是 FileSystemResourceLoader 的内部类,继承了 FileSystemResource 类,并实现了 ContextResource 接口。
@Override
protected Resource getResourceByPath(String path) {
    // 截取首 /
    if (path.startsWith("/")) {
        path = path.substring(1);
    }
    // 创建 FileSystemContextResource 类型的资源
    return new FileSystemContextResource(path);
}

2.2.4 ClassRelativeResourceLoader

  • org.springframework.core.io.ClassRelativeResourceLoader 也是 DefaultResourceLoader 的子类实现。和 FileSystemResourceLoader 类似,也覆写了 getResourceByPath(String path) 方法,并返回其对应的 ClassRelativeContextResource 资源类型。
  • ClassRelativeContextResource 对象表示上下文相对路径,可以根据给定的 class 所在包或者所在包的子包下加载资源,如下,启动服务器,可以加载资源成功。
@Controller
public class LoginController  {
            
    @RequestMapping(value="/index.html")
    public String loginPage() throws IOException {
        
        ResourceLoader resourceLoader=new ClassRelativeResourceLoader(this.getClass());
        Resource resource=resourceLoader.getResource("test.xml");
        System.out.println(resource.getFile().getPath());
        return "index";
    }   
}

2.2.5 ResourcePatternResolver

  • ResourceLoadergetResource(String location) 方法,每次只能根据 location 返回一个 Resource
  • org.springframework.core.io.support.ResourcePatternResolverResourceLoader 的扩展,支持根据指定的资源路径匹配模式每次返回多个 Resource 实例。
public interface ResourcePatternResolver extends ResourceLoader {
    String CLASSPATH_ALL_URL_PREFIX = "classpath*:";
    Resource[] getResources(String locationPattern) throws IOException;
}
  • ResourcePatternResolverResourceLoader 的基础上增加了 getResources(String locationPattern) 方法,以支持根据路径匹配模式返回 多个 Resource 实例。
  • 新增了一种新的协议前缀 " classpath*: ",该协议前缀由其子类负责实现。

2.2.6 PathMatchingResourcePatternResolver

  • org.springframework.core.io.support.PathMatchingResourcePatternResolverResourcePatternResolver 最常用的子类,除了支持 ResourceLoaderResourcePatternResolver 新增的 " classpath*: " 前缀外,还支持 Ant 风格 的路径匹配模式(类似于 " **/*.xml ")。
/**
 * 内置的 ResourceLoader 资源定位器
 */
private final ResourceLoader resourceLoader;
/**
 * Ant 路径匹配器
 */
private PathMatcher pathMatcher = new AntPathMatcher();
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);
}
  • PathMatchingResourcePatternResolver 实例化时,可以指定 ResourceLoader,不指定则构造一个 DefaultResourceLoader
  • 默认的 AntPathMatcher 对象,用于支持 Ant 类型的路径匹配。

getResource 方法实现

  • 如果在实例化 PathMatchingResourcePatternResolver 时,未指定 ResourceLoader 参数,在加载资源时,则执行 DefaultResourceLoader 过程。
@Override
public Resource getResource(String location) {
    return getResourceLoader().getResource(location);
}
public ResourceLoader getResourceLoader() {
    return this.resourceLoader;
}

getResources 方法实现

@Override
public Resource[] getResources(String locationPattern) throws IOException {
    Assert.notNull(locationPattern, "Location pattern must not be null");
    // 以 "classpath*:" 开头
    if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
        // 路径包含通配符
        // a class path resource (multiple resources for same name possible)
        if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) {
            // a class path resource pattern
            return findPathMatchingResources(locationPattern);
        // 路径不包含通配符
        } else {
            // all class path resources with the given name
            return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));
        }
    // 不以 "classpath*:" 开头
    } else {
        // Generally only look for a pattern after a prefix here, // 通常只在这里的前缀后面查找模式
        // and on Tomcat only after the "*/" separator for its "war:" protocol. 而在 Tomcat 上只有在 “*/ ”分隔符之后才为其 “war:” 协议
        int prefixEnd = (locationPattern.startsWith("war:") ? locationPattern.indexOf("*/") + 1 :
                locationPattern.indexOf(':') + 1);
        // 路径包含通配符
        if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {
            // a file pattern
            return findPathMatchingResources(locationPattern);
        // 路径不包含通配符
        } else {
            // a single resource with the given name
            return new Resource[] {getResourceLoader().getResource(locationPattern)};
        }
    }
}
  • 方法的调用流程。
    • 步骤 1,非 " classpath*: " 开头,且路径不包含通配符,直接委托给相应的 ResourceLoader 实现。
    • 步骤 2,其他情况,调用 findAllClassPathResources()、或 findPathMatchingResources() 方法,返回多个 Resource

findAllClassPathResources 方法实现

  • 当 locationPattern 以 " classpath*: " 开头但是不包含通配符,则调用 findAllClassPathResources() 方法加载资源。
    • 该方法返回 classes 路径下和所有 jar 包中的所有相匹配的资源。
protected Resource[] findAllClassPathResources(String location) throws IOException {
    String path = location;
    // 去除首个 /
    if (path.startsWith("/")) {
        path = path.substring(1);
    }
    // 真正执行加载所有 classpath 资源
    Set<Resource> result = doFindAllClassPathResources(path);
    if (logger.isTraceEnabled()) {
        logger.trace("Resolved classpath location [" + location + "] to resources " + result);
    }
    // 转换成 Resource 数组返回
    return result.toArray(new Resource[0]);
}

doFindAllClassPathResources 方法实现

protected Set<Resource> doFindAllClassPathResources(String path) throws IOException {
    Set<Resource> result = new LinkedHashSet<>(16);
    ClassLoader cl = getClassLoader();
    // 1. 根据 ClassLoader 加载路径下的所有资源
    Enumeration<URL> resourceUrls = (cl != null ? cl.getResources(path) : ClassLoader.getSystemResources(path));
    // 2.
    while (resourceUrls.hasMoreElements()) {
        URL url = resourceUrls.nextElement();
        // 将 URL 转换成 UrlResource
        result.add(convertClassLoaderURL(url));
    }
    // 3. 加载路径下得所有 jar 包
    if ("".equals(path)) {
        // The above result is likely to be incomplete, i.e. only containing file system references.
        // We need to have pointers to each of the jar files on the classpath as well...
        addAllClassLoaderJarRoots(cl, result);
    }
    return result;
}

protected Resource convertClassLoaderURL(URL url) {
    return new UrlResource(url);
}

// java.lang.ClassLoader.java
public Enumeration<URL> getResources(String name) throws IOException {
    @SuppressWarnings("unchecked")
    Enumeration<URL>[] tmp = (Enumeration<URL>[]) new Enumeration<?>[2];
    if (parent != null) {
        tmp[0] = parent.getResources(name);
    } else {
        tmp[0] = getBootstrapResources(name);
    }
    tmp[1] = findResources(name);

    return new CompoundEnumeration<>(tmp);
}
  • 方法的调用流程。
    • 步骤 1,根据 ClassLoader 加载路径下的所有资源。在加载资源过程时,如果在构造 PathMatchingResourcePatternResolver 实例时传入 ClassLoader,则调用该 ClassLoadergetResources() 方法,否则调用 ClassLoader#getSystemResources(path) 方法。
    • 步骤 2,遍历 URL 集合,调用 convertClassLoaderURL(URL url) 方法,将 URL 转换成 UrlResource 对象。
    • 步骤 3,若 path 为空字符串时,则调用 addAllClassLoaderJarRoots() 方法,加载路径下得所有 jar 包。
  • findAllClassPathResources() 方法利用 ClassLoader 加载指定路径下的资源,不论它是在 class 路径下还是在 jar 包中。如果传入的路径为空或者 " / ",则调用 addAllClassLoaderJarRoots() 方法,加载所有的 jar 包。

findPathMatchingResources 方法实现

  • 当 locationPattern 中包含了通配符,则调用该方法进行资源加载。
protected Resource[] findPathMatchingResources(String locationPattern) throws IOException {
    // 确定根路径、子路径
    String rootDirPath = determineRootDir(locationPattern);
    String subPattern = locationPattern.substring(rootDirPath.length());
    // 获取根据路径下的资源
    Resource[] rootDirResources = getResources(rootDirPath);
    // 遍历,迭代
    Set<Resource> result = new LinkedHashSet<>(16);
    for (Resource rootDirResource : rootDirResources) {
        rootDirResource = resolveRootDirResource(rootDirResource);
        URL rootDirUrl = rootDirResource.getURL();
        // bundle 资源类型
        if (equinoxResolveMethod != null && rootDirUrl.getProtocol().startsWith("bundle")) {
            URL resolvedUrl = (URL) ReflectionUtils.invokeMethod(equinoxResolveMethod, null, rootDirUrl);
            if (resolvedUrl != null) {
                rootDirUrl = resolvedUrl;
            }
            rootDirResource = new UrlResource(rootDirUrl);
        }
        // vfs 资源类型
        if (rootDirUrl.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {
            result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirUrl, subPattern, getPathMatcher()));
        // jar 资源类型
        } else if (ResourceUtils.isJarURL(rootDirUrl) || isJarResource(rootDirResource)) {
            result.addAll(doFindPathMatchingJarResources(rootDirResource, rootDirUrl, subPattern));
        // 其它资源类型
        } else {
            result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern));
        }
    }
    if (logger.isTraceEnabled()) {
        logger.trace("Resolved location pattern [" + locationPattern + "] to resources " + result);
    }
    // 转换成 Resource 数组返回
    return result.toArray(new Resource[0]);
}
  • 方法的调用流程。
    • 步骤 1,确定目录,获取该目录下得所有资源。
    • 步骤 2,在获得所有资源后,进行迭代匹配获取想要的资源。

determineRootDir 方法实现

  • 主要用于确定根路径。
protected String determineRootDir(String location) {
    // 找到冒号的后一位
    int prefixEnd = location.indexOf(':') + 1;
    // 根目录结束位置
    int rootDirEnd = location.length();
    // 在从冒号开始到最后的字符串中,循环判断是否包含通配符,如果包含,则截断最后一个由”/”分割的部分。
    // 再循环判断剩下的部分,直到剩下的路径中都不包含通配符。
    while (rootDirEnd > prefixEnd && getPathMatcher().isPattern(location.substring(prefixEnd, rootDirEnd))) {
        rootDirEnd = location.lastIndexOf('/', rootDirEnd - 2) + 1;
    }
    // 如果查找完成后,rootDirEnd = 0 了,则将之前赋值的 prefixEnd 的值赋给 rootDirEnd ,也就是冒号的后一位
    if (rootDirEnd == 0) {
        rootDirEnd = prefixEnd;
    }
    // 截取根目录
    return location.substring(0, rootDirEnd);
}
  • 原路径(classpath*:test/cc*/spring-*.xml)获取的确定根路径(classpath*:test/
  • 原路径(classpath*:test/aa/spring-*.xml)获取的确定根路径(classpath*:test/aa/

doFindPathMatchingJarResources 方法实现

  • 方法的调用流程。
    • 步骤 1,计算当前 Resource 在 jar 文件中的根路径 rootEntryPath。
    • 步骤 2,遍历 jar 文件中所有 entry,如果当前 entry 名以 rootEntryPath 开头,并且之后的路径信息和之前从 patternLocation 中截取出的 subPattern 使用 PathMatcher 匹配,若匹配成功,则调用 rootDirResource#createRelative 方法创建一个 Resource,将新创建的 Resource 添加入结果集中。

doFindPathMatchingFileResources 方法实现

  • 方法的调用流程。
    • 步骤 1,获取要查找资源的根路径(根路径全名)。
    • 步骤 2,递归获得根路径下的所有资源,使用 PathMatcher 匹配,如果匹配成功,则创建 FileSystemResource,并将其加入到结果集中。
      • 在递归进入一个目录前首先调用 PathMatcher#matchStart() 方法,用以先简单的判断是否需要递归,以提升性能。
protected void doRetrieveMatchingFiles(String fullPattern, File dir, Set<File> result) throws IOException {
    if (logger.isDebugEnabled()) {
        logger.debug("Searching directory [" + dir.getAbsolutePath() +
                "] for files matching pattern [" + fullPattern + "]");
    }
    //从根目录开始罗列文件集合
    File[] dirContents = dir.listFiles();
    if (dirContents == null) {
        //查找到没有了则直接返回
        if (logger.isWarnEnabled()) {
            logger.warn("Could not retrieve contents of directory [" + dir.getAbsolutePath() + "]");
        }
        return;
    }
    //遍历
    for (File content : dirContents) {
        //获取当前文件路径
        String currPath = StringUtils.replace(content.getAbsolutePath(), File.separator, "/");
        //查找到的子文件仍是目录且以根目录为开头
        if (content.isDirectory() && getPathMatcher().matchStart(fullPattern, currPath + "/")) {
            if (!content.canRead()) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Skipping subdirectory [" + dir.getAbsolutePath() +
                            "] because the application is not allowed to read the directory");
                }
            }
            else {
                //递归调用查找所有的文件
                doRetrieveMatchingFiles(fullPattern, content, result);
            }
        }
        //查看当前文件路径是否满足**/*.class格式,满足则添加
        if (getPathMatcher().match(fullPattern, currPath)) {
            result.add(content);
        }
    }
}
  • 需要注意的是,由于 ClassLoader#getResources() 方法存在的限制,当传入一个空字符串时,只能从 classpath 的文件目录下查找,而不会从 jar 文件的根目录下查找,因而对 " classpath*: " 前缀的资源来说,会找不到 jar 根路径下的资源。
    • 如果类似定义 " classpath*:*.xml ",并且只有在 jar 文件的根目录下存在 XML 配置文件,那么这个 pattern 将返回空的 Resource 数组。
    • 解决方法是不要在 jar 文件的根目录中放配置文件,可以将这些配置文件放到 jar 文件中的resources、config 等目录下。

2.2.7 ServletContextResourcePatternResolver

  • ServletContextResourcePatternResolver 类继承 PathMatchingResourcePatternResolver 类,重写了文件查找逻辑,对 ServletContextResource 资源使用 ServletContext.getResourcePaths() 方法来查找参数目录下的文件,而不是 File#listFiles() 方法。
protected Set<Resource> doFindPathMatchingFileResources(Resource rootDirResource, String subPattern) throws IOException {
    if (rootDirResource instanceof ServletContextResource) {
        ServletContextResource scResource = (ServletContextResource)rootDirResource;
        ServletContext sc = scResource.getServletContext();
        String fullPattern = scResource.getPath() + subPattern;
        Set<Resource> result = new LinkedHashSet(8);
        this.doRetrieveMatchingServletContextResources(sc, fullPattern, scResource.getPath(), result);
        return result;
    } else {
        return super.doFindPathMatchingFileResources(rootDirResource, subPattern);
    }
}

2.3 小结

  • Spring 提供 ResourceResourceLoader 统一抽象整个资源及其定位,使资源与资源的定位有了一个清晰的界限,提供了合适的默认类,使自定义实现更加方便和清晰。
    • AbstractResourceResource 的默认抽象实现,对 Resource 接口做了统一的实现,子类继承该类后只需要覆盖相应的方法即可,同时对于自定义的 Resource 也需要继承该类。
    • DefaultResourceLoaderResourceLoader 的默认实现,自定义 ResourceLoader 时除了可以继承该类以外还可以通过实现 ProtocolResolver 接口来实现自定资源加载协议。
    • DefaultResourceLoader 每次只能返回单一的资源,而 ResourcePatternResolver 接口,根据指定的 locationPattern 返回多个资源,其子类 PathMatchingResourcePatternResolver 即实现了 getResource(String location) 方法,也实现了 getResources(String locationPattern) 方法。
  • ResourceResourceLoader 的核心都包含在 spring-core 项目中。

相关文章

  • 【Spring 笔记】资源加载策略相关整理

    文前说明作为码农中的一员,需要不断的学习,我工作之余将一些分析总结和学习笔记写成博客与大家一起交流,也希望采用这种...

  • Spring IoC 体系分析

    一. 统一资源加载策略: Spring 将资源的定义和资源的加载区分出来资源描述接口: Resource资源加载规...

  • Spring FactoryBean 缓存

    相关文章 Spring 整体架构 编译Spring5.2.0源码 Spring-资源加载 Spring 容器的初始...

  • 【Spring 笔记】Bean 加载相关整理

    文前说明作为码农中的一员,需要不断的学习,我工作之余将一些分析总结和学习笔记写成博客与大家一起交流,也希望采用这种...

  • Spring Boot 资源预加载

    项目出现调用spring bean 资源出现空指针异常。经分析是spring 资源出现懒加载情况导致资源未加载,但...

  • Spring中的资源文件框架——Resource

    Spring中资源加载的框架 Spring对于资源加载有着一套自己的框架——Resource,Resource继承...

  • 大厂面试系列(四):Spring相关

    Spring相关 spring工作原理简单介绍 循环依赖问题 spring/spring boot启动加载过程 ?...

  • 【Spring 笔记】基础相关整理

    文前说明作为码农中的一员,需要不断的学习,我工作之余将一些分析总结和学习笔记写成博客与大家一起交流,也希望采用这种...

  • 【Spring 笔记】总结相关整理

    文前说明作为码农中的一员,需要不断的学习,我工作之余将一些分析总结和学习笔记写成博客与大家一起交流,也希望采用这种...

  • 【Spring 笔记】BeanWrapper 相关整理

    文前说明作为码农中的一员,需要不断的学习,我工作之余将一些分析总结和学习笔记写成博客与大家一起交流,也希望采用这种...

网友评论

    本文标题:【Spring 笔记】资源加载策略相关整理

    本文链接:https://www.haomeiwen.com/subject/airiyctx.html